Skip to content

Commit

Permalink
Fix key math methods and unittests
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryp Toon committed Mar 2, 2024
2 parents 48a5b19 + d69a2b4 commit 1855ada
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 82 deletions.
8 changes: 4 additions & 4 deletions bitcoinlib/config/secp256k1.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@

# Parameters secp256k1
# from http://www.secg.org/sec2-v2.pdf, par 2.4.1
secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F # field size
secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 # order
secp256k1_b = 7
secp256k1_a = 0
secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 # generator point x
secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 # generator point y

# secp256k1_curve = ecdsa.ellipticcurve.CurveFp(secp256k1_p, secp256k1_a, secp256k1_b)
# secp256k1_generator = ecdsa.ellipticcurve.Point(secp256k1_curve, secp256k1_Gx, secp256k1_Gy, secp256k1_n)
17 changes: 17 additions & 0 deletions bitcoinlib/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,23 @@ def double_sha256(string, as_hex=False):
return hashlib.sha256(hashlib.sha256(string).digest()).hexdigest()


def sha256(string, as_hex=False):
"""
Get SHA256 hash of string
:param string: String to be hashed
:type string: bytes
:param as_hex: Return value as hexadecimal string. Default is False
:type as_hex: bool
:return bytes, str:
"""
if not as_hex:
return hashlib.sha256(string).digest()
else:
return hashlib.sha256(string).hexdigest()


def ripemd160(string):
try:
return RIPEMD160.new(string).digest()
Expand Down
235 changes: 163 additions & 72 deletions bitcoinlib/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ def get_key_format(key, is_private=None):
elif isinstance(key, bytes) and len(key) == 32:
key_format = 'bin'
is_private = True
elif isinstance(key, tuple):
key_format = 'point'
is_private = False
elif len(key) == 130 and key[:2] == '04' and not is_private:
key_format = 'public_uncompressed'
is_private = False
Expand Down Expand Up @@ -557,49 +560,49 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, public_key_type="com
if not 0 < int.from_bytes(factor_b, 'big') < secp256k1_n:
raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)")

public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type)
address: str = public_key_to_addresses(public_key=public_key, network=network)
address_hash: bytes = get_checksum(get_bytes(address, unhexlify=False))
salt: bytes = address_hash + owner_entropy
scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64)
derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:]

aes: AESModeOfOperationECB = AESModeOfOperationECB(key)
encrypted_half_1: bytes = aes.encrypt(integer_to_bytes(
bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1)
))
encrypted_half_2: bytes = aes.encrypt(integer_to_bytes(
bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2)
))
encrypted_wif: str = ensure_string(check_encode((
integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2
)))

point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed"))
point_b_prefix: bytes = integer_to_bytes(
(bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1])
)
point_b_half_1: bytes = aes.encrypt(integer_to_bytes(
bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1)
))
point_b_half_2: bytes = aes.encrypt(integer_to_bytes(
bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2)
))
encrypted_point_b: bytes = (
point_b_prefix + point_b_half_1 + point_b_half_2
)
confirmation_code: str = ensure_string(check_encode((
integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b
)))

return dict(
encrypted_wif=encrypted_wif,
confirmation_code=confirmation_code,
public_key=bytes_to_string(public_key),
seed=bytes_to_string(seed_b),
public_key_type=public_key_type,
address=address
)
# public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type)
# address: str = public_key_to_addresses(public_key=public_key, network=network)
# address_hash: bytes = get_checksum(get_bytes(address, unhexlify=False))
# salt: bytes = address_hash + owner_entropy
# scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64)
# derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:]
#
# aes: AESModeOfOperationECB = AESModeOfOperationECB(key)
# encrypted_half_1: bytes = aes.encrypt(integer_to_bytes(
# bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1)
# ))
# encrypted_half_2: bytes = aes.encrypt(integer_to_bytes(
# bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2)
# ))
# encrypted_wif: str = ensure_string(check_encode((
# integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2
# )))
#
# point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed"))
# point_b_prefix: bytes = integer_to_bytes(
# (bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1])
# )
# point_b_half_1: bytes = aes.encrypt(integer_to_bytes(
# bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1)
# ))
# point_b_half_2: bytes = aes.encrypt(integer_to_bytes(
# bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2)
# ))
# encrypted_point_b: bytes = (
# point_b_prefix + point_b_half_1 + point_b_half_2
# )
# confirmation_code: str = ensure_string(check_encode((
# integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b
# )))
#
# return dict(
# encrypted_wif=encrypted_wif,
# confirmation_code=confirmation_code,
# public_key=bytes_to_string(public_key),
# seed=bytes_to_string(seed_b),
# public_key_type=public_key_type,
# address=address
# )



