# -*- coding: utf-8 -*-
from __future__ import absolute_import
import hashlib
import re
import os
from binascii import hexlify, unhexlify
from .base58 import ripemd160, Base58, doublesha256
from .dictionary import words as BrainKeyDictionary
from .utils import _bytes
from .prefix import Prefix
import ecdsa
class PasswordKey(Prefix):
""" This class derives a private key given the account name, the
role and a password. It leverages the technology of Brainkeys
and allows people to have a secure private key by providing a
passphrase only.
"""
def __init__(self, account, password, role="active", prefix=None):
self.set_prefix(prefix)
self.account = account
self.role = role
self.password = password
[docs] def get_private(self):
""" Derive private key from the brain key and the current sequence
number
"""
a = _bytes(self.account + self.role + self.password)
s = hashlib.sha256(a).digest()
return PrivateKey(hexlify(s).decode("ascii"), prefix=self.prefix)
[docs] def get_public(self):
return self.get_private().pubkey
[docs] def get_private_key(self):
return self.get_private()
[docs] def get_public_key(self):
return self.get_public()
class BrainKey(Prefix):
"""Brainkey implementation similar to the graphene-ui web-wallet.
:param str brainkey: Brain Key
:param int sequence: Sequence number for consecutive keys
Keys in Graphene are derived from a seed brain key which is a string of
16 words out of a predefined dictionary with 49744 words. It is a
simple single-chain key derivation scheme that is not compatible with
BIP44 but easy to use.
Given the brain key, a private key is derived as::
privkey = SHA256(SHA512(brainkey + " " + sequence))
Incrementing the sequence number yields a new key that can be
regenerated given the brain key.
"""
def __init__(self, brainkey=None, sequence=0, prefix=None):
self.set_prefix(prefix)
if not brainkey:
self.brainkey = BrainKey.suggest()
else:
self.brainkey = self.normalize(brainkey).strip()
self.sequence = sequence
def __next__(self):
""" Get the next private key (sequence number increment) for
iterators
"""
return self.next_sequence()
[docs] def next_sequence(self):
""" Increment the sequence number by 1 """
self.sequence += 1
return self
[docs] def normalize(self, brainkey):
""" Correct formating with single whitespace syntax and no trailing
space
"""
return " ".join(re.compile("[\t\n\v\f\r ]+").split(brainkey))
[docs] def get_brainkey(self):
""" Return brain key of this instance """
return self.normalize(self.brainkey)
[docs] def get_private(self):
""" Derive private key from the brain key and the current sequence
number
"""
encoded = "%s %d" % (self.brainkey, self.sequence)
a = _bytes(encoded)
s = hashlib.sha256(hashlib.sha512(a).digest()).digest()
return PrivateKey(hexlify(s).decode("ascii"), prefix=self.prefix)
[docs] def get_blind_private(self):
""" Derive private key from the brain key (and no sequence number)
"""
a = _bytes(self.brainkey)
return PrivateKey(hashlib.sha256(a).hexdigest(), prefix=self.prefix)
[docs] def get_public(self):
return self.get_private().pubkey
[docs] def get_private_key(self):
return self.get_private()
[docs] def get_public_key(self):
return self.get_public()
[docs] @staticmethod
def suggest():
""" Suggest a new random brain key. Randomness is provided by the
operating system using ``os.urandom()``.
"""
word_count = 16
brainkey = [None] * word_count
dict_lines = BrainKeyDictionary.split(",")
assert len(dict_lines) == 49744
for j in range(0, word_count):
num = int.from_bytes(os.urandom(2), byteorder="little")
rndMult = num / 2 ** 16 # returns float between 0..1 (inclusive)
wIdx = round(len(dict_lines) * rndMult)
brainkey[j] = dict_lines[wIdx]
return " ".join(brainkey).upper()
class Address(Prefix):
""" Address class
This class serves as an address representation for Public Keys.
:param str address: Base58 encoded address (defaults to ``None``)
:param str pubkey: Base58 encoded pubkey (defaults to ``None``)
:param str prefix: Network prefix (defaults to ``GPH``)
Example::
Address("GPHFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi")
"""
def __init__(self, address, prefix=None):
self.set_prefix(prefix)
self._address = Base58(address, prefix=self.prefix)
[docs] @classmethod
def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None):
""" Load an address provided the public key.
Version: 56 => PTS
"""
# Ensure this is a public key
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
if compressed:
pubkey_plain = pubkey.compressed()
else:
pubkey_plain = pubkey.uncompressed()
sha = hashlib.sha256(unhexlify(pubkey_plain)).hexdigest()
rep = hexlify(ripemd160(sha)).decode("ascii")
s = ("%.2x" % version) + rep
result = s + hexlify(doublesha256(s)[:4]).decode("ascii")
result = hexlify(ripemd160(result)).decode("ascii")
return cls(result, prefix=pubkey.prefix)
def __repr__(self):
""" Gives the hex representation of the ``GrapheneBase58CheckEncoded``
Graphene address.
"""
return repr(self._address)
def __str__(self):
""" Returns the readable Graphene address. This call is equivalent to
``format(Address, "GPH")``
"""
return format(self._address, self.prefix)
def __format__(self, _format):
""" May be issued to get valid "MUSE", "PLAY" or any other Graphene compatible
address with corresponding prefix.
"""
return format(self._address, _format)
def __bytes__(self):
""" Returns the raw content of the ``Base58CheckEncoded`` address """
return bytes(self._address)
class GrapheneAddress(Address):
""" Graphene Addresses are different. Hence we have a different class
"""
@classmethod
def from_pubkey(cls, pubkey, compressed=True, version=56, prefix=None):
# Ensure this is a public key
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
if compressed:
pubkey_plain = pubkey.compressed()
else:
pubkey_plain = pubkey.uncompressed()
""" Derive address using ``RIPEMD160(SHA512(x))`` """
addressbin = ripemd160(hashlib.sha512(unhexlify(pubkey_plain)).hexdigest())
result = Base58(hexlify(addressbin).decode("ascii"))
return cls(result, prefix=pubkey.prefix)
class PublicKey(Prefix):
""" This class deals with Public Keys and inherits ``Address``.
:param str pk: Base58 encoded public key
:param str prefix: Network prefix (defaults to ``GPH``)
Example:::
PublicKey("GPH6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL")
.. note:: By default, graphene-based networks deal with **compressed**
public keys. If an **uncompressed** key is required, the
method ``unCompressed`` can be used::
PublicKey("xxxxx").unCompressed()
"""
def __init__(self, pk, prefix=None):
self.set_prefix(prefix)
if isinstance(pk, PublicKey):
pk = format(pk, self.prefix)
if str(pk).startswith("04"):
# We only ever deal with compressed keys, so let's make it
# compressed
order = ecdsa.SECP256k1.order
p = ecdsa.VerifyingKey.from_string(
unhexlify(pk[2:]), curve=ecdsa.SECP256k1
).pubkey.point
x_str = ecdsa.util.number_to_string(p.x(), order)
pk = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode("ascii")
self._pk = Base58(pk, prefix=self.prefix)
@property
def pubkey(self):
return self._pk
@property
def compressed_key(self):
return PublicKey(self.compressed())
def _derive_y_from_x(self, x, is_even):
""" Derive y point from x point """
curve = ecdsa.SECP256k1.curve
# The curve equation over F_p is:
# y^2 = x^3 + ax + b
a, b, p = curve.a(), curve.b(), curve.p()
alpha = (pow(x, 3, p) + a * x + b) % p
beta = ecdsa.numbertheory.square_root_mod_prime(alpha, p)
if (beta % 2) == is_even:
beta = p - beta
return beta
[docs] def compressed(self):
""" returns the compressed key """
return repr(self._pk)
[docs] def uncompressed(self):
""" Derive uncompressed key """
public_key = repr(self._pk)
prefix = public_key[0:2]
assert prefix == "02" or prefix == "03"
x = int(public_key[2:], 16)
y = self._derive_y_from_x(x, (prefix == "02"))
key = "04" + "%064x" % x + "%064x" % y
return key
[docs] def point(self):
""" Return the point for the public key """
string = unhexlify(self.unCompressed())
return ecdsa.VerifyingKey.from_string(
string[1:], curve=ecdsa.SECP256k1
).pubkey.point
[docs] def child(self, offset256):
""" Derive new public key from this key and a sha256 "offset" """
a = bytes(self) + offset256
s = hashlib.sha256(a).digest()
return self.add(s)
[docs] def add(self, digest256):
""" Derive new public key from this key and a sha256 "digest" """
from .ecdsa import tweakaddPubkey
return tweakaddPubkey(self, digest256)
[docs] @classmethod
def from_privkey(cls, privkey, prefix=None):
""" Derive uncompressed public key """
privkey = PrivateKey(privkey, prefix=prefix or Prefix.prefix)
secret = unhexlify(repr(privkey))
order = ecdsa.SigningKey.from_string(
secret, curve=ecdsa.SECP256k1
).curve.generator.order()
p = ecdsa.SigningKey.from_string(
secret, curve=ecdsa.SECP256k1
).verifying_key.pubkey.point
x_str = ecdsa.util.number_to_string(p.x(), order)
# y_str = ecdsa.util.number_to_string(p.y(), order)
compressed = hexlify(chr(2 + (p.y() & 1)).encode("ascii") + x_str).decode(
"ascii"
)
# uncompressed = hexlify(
# chr(4).encode('ascii') + x_str + y_str).decode('ascii')
return cls(compressed, prefix=prefix or Prefix.prefix)
def __repr__(self):
""" Gives the hex representation of the Graphene public key. """
return repr(self._pk)
def __str__(self):
""" Returns the readable Graphene public key. This call is equivalent to
``format(PublicKey, "GPH")``
"""
return format(self._pk, self.prefix)
def __format__(self, _format):
""" Formats the instance of:doc:`Base58 <base58>` according to
``_format``
"""
return format(self._pk, _format)
def __bytes__(self):
""" Returns the raw public key (has length 33)"""
return bytes(self._pk)
def __lt__(self, other):
""" For sorting of public keys (due to graphene),
we actually sort according to addresses
"""
assert isinstance(other, PublicKey)
return repr(self.address) < repr(other.address)
[docs] def unCompressed(self):
""" Alias for self.uncompressed() - LEGACY"""
return self.uncompressed()
@property
def address(self):
""" Obtain a GrapheneAddress from a public key
"""
return GrapheneAddress.from_pubkey(repr(self), prefix=self.prefix)
class PrivateKey(Prefix):
""" Derives the compressed and uncompressed public keys and
constructs two instances of ``PublicKey``:
:param str wif: Base58check-encoded wif key
:param str prefix: Network prefix (defaults to ``GPH``)
Example:::
PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd")
Compressed vs. Uncompressed:
* ``PrivateKey("w-i-f").pubkey``:
Instance of ``PublicKey`` using compressed key.
* ``PrivateKey("w-i-f").pubkey.address``:
Instance of ``Address`` using compressed key.
* ``PrivateKey("w-i-f").uncompressed``:
Instance of ``PublicKey`` using uncompressed key.
* ``PrivateKey("w-i-f").uncompressed.address``:
Instance of ``Address`` using uncompressed key.
"""
def __init__(self, wif=None, prefix=None):
self.set_prefix(prefix)
if wif is None:
import os
self._wif = Base58(hexlify(os.urandom(32)).decode("ascii"))
elif isinstance(wif, PrivateKey):
self._wif = wif._wif
elif isinstance(wif, Base58):
self._wif = wif
else:
self._wif = Base58(wif)
# test for valid key by trying to obtain a public key
assert len(repr(self._wif)) == 64
@property
def bitcoin(self):
return BitcoinPublicKey.from_privkey(self)
@property
def address(self):
return Address.from_pubkey(self.pubkey, prefix=self.prefix)
@property
def pubkey(self):
return self.compressed
@property
def compressed(self):
return PublicKey.from_privkey(self, prefix=self.prefix)
@property
def uncompressed(self):
return PublicKey(self.pubkey.uncompressed(), prefix=self.prefix)
[docs] def get_secret(self):
""" Get sha256 digest of the wif key.
"""
return hashlib.sha256(bytes(self)).digest()
[docs] def derive_private_key(self, sequence):
""" Derive new private key from this private key and an arbitrary
sequence number
"""
encoded = "%s %d" % (str(self), sequence)
a = bytes(encoded, "ascii")
s = hashlib.sha256(hashlib.sha512(a).digest()).digest()
return PrivateKey(hexlify(s).decode("ascii"), prefix=self.pubkey.prefix)
[docs] def child(self, offset256):
""" Derive new private key from this key and a sha256 "offset"
"""
a = bytes(self.pubkey) + offset256
s = hashlib.sha256(a).digest()
return self.derive_from_seed(s)
[docs] def derive_from_seed(self, offset):
""" Derive private key using "generate_from_seed" method.
Here, the key itself serves as a `seed`, and `offset`
is expected to be a sha256 digest.
"""
seed = int(hexlify(bytes(self)).decode("ascii"), 16)
z = int(hexlify(offset).decode("ascii"), 16)
order = ecdsa.SECP256k1.order
secexp = (seed + z) % order
secret = "%0x" % secexp
if len(secret) < 64: # left-pad with zeroes
secret = ("0" * (64 - len(secret))) + secret
return PrivateKey(secret, prefix=self.pubkey.prefix)
def __format__(self, _format):
""" Formats the instance of:doc:`Base58 <base58>` according to
``_format``
"""
return format(self._wif, _format)
def __repr__(self):
""" Gives the hex representation of the Graphene private key."""
return repr(self._wif)
def __str__(self):
""" Returns the readable (uncompressed wif format) Graphene private key. This
call is equivalent to ``format(PrivateKey, "WIF")``
"""
return format(self._wif, "WIF")
def __bytes__(self): # pragma: no cover
""" Returns the raw private key """
return bytes(self._wif)
class BitcoinAddress(Address):
@classmethod
def from_pubkey(cls, pubkey, compressed=False, version=56, prefix=None):
# Ensure this is a public key
pubkey = PublicKey(pubkey)
if compressed:
pubkey = pubkey.compressed()
else:
pubkey = pubkey.uncompressed()
""" Derive address using ``RIPEMD160(SHA256(x))`` """
addressbin = ripemd160(hexlify(hashlib.sha256(unhexlify(pubkey)).digest()))
return cls(hexlify(addressbin).decode("ascii"))
def __str__(self):
""" Returns the readable Graphene address. This call is equivalent to
``format(Address, "GPH")``
"""
return format(self._address, "BTC")
class BitcoinPublicKey(PublicKey):
@property
def address(self):
return BitcoinAddress.from_pubkey(repr(self))