From d0a2d3517633634b09e77e2ba95ebd2f428780f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Thu, 3 Jan 2019 01:32:56 +0000 Subject: [PATCH] Add offline subaddress generation --- monero/account.py | 2 +- monero/backends/offline.py | 72 +++++++++++++++++++++++++++++++ monero/ed25519.py | 68 +++++++++++++++++------------ monero/wallet.py | 40 +++++++++++++++++ tests/__init__.py | 4 +- tests/data/mainnet-subaddrs.json | 62 ++++++++++++++++++++++++++ tests/data/stagenet-subaddrs.json | 62 ++++++++++++++++++++++++++ tests/data/testnet-subaddrs.json | 62 ++++++++++++++++++++++++++ tests/test_address.py | 8 ++-- tests/test_ed25519.py | 8 ++++ tests/test_offline.py | 53 +++++++++++++++++++++++ 11 files changed, 409 insertions(+), 32 deletions(-) create mode 100644 monero/backends/offline.py create mode 100644 tests/data/mainnet-subaddrs.json create mode 100644 tests/data/stagenet-subaddrs.json create mode 100644 tests/data/testnet-subaddrs.json create mode 100644 tests/test_ed25519.py create mode 100644 tests/test_offline.py diff --git a/monero/account.py b/monero/account.py index 3997c4a..4f1c9ca 100644 --- a/monero/account.py +++ b/monero/account.py @@ -1,4 +1,3 @@ -import warnings from . import prio from .transaction import PaymentManager @@ -16,6 +15,7 @@ class Account(object): :param index: the account's index within the wallet """ index = None + wallet = None def __init__(self, backend, index): self.index = index diff --git a/monero/backends/offline.py b/monero/backends/offline.py new file mode 100644 index 0000000..df47406 --- /dev/null +++ b/monero/backends/offline.py @@ -0,0 +1,72 @@ +from .. import exceptions +from ..account import Account +from ..address import Address +from ..seed import Seed + + +class WalletIsOffline(exceptions.BackendException): + pass + + +class OfflineWallet(object): + """ + Offline backend for Monero wallet. Provides support for address generation. + """ + _address = None + _svk = None + _ssk = None + + def __init__(self, address, view_key=None, spend_key=None): + self._address = Address(address) + self._svk = view_key or self._svk + self._ssk = spend_key or self._ssk + + def height(self): + raise WalletIsOffline() + + def spend_key(self): + return self._ssk + + def view_key(self): + return self._svk + + def seed(self): + return Seed(self._ssk) + + def accounts(self): + return [Account(self, 0)] + + def new_account(self, label=None): + raise WalletIsOffline() + + def addresses(self, account=0): + if account == 0: + return [self._address] + raise WalletIsOffline() + + def new_address(self, account=0, label=None): + raise WalletIsOffline() + + def balances(self, account=0): + raise WalletIsOffline() + + def transfers_in(self, account, pmtfilter): + raise WalletIsOffline() + + def transfers_out(self, account, pmtfilter): + raise WalletIsOffline() + + def export_outputs(self): + raise WalletIsOffline() + + def import_outputs(self, outputs_hex): + raise WalletIsOffline() + + def export_key_images(self): + raise WalletIsOffline() + + def import_key_images(self, key_images): + raise WalletIsOffline() + + def transfer(self, *args, **kwargs): + raise WalletIsOffline() diff --git a/monero/ed25519.py b/monero/ed25519.py index 354223c..55de635 100644 --- a/monero/ed25519.py +++ b/monero/ed25519.py @@ -49,6 +49,13 @@ def xrecover(y): if x % 2 != 0: x = q-x 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) Bx = xrecover(By) B = [Bx%q, By%q] @@ -62,6 +69,20 @@ def edwards(P, Q): y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) return [x3%q, y3%q] +def add(P, Q): + A = (P[1]-P[0])*(Q[1]-Q[0]) % q + B = (P[1]+P[0])*(Q[1]+Q[0]) % q + C = 2 * P[3] * Q[3] * d % q + D = 2 * P[2] * Q[2] % q + E = B-A + F = D-C + G = D+C + H = B+A + return (E*F, G*H, F*G, E*H) + +def add_compressed(P, Q): + return compress(add(decompress(P), decompress(Q))) + def scalarmult(P, e): if e == 0: return [0, 1] Q = scalarmult(P, e//2) @@ -92,14 +113,6 @@ def Hint(m): h = H(m) return sum(2**i * bit(h, i) for i in range(2*b)) -def signature(m, sk, pk): - h = H(sk) - a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2)) - r = Hint(intlist2bytes([indexbytes(h, j) for j in range(b//8, b//4)]) + m) - R = scalarmult(B, r) - S = (r + Hint(encodepoint(R)+pk+m) * a) % l - return encodepoint(R) + encodeint(S) - def isoncurve(P): x = P[0] y = P[1] @@ -116,28 +129,29 @@ def decodepoint(s): if not isoncurve(P): raise Exception("decoding point that is not on curve") return P -def checkvalid(s, m, pk): - if len(s) != b//4: raise Exception("signature length is wrong") - if len(pk) != b//8: raise Exception("public-key length is wrong") - R = decodepoint(s[0:b//8]) - A = decodepoint(pk) - S = decodeint(s[b//8:b//4]) - h = Hint(encodepoint(R) + pk + m) - if scalarmult(B, S) != edwards(R, scalarmult(A, h)): - raise Exception("signature does not pass verification") - -# This is from https://github.com/monero-project/mininero by Shen Noether with The Monero Project -def scalarmultbase(e): - if e == 0: return [0, 1] - Q = scalarmult(B, e//2) - Q = edwards(Q, Q) - if e & 1: Q = edwards(Q, B) - return Q +# These are unused but let's keep them +#def signature(m, sk, pk): +# h = H(sk) +# a = 2**(b-2) + sum(2**i * bit(h, i) for i in range(3, b-2)) +# r = Hint(intlist2bytes([indexbytes(h, j) for j in range(b//8, b//4)]) + m) +# R = scalarmult(B, r) +# S = (r + Hint(encodepoint(R)+pk+m) * a) % l +# return encodepoint(R) + encodeint(S) +# +#def checkvalid(s, m, pk): +# if len(s) != b//4: raise Exception("signature length is wrong") +# if len(pk) != b//8: raise Exception("public-key length is wrong") +# R = decodepoint(s[0:b//8]) +# A = decodepoint(pk) +# S = decodeint(s[b//8:b//4]) +# h = Hint(encodepoint(R) + pk + m) +# if scalarmult(B, S) != edwards(R, scalarmult(A, h)): +# raise Exception("signature does not pass verification") def public_from_secret(k): keyInt = decodeint(k) - aG = scalarmultbase(keyInt) - return encodepoint(aG) + aB = scalarmult(B, keyInt) + return encodepoint(aB) def public_from_secret_hex(hk): return hexlify(public_from_secret(unhexlify(hk))).decode() diff --git a/monero/wallet.py b/monero/wallet.py index 74f8d37..5d01b21 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -1,3 +1,10 @@ +from binascii import hexlify, unhexlify +from sha3 import keccak_256 +import struct + +from . import address +from . import base58 +from . import ed25519 from . import prio from .transaction import Payment, PaymentManager @@ -36,6 +43,7 @@ class Wallet(object): self.accounts = self.accounts or [] idx = 0 for _acc in self._backend.accounts(): + _acc.wallet = self try: if self.accounts[idx]: continue @@ -184,6 +192,38 @@ class Wallet(object): """ return self.accounts[0].new_address(label=label) + def get_address(self, major, minor): + """ + Calculates sub-address for account index (`major`) and address index within + the account (`minor`). + + :rtype: :class:`BaseAddress ` + """ + master_address = self.address() + if major == minor == 0: + return master_address + master_svk = unhexlify(self.view_key()) + master_psk = unhexlify(self.address().spend_key()) + # m = Hs("SubAddr\0" || master_svk || major || minor) + hsdata = b''.join([ + b'SubAddr\0', master_svk, + struct.pack('