#!/usr/bin/env python3 ################################################################################ # XRPL Key Derivation Code # Author: rome@ripple.com # Copyright Ripple 2019 # This sample code is provided as a reference for educational purposes. It is # not optimized for speed or for security. Use this code at your own risk and # exercise due caution before using it with real money or infrastructure. # This file is provided under the MIT license along with the rest of the # XRP Ledger Dev Portal docs and sample code: # https://github.com/ripple/xrpl-dev-portal/blob/master/LICENSE # Some of its dependencies are released under other licenses or are adapted # from public domain code. See their respective files for details. ################################################################################ import argparse import sys from hashlib import sha512 if sys.version_info[0] < 3: sys.exit("Python 3+ required") elif sys.version_info.minor < 6: from random import SystemRandom randbits = SystemRandom().getrandbits else: from secrets import randbits from fastecdsa import keys, curve import ed25519 import RFC1751 from base58 import base58 XRPL_SEED_PREFIX = b'\x21' XRPL_ACCT_PUBKEY_PREFIX = b'\x23' XRPL_VALIDATOR_PUBKEY_PREFIX = b'\x1c' ED_PREFIX = b'\xed' def sha512half(buf): """ Return the first 256 bits (32 bytes) of a SHA-512 hash. """ return sha512(buf).digest()[:32] class Seed: """ A 16-byte value used for key derivation. """ def __init__(self, in_string=None, correct_rfc1751=False): """ Decode a buffer input in one of the formats the XRPL supports and convert it to a buffer representing the 16-byte seed to use for key derivation. Formats include: - XRPL base58 encoding - RFC-1751 - hexadecimal - passphrase """ self.correct_rfc1751 = correct_rfc1751 # Keys are lazy-derived later self._secp256k1_sec = None self._secp256k1_pub = None self._secp256k1_root_pub = None self._ed25519_sec = None self._ed25519_pub = None if in_string is None: # Generate a new seed randomly from OS-level RNG. self.bytes = randbits(16*8).to_bytes(16, byteorder="big") return # Is it base58? try: decoded = base58.b58decode_check(in_string) if decoded[:1] == XRPL_SEED_PREFIX and len(decoded) == 17: self.bytes = decoded[1:] return else: raise ValueError except: pass # Maybe it's RFC1751? try: decoded = RFC1751.english_to_key(in_string) if len(decoded) == 16: if correct_rfc1751: self.bytes = decoded else: self.bytes = swap_byte_order(decoded) return else: raise ValueError except: pass # OK, how about hexadecimal? try: decoded = bytes.fromhex(in_string) if len(decoded) == 16: self.bytes = decoded return else: raise ValueError except ValueError as e: pass # Fallback: Guess it's a passphrase. encoded = in_string.encode("UTF-8") self.bytes = sha512(encoded).digest()[:16] return def encode_base58(self): """ Returns a string representation of this seed as an XRPL base58 encoded string such as 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb'. """ return base58.b58encode_check(XRPL_SEED_PREFIX + self.bytes).decode() def encode_hex(self): """ Returns a string representation of this seed as hexadecimal. """ return self.bytes.hex().upper() def encode_rfc1751(self, correct_rfc1751=None): """ Returns a string representation of this seed as an RFC-1751 encoded passphrase. """ # Use the default byte order swap this Seed was generated with # unless the method call overrides it. if correct_rfc1751 is None: correct_rfc1751=self.correct_rfc1751 if correct_rfc1751: buf = self.bytes else: buf = swap_byte_order(self.bytes) return RFC1751.key_to_english(buf) @property def ed25519_secret_key(self): """ Returns a 32-byte Ed25519 secret key (bytes). Saves the calculation for later calls. """ if self._ed25519_sec is None: self._ed25519_sec = sha512half(self.bytes) return self._ed25519_sec @property def ed25519_public_key(self): """ 33-byte Ed25519 public key (bytes)—really a 32-byte key prefixed with the byte 0xED to indicate that it's an Ed25519 key. """ if self._ed25519_pub is None: self._ed25519_pub = (ED_PREFIX + ed25519.publickey(self.ed25519_secret_key)) return self._ed25519_pub @property def secp256k1_secret_key(self): """ 32-byte secp256k1 secret key (bytes) """ if self._secp256k1_sec is None: self.derive_secp256k1_master_keys() return self._secp256k1_sec @property def secp256k1_public_key(self): """ 33-byte secp256k1 account public key (bytes) """ if self._secp256k1_pub is None: self.derive_secp256k1_master_keys() return self._secp256k1_pub @property def secp256k1_root_public_key(self): """ 33-byte secp256k1 root public key (bytes) This is the public key used for validators. """ if self._secp256k1_root_pub is None: self.derive_secp256k1_master_keys() return self._secp256k1_root_pub def derive_secp256k1_master_keys(self): """ Uses the XRPL's convoluted key derivation process to get the secp256k1 master keypair for this seed value. Saves the values to the object for later reference. """ root_sec_i = secp256k1_secret_key_from(self.bytes) root_pub_point = keys.get_public_key(root_sec_i, curve.secp256k1) root_pub_b = compress_secp256k1_public(root_pub_point) fam_b = bytes(4) # Account families are unused; just 4 bytes of zeroes inter_pk_i = secp256k1_secret_key_from( b''.join([root_pub_b, fam_b]) ) inter_pub_point = keys.get_public_key(inter_pk_i, curve.secp256k1) # Secret keys are ints, so just add them mod the secp256k1 group order master_sec_i = (root_sec_i + inter_pk_i) % curve.secp256k1.q # Public keys are points, so the fastecdsa lib handles adding them master_pub_point = root_pub_point + inter_pub_point self._secp256k1_sec = master_sec_i.to_bytes(32, byteorder="big", signed=False) self._secp256k1_pub = compress_secp256k1_public(master_pub_point) self._secp256k1_root_pub = root_pub_b # Saving the full key to make it easier to sign things later self._secp256k1_full = master_pub_point def encode_secp256k1_public_base58(self, validator=False): """ Return the base58-encoded version of the secp256k1 public key. """ if validator: # Validators use the "root" public key key = self.secp256k1_root_public_key prefix = XRPL_VALIDATOR_PUBKEY_PREFIX else: # Accounts use the derived "master" public key key = self.secp256k1_public_key prefix = XRPL_ACCT_PUBKEY_PREFIX return base58.b58encode_check(prefix + key).decode() def encode_ed25519_public_base58(self): """ Return the base58-encoded version of the Ed25519 public key. """ # Unlike secp256k1, Ed25519 public keys are the same for # accounts and for validators. prefix = XRPL_ACCT_PUBKEY_PREFIX return base58.b58encode_check(prefix + self.ed25519_public_key).decode() def secp256k1_secret_key_from(seed): """ Calculate a valid secp256k1 secret key by hashing a seed value; if the result isn't a valid key, increment a seq value and try again. Returns a secret key as a 32-byte integer. """ seq = 0 while True: buf = seed + seq.to_bytes(4, byteorder="big", signed=False) h = sha512half(buf) h_i = int.from_bytes(h, byteorder="big", signed=False) if h_i < curve.secp256k1.q and h_i != 0: return h_i # Else, not a valid secp256k1 key; try again with a new sequence value. seq += 1 def compress_secp256k1_public(point): """ Returns a 33-byte compressed key from an secp256k1 public key, which is a point in the form (x,y) where both x and y are 32-byte ints """ if point.y % 2: prefix = b'\x03' else: prefix = b'\x02' return prefix + point.x.to_bytes(32, byteorder="big", signed=False) def swap_byte_order(buf): """ Swap the byte order of a bytes object. The rippled implementation of RFC-1751 uses the reversed byte order as the examples included in the RFC-1751 spec (which doesn't mention byte order). """ size = len(buf) # doesn't actually matter if it's "really" big-endian i = int.from_bytes(buf, byteorder="big", signed=False) revbuf = i.to_bytes(size, byteorder="little", signed=False) return revbuf if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("secret", nargs="?", default=None, help="The seed to "+ "derive a key from, in hex, XRPL base58, or RFC-1751; or the " + "passphrase to derive a seed and key from. If omitted, generate a "+ "random seed.") p.add_argument("--unswap", "-u", default=False, action="store_true", help="If specified, preserve the byte order of RFC-1751 encoding"+ "/decoding. Not compatible with rippled's RFC-1751 implementation.") args = p.parse_args() seed = Seed(args.secret, correct_rfc1751=args.unswap) seed.derive_secp256k1_master_keys() print(""" Seed (base58): {base58} Seed (hex): {hex} Seed (true RFC-1751): {rfc1751_true} Seed (rippled RFC-1751): {rfc1751_rippled} Ed25519 Secret Key (hex): {ed25519_secret} Ed25519 Public Key (hex): {ed25519_public} Ed25519 Public Key (base58 - Account): {ed25519_pub_base58} secp256k1 Secret Key (hex): {secp256k1_secret} secp256k1 Public Key (hex): {secp256k1_public} secp256k1 Public Key (base58 - Account): {secp256k1_pub_base58} secp256k1 Public Key (base58 - Validator): {secp256k1_pub_base58_val} """.format( base58=seed.encode_base58(), hex=seed.encode_hex(), rfc1751_true=seed.encode_rfc1751(correct_rfc1751=True), rfc1751_rippled=seed.encode_rfc1751(correct_rfc1751=False), ed25519_secret=seed.ed25519_secret_key.hex().upper(), ed25519_public=seed.ed25519_public_key.hex().upper(), secp256k1_secret=seed.secp256k1_secret_key.hex().upper(), secp256k1_public=seed.secp256k1_public_key.hex().upper(), secp256k1_pub_base58=seed.encode_secp256k1_public_base58(), secp256k1_pub_base58_val=seed.encode_secp256k1_public_base58( validator=True), ed25519_pub_base58=seed.encode_ed25519_public_base58(), ))