From 447e4b6fb5544c77983b9aaa0cf658b97b25c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Tue, 30 Nov 2021 22:48:29 +0100 Subject: [PATCH] Introduce ed25519 from pynacl --- monero/ed25519.py | 191 ++++++--------------------------- monero/seed.py | 15 ++- monero/transaction/__init__.py | 14 +-- monero/wallet.py | 8 +- requirements.txt | 1 + 5 files changed, 49 insertions(+), 180 deletions(-) diff --git a/monero/ed25519.py b/monero/ed25519.py index 1e74b7a..ebf1a8a 100644 --- a/monero/ed25519.py +++ b/monero/ed25519.py @@ -33,16 +33,8 @@ arithmetic, so we cannot handle secrets without risking their disclosure. import binascii import six -import sys - -if sys.version_info >= (3,): # pragma: no cover - intlist2bytes = bytes -else: # pragma: no cover - range = xrange - - def intlist2bytes(l): - return b"".join(chr(c) for c in l) +import nacl.bindings b = 256 @@ -50,36 +42,41 @@ q = 2 ** 255 - 19 l = 2 ** 252 + 27742317777372353535851937790883648493 -def pow2(x, p): - """== pow(x, 2**p, q)""" - while p > 0: - x = x * x % q - p -= 1 - return x +def bit(h, i): + return (six.indexbytes(h, i // 8) >> (i % 8)) & 1 + +def encodeint(y): + bits = [(y >> i) & 1 for i in range(b)] + return b"".join( + [ + six.int2byte(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b // 8) + ] + ) -def inv(z): - # Adapted from curve25519_athlon.c in djb's Curve25519. - z2 = z * z % q # 2 - z9 = pow2(z2, 2) * z % q # 9 - z11 = z9 * z2 % q # 11 - z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0 - z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 - z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... - z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q - z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q - z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q - z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q - z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 - return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 +def decodeint(s): + return sum(2 ** i * bit(s, i) for i in range(0, b)) -d = -121665 * inv(121666) % q + +edwards_add = nacl.bindings.crypto_core_ed25519_add +inv = nacl.bindings.crypto_core_ed25519_scalar_invert +public_from_secret = nacl.bindings.crypto_sign_ed25519_sk_to_pk +scalar_reduce = nacl.bindings.crypto_core_ed25519_scalar_reduce +scalarmult_B = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp + + +def scalarmult(P, e): + return nacl.bindings.crypto_scalarmult_ed25519_noclamp(e, P) + + +d = -121665 * decodeint(inv(encodeint(121666))) % q I = pow(2, (q - 1) // 4, q) def xrecover(y): - xx = (y * y - 1) * inv(d * y * y + 1) + xx = (y * y - 1) * decodeint(inv(encodeint(d * y * y + 1))) x = pow(xx, (q + 3) // 8, q) if (x * x - xx) % q != 0: @@ -91,113 +88,12 @@ def xrecover(y): return x -def compress(P): - zinv = inv(P[2]) - return (P[0] * zinv % q, P[1] * zinv % q) - - -def decompress(P): - return (P[0], P[1], 1, P[0] * P[1] % q) - - -By = 4 * inv(5) +By = 4 * decodeint(inv(encodeint(5))) Bx = xrecover(By) B = (Bx % q, By % q, 1, (Bx * By) % q) ident = (0, 1, 1, 0) -def edwards_add(P, Q): - # This is formula sequence 'addition-add-2008-hwcd-3' from - # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html - (x1, y1, z1, t1) = P - (x2, y2, z2, t2) = Q - - a = (y1 - x1) * (y2 - x2) % q - b = (y1 + x1) * (y2 + x2) % q - c = t1 * 2 * d * t2 % q - dd = z1 * 2 * z2 % q - e = b - a - f = dd - c - g = dd + c - h = b + a - x3 = e * f - y3 = g * h - t3 = e * h - z3 = f * g - - return (x3 % q, y3 % q, z3 % q, t3 % q) - - -def edwards_double(P): - # This is formula sequence 'dbl-2008-hwcd' from - # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html - (x1, y1, z1, t1) = P - - a = x1 * x1 % q - b = y1 * y1 % q - c = 2 * z1 * z1 % q - # dd = -a - e = ((x1 + y1) * (x1 + y1) - a - b) % q - g = -a + b # dd + b - f = g - c - h = -a - b # dd - b - x3 = e * f - y3 = g * h - t3 = e * h - z3 = f * g - - return (x3 % q, y3 % q, z3 % q, t3 % q) - - -def scalarmult(P, e): - if e == 0: - return ident - Q = scalarmult(P, e // 2) - Q = edwards_double(Q) - if e & 1: - Q = edwards_add(Q, P) - return Q - - -# Bpow[i] == scalarmult(B, 2**i) -Bpow = [] - - -def make_Bpow(): - P = B - for i in range(253): - Bpow.append(P) - P = edwards_double(P) - - -make_Bpow() - - -def scalarmult_B(e): - """ - Implements scalarmult(B, e) more efficiently. - """ - # scalarmult(B, l) is the identity - e = e % l - P = ident - for i in range(253): - if e & 1: - P = edwards_add(P, Bpow[i]) - e = e // 2 - assert e == 0, e - return P - - -def encodeint(y): - bits = [(y >> i) & 1 for i in range(b)] - return b"".join( - [ - six.int2byte(sum([bits[i * 8 + j] << j for j in range(8)])) - for i in range(b // 8) - ] - ) - - def encodepoint(P): (x, y, z, t) = P zi = inv(z) @@ -212,39 +108,20 @@ def encodepoint(P): ) -def bit(h, i): - return (six.indexbytes(h, i // 8) >> (i % 8)) & 1 - - -def isoncurve(P): - (x, y, z, t) = P - return ( - z % q != 0 - and x * y % q == z * t % q - and (y * y - x * x - z * z - d * t * t) % q == 0 - ) - - -def decodeint(s): - return sum(2 ** i * bit(s, i) for i in range(0, b)) - - def decodepoint(s): y = sum(2 ** i * bit(s, i) for i in range(0, b - 1)) x = xrecover(y) if x & 1 != bit(s, b - 1): x = q - x P = (x, y, 1, (x * y) % q) - if not isoncurve(P): - raise ValueError("decoding point that is not on curve") return P -def public_from_secret(k): - keyInt = decodeint(k) - aB = scalarmult_B(keyInt) - return encodepoint(aB) +def pad_to_64B(v): + return nacl.bindings.utils.sodium_pad(v, 64) def public_from_secret_hex(hk): - return binascii.hexlify(public_from_secret(binascii.unhexlify(hk))).decode() + return binascii.hexlify( + public_from_secret(pad_to_64B(binascii.unhexlify(hk))) + ).decode() diff --git a/monero/seed.py b/monero/seed.py index c26a4dd..e5080b8 100644 --- a/monero/seed.py +++ b/monero/seed.py @@ -37,7 +37,6 @@ from binascii import hexlify, unhexlify from os import urandom -import warnings from . import base58, const, ed25519, wordlists from .address import address from .keccak import keccak_256 @@ -93,7 +92,7 @@ class Seed(object): "Not valid mnemonic phrase or hex: {arg}".format(arg=phrase_or_hex) ) else: - self.hex = generate_hex() + self.hex = generate_random_hex() self._encode_seed() def is_mymonero(self): @@ -118,10 +117,8 @@ class Seed(object): return True raise ValueError("Invalid checksum") - def sc_reduce(self, input): - integer = ed25519.decodeint(input) - modulo = integer % ed25519.l - return hexlify(ed25519.encodeint(modulo)).decode() + def _sc_reduce(self, input): + return hexlify(ed25519.scalar_reduce(ed25519.pad_to_64B(input))).decode() def hex_seed(self): return self.hex @@ -131,7 +128,7 @@ class Seed(object): def secret_spend_key(self): a = self._hex_seed_keccak() if self.is_mymonero() else unhexlify(self.hex) - return self.sc_reduce(a) + return self._sc_reduce(a) def secret_view_key(self): b = ( @@ -139,7 +136,7 @@ class Seed(object): if self.is_mymonero() else unhexlify(self.secret_spend_key()) ) - return self.sc_reduce(keccak_256(b).digest()) + return self._sc_reduce(keccak_256(b).digest()) def public_spend_key(self): if self._ed_pub_spend_key: @@ -176,7 +173,7 @@ class Seed(object): return address(base58.encode(data + checksum[0:8])) -def generate_hex(n_bytes=32): +def generate_random_hex(n_bytes=32): """Generate a secure and random hexadecimal string. 32 bytes by default, but arguments can override. :rtype: str diff --git a/monero/transaction/__init__.py b/monero/transaction/__init__.py index 19d013f..474a44d 100644 --- a/monero/transaction/__init__.py +++ b/monero/transaction/__init__.py @@ -144,10 +144,8 @@ class Transaction(object): for keyidx, tx_key in enumerate(self.pubkeys): hsdata = b"".join( [ - ed25519.encodepoint( - ed25519.scalarmult( - ed25519.decodepoint(tx_key), ed25519.decodeint(svk) * 8 - ) + ed25519.scalarmult( + tx_key, ed25519.encodeint(ed25519.decodeint(svk) * 8) ), varint.encode(idx), ] @@ -159,11 +157,9 @@ class Transaction(object): Hsint = Hsint_ur % ed25519.l Hs = ed25519.encodeint(Hsint) - k = ed25519.encodepoint( - ed25519.edwards_add( - ed25519.scalarmult_B(Hsint), - ed25519.decodepoint(psk), - ) + k = ed25519.edwards_add( + ed25519.scalarmult_B(Hs), + psk, ) if k != stealth_address: continue diff --git a/monero/wallet.py b/monero/wallet.py index 211235e..bda66b3 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -231,15 +231,13 @@ class Wallet(object): ) m = keccak_256(hsdata).digest() # D = master_psk + m * B - D = ed25519.edwards_add( - ed25519.decodepoint(master_psk), ed25519.scalarmult_B(ed25519.decodeint(m)) - ) + D = ed25519.edwards_add(master_psk, ed25519.scalarmult_B(m)) # C = master_svk * D - C = ed25519.scalarmult(D, ed25519.decodeint(master_svk)) + C = ed25519.scalarmult(D, master_svk) netbyte = bytearray( [const.SUBADDR_NETBYTES[const.NETS.index(master_address.net)]] ) - data = netbyte + ed25519.encodepoint(D) + ed25519.encodepoint(C) + data = netbyte + D + C checksum = keccak_256(data).digest()[:4] return address.SubAddress(base58.encode(hexlify(data + checksum))) diff --git a/requirements.txt b/requirements.txt index e325dce..de73715 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pycryptodomex~=3.10 +pynacl~=1.4.0 requests six>=1.12.0 ipaddress