Expand Down Expand Up @@ -856,7 +859,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='',
12127227708610754620337553985245292396444216111803695028419544944213442390363
:param import_key: If specified import given private or public key. If not specified a new private key is generated.
:type import_key: str, int, bytes
:type import_key: str, int, bytes, tuple
:param network: Bitcoin, testnet, litecoin or other network
:type network: str, Network
:param compressed: Is key compressed or not, default is True
Expand Down Expand Up @@ -926,32 +929,46 @@ def __init__(self, import_key=None, network=None, compressed=True, password='',

if not self.is_private:
self.secret = None
pub_key = to_hexstring(import_key)
if len(pub_key) == 130:
self._public_uncompressed_hex = pub_key
self.x_hex = pub_key[2:66]
self.y_hex = pub_key[66:130]
self._y = int(self.y_hex, 16)
self.compressed = False
if self._y % 2:
prefix = '03'
else:
prefix = '02'
self.public_hex = pub_key
self.public_compressed_hex = prefix + self.x_hex
else:
self.public_hex = pub_key
self.x_hex = pub_key[2:66]
if self.key_format == 'point':
self.compressed = True
self._x = int(self.x_hex, 16)
self.public_compressed_hex = pub_key
self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex)
if self._public_uncompressed_hex:
self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex)
if self.compressed:
self.public_byte = self.public_compressed_byte
self._x = import_key[0]
self._y = import_key[1]
self.x_bytes = self._x.to_bytes(32, 'big')
self.y_bytes = self._y.to_bytes(32, 'big')
self.x_hex = self.x_bytes.hex()
self.y_hex = self.y_bytes.hex()
prefix = '03' if self._y % 2 else '02'
self.public_hex = prefix + self.x_hex
self.public_compressed_hex = prefix + self.x_hex
self.public_byte = (b'\3' if self._y % 2 else b'\2') + self.x_bytes
else:
self.public_byte = self.public_uncompressed_byte
pub_key = to_hexstring(import_key)
if len(pub_key) == 130:
self._public_uncompressed_hex = pub_key
self.x_hex = pub_key[2:66]
self.y_hex = pub_key[66:130]
self._y = int(self.y_hex, 16)
self.compressed = False
if self._y % 2:
prefix = '03'
else:
prefix = '02'
self.public_hex = pub_key
self.public_compressed_hex = prefix + self.x_hex
else:
self.public_hex = pub_key
self.x_hex = pub_key[2:66]
self.compressed = True
self._x = int(self.x_hex, 16)
self.public_compressed_hex = pub_key
self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex)
if self._public_uncompressed_hex:
self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex)
if self.compressed:
self.public_byte = self.public_compressed_byte
else:
self.public_byte = self.public_uncompressed_byte

elif self.is_private and self.key_format == 'decimal':
self.secret = int(import_key)
self.private_hex = change_base(self.secret, 10, 16, 64)
Expand Down Expand Up @@ -1049,10 +1066,55 @@ def __bytes__(self):
return self.public_byte

def __add__(self, other):
return self.public_byte + other
"""
Scalar addition over secp256k1 order of 2 keys secrets. Returns a new private key with network and compressed
attributes from first key.
def __radd__(self, other):
return other + self.public_byte
:param other: Private Key class
:type other: Key
:return: Key
"""
assert self.is_private
assert isinstance(other, Key)
assert other.is_private
return Key((self.secret + other.secret) % secp256k1_n, self.network, self.compressed)

def __sub__(self, other):
"""
Scalar substraction over secp256k1 order of 2 keys secrets. Returns a new private key with network and
compressed attributes from first key.
:param other: Private Key class
:type other: Key
:return: Key
"""
assert self.is_private
assert isinstance(other, Key)
assert other.is_private
return Key((self.secret - other.secret) % secp256k1_n, self.network, self.compressed)

def __mul__(self, other):
"""
Scalar multiplication over secp256k1 order of 2 keys secrets. Returns a new private key with network and
compressed attributes from first key.
:param other: Private Key class
:type other: Key
:return: Key
"""
assert isinstance(other, Key)
assert self.secret
assert other.is_private
return Key((self.secret * other.secret) % secp256k1_n, self.network, self.compressed)

def __rmul__(self, other):
return self * other

def __neg__(self):
return self.inverse()

def __len__(self):
return len(self.public_byte)
Expand All @@ -1077,6 +1139,18 @@ def __int__(self):
else:
return None

def inverse(self):
"""
Return inverse of private or public key
:return Key:
"""
if self.is_private:
return Key(secp256k1_n - self.secret, network=self.network, compressed=self.compressed)
else:
# Inverse y in init: self._y = secp256k1_p - self._y
return Key(('02' if self._y % 2 else '03') + self.x_hex, network=self.network, compressed=self.compressed)

@property
def x(self):
if not self._x and self.x_hex:
Expand Down Expand Up @@ -1485,7 +1559,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger
<HDKey(public_hex=0363c152144dcd5253c1216b733fdc6eb8a94ab2cd5caa8ead5e59ab456ff99927, wif_public=xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6SmypHzZG2cYrwpGkWJqRxS6EAW77gd7CHFoXNpBd3LN8xjAyCW, network=bitcoin)>
:param import_key: HD Key to import in WIF format or as byte with key (32 bytes) and chain (32 bytes)
:type import_key: str, bytes, int
:type import_key: str, bytes, int, tuple
:param key: Private or public key (length 32)
:type key: bytes
:param chain: A chain code (length 32)
Expand Down Expand Up @@ -1596,6 +1670,23 @@ def __repr__(self):
return "<HDKey(public_hex=%s, wif_public=%s, network=%s)>" % \
(self.public_hex, self.wif_public(), self.network.name)

def __neg__(self):
return self.inverse()

def inverse(self):
"""
Return inverse of private or public key
:return Key:
"""
if self.is_private:
return HDKey(secp256k1_n - self.secret, network=self.network, compressed=self.compressed,
witness_type=self.witness_type, multisig=self.multisig, encoding=self.encoding)
else:
# Inverse y in init: self._y = secp256k1_p - self._y
return HDKey(('02' if self._y % 2 else '03') + self.x_hex, network=self.network, compressed=self.compressed,
witness_type=self.witness_type, multisig=self.multisig, encoding=self.encoding)

def info(self):
"""
Prints key information to standard output
Expand Down
2 changes: 1 addition & 1 deletion bitcoinlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ def serialize(self):
if isinstance(cmd, int):
raw += bytes([cmd])
else:
raw += data_pack(cmd)
raw += data_pack(bytes(cmd))
self._raw = raw
return raw

Expand Down
Loading

0 comments on commit 1855ada

Please sign in to comment.