From dd2294e7a55897ca7383ac33e496e7b8171f47ef Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Mon, 9 Sep 2019 18:37:06 -0700 Subject: [PATCH 01/13] Key derivation clarifications --- content/_img-sources/key-address-rels.uxf | 98 ++++++++++++------ .../accounts/accounts.md | 4 +- .../accounts/cryptographic-keys.md | 52 ++++++++++ .../status-and-debugging-methods/peers.md | 2 +- .../ledger-object-types/accountroot.md | 2 +- img/key-address-rels.png | Bin 23156 -> 28428 bytes 6 files changed, 120 insertions(+), 38 deletions(-) diff --git a/content/_img-sources/key-address-rels.uxf b/content/_img-sources/key-address-rels.uxf index 9dc94dce10..9c2823ed9f 100644 --- a/content/_img-sources/key-address-rels.uxf +++ b/content/_img-sources/key-address-rels.uxf @@ -5,8 +5,8 @@ UMLObject 60 - 60 - 150 + 50 + 110 70 Passphrase @@ -19,33 +19,34 @@ lt=. UMLObject - 350 - 60 - 180 + 320 + 50 + 130 70 - Secret Key + Seed -- - +(16 bytes) Relation - 200 - 80 - 170 - 40 + 160 + 70 + 180 + 50 lt=<. -SHA-512Half - 150.0;20.0;10.0;20.0 +SHA512, keep +first 16 bytes + 160.0;20.0;10.0;20.0 UMLObject - 650 - 60 + 60 + 180 220 70 @@ -58,21 +59,21 @@ SHA-512Half Relation - 520 + 160 80 - 150 - 50 + 600 + 120 lt=<- Public Key Derivation - 130.0;20.0;10.0;20.0 + 10.0;100.0;10.0;70.0;580.0;70.0;580.0;10.0;550.0;10.0 UMLObject 360 - 190 + 270 180 80 @@ -84,25 +85,28 @@ Derivation Relation - 300 - 120 - 440 - 130 + 270 + 180 + 220 + 160 lt=<- - SHA-256 of RIPEMD160 + + + +RIPEMD160 of SHA-256 - 60.0;90.0;10.0;90.0;10.0;40.0;420.0;40.0;420.0;10.0 + 90.0;110.0;40.0;110.0;40.0;40.0;10.0;40.0 UMLObject 690 - 190 + 270 180 80 @@ -116,7 +120,7 @@ Checksum (4 bytes) UMLObject 60 - 190 + 270 190 80 @@ -130,7 +134,7 @@ Checksum (4 bytes) Relation 240 - 230 + 310 140 30 @@ -141,7 +145,7 @@ Checksum (4 bytes) UMLObject 400 - 320 + 400 100 70 @@ -154,7 +158,7 @@ Checksum (4 bytes) Relation 490 - 230 + 310 120 140 @@ -165,7 +169,7 @@ Checksum (4 bytes) Relation 530 - 210 + 290 80 30 @@ -176,7 +180,7 @@ Checksum (4 bytes) UMLState 590 - 210 + 290 100 40 @@ -188,7 +192,7 @@ type=sender Relation 420 - 260 + 340 130 80 @@ -196,4 +200,30 @@ type=sender SHA-256 twice 10.0;60.0;10.0;10.0 + + UMLObject + + 600 + 50 + 110 + 70 + + Private Key +-- +(32 bytes) + + + + Relation + + 440 + 70 + 180 + 50 + + lt=<- +Private Key +Derivation + 160.0;20.0;10.0;20.0 + diff --git a/content/concepts/payment-system-basics/accounts/accounts.md b/content/concepts/payment-system-basics/accounts/accounts.md index 8a0dec754e..e2a31ebfe4 100644 --- a/content/concepts/payment-system-basics/accounts/accounts.md +++ b/content/concepts/payment-system-basics/accounts/accounts.md @@ -93,9 +93,9 @@ XRP Ledger addresses are encoded using [base58](https://en.wikipedia.org/wiki/Ba The following diagram shows the relationship between keys and addresses: -![Passphrase → Secret Key → Public Key + Type Prefix → Account ID + Checksum → Address](img/key-address-rels.png) +![Passphrase → Seed → Private Key → Public Key + Type Prefix → Account ID + Checksum → Address](img/key-address-rels.png) -The formula for calculating an XRP Ledger address is as follows. For the complete example code, see [`encode_address.js`](https://github.com/ripple/ripple-dev-portal/blob/master/content/_code-samples/address_encoding/encode_address.js). +The formula for calculating an XRP Ledger address from a public key is as follows. For the complete example code, see [`encode_address.js`](https://github.com/ripple/ripple-dev-portal/blob/master/content/_code-samples/address_encoding/encode_address.js). For the process of deriving a public key from a passphrase or seed value, see [Key Derivation](cryptographic-keys.html#key-derivation). 1. Import required algorithms: SHA-256, RIPEMD160, and base58. Set the dictionary for base58. diff --git a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md index a511e2c38d..3855b77255 100644 --- a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md +++ b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md @@ -116,6 +116,58 @@ The supported types of key pairs can be used interchangeably throughout the XRP In the future, it is likely that the XRP Ledger will need new cryptographic signing algorithms to keep up with developments in cryptography. For example, if quantum computers using [Shor's algorithm](https://en.wikipedia.org/wiki/Shor's_algorithm) (or something similar) will soon be practical enough to break elliptic curve cryptography, XRP Ledger developers can add a cryptographic signing algorithm that isn't easily broken. As of mid 2019, there's no clear first choice "quantum-resistant" signing algorithm and quantum computers are not yet practical enough to be a threat, so there are no immediate plans to add any specific algorithms. +## Key Derivation + +The process of deriving a key pair depends on the signing algorithm. In all cases, keys are generated from a _seed_ value that is 16 bytes (128 bits) in length. The seed value can be completely random (recommended) or it can be derived from a specific passphrase by taking the [SHA-512 hash][Hash] and keeping the first 16 bytes (similar to [SHA-512Half][], but keeping only 128 bits instead of 256 bits of the output). + +### Ed25519 Key Derivation +[[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/protocol/impl/SecretKey.cpp#L203 "Source") + +All 32-byte numbers are valid Ed25519 private keys, so Ed25519 private key derivation is a single step: + +- Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte private key. + +To calculate an Ed25519 public key, use the standard public key derivation for [Ed25519](https://ed25519.cr.yp.to/software.html) to derive the public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) + +### secp256k1 Key Derivation + +Key derivation for secp256k1 XRP Ledger keys involves more steps than Ed25519 key derivation for a couple reasons: + +- Not all 32-byte numbers are valid secp256k1 private keys. +- The XRP Ledger's reference implementation has an unused, incomplete framework for deriving a family of key pairs from a single seed value. + +The steps to derive a valid secp256k1 private key from a seed value are as follows: + +1. Calculate a "root key pair" from the seed value, as follows: + + 1. Concatenate the following in order, for a total of 20 bytes: + - The seed value (16 bytes) + - A "root sequence" value (4 bytes), as a big-endian unsigned integer. Use 0 as a starting value for the root sequence. + + 2. Calculate the [SHA-512Half][] of the concatenated (seed+root sequence) value. + + 3. If the result is not a valid secp265k1 private key, increment the root sequence by 1 and start over. [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/crypto/impl/GenerateDeterministicKey.cpp#L103 "Source") + + A valid secp256k1 key must not be zero, and it must be numerically less than the _secp256k1 modulus_ (also called the "group order"). The secp256k1 modulus is the constant value `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141`. + + 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the root public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) + +2. Derive an intermediate key pair from the root public key you calculated in step 1, as follows: + + 1. Concatenate the following in order, for a total of 40 bytes: + - The root public key (32 bytes) + - `0x00000000000000000000000000000000` (4 bytes of zeroes). (This value was intended to be used to derive different members of the same family, but in practice only the value 0 is used.) + - A "key sequence" value (4 bytes), as a big-endian unsigned integer. Use 0 as a starting value for the key sequence. + + 2. Calculate the [SHA-512Half][] of the concatenated value. + + 3. If the result is not a valid secp265k1 private key, increment the key sequence by 1 and restart deriving the account's intermediate key pair. + + 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the intermediate public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) + +3. Calculate the sum of the root public key and the intermediate public key. The result is the master public key. ***TODO: which private key do you use?*** + + ## See Also - **Concepts:** diff --git a/content/references/rippled-api/admin-rippled-methods/status-and-debugging-methods/peers.md b/content/references/rippled-api/admin-rippled-methods/status-and-debugging-methods/peers.md index 06cca0c621..575b0ac5b5 100644 --- a/content/references/rippled-api/admin-rippled-methods/status-and-debugging-methods/peers.md +++ b/content/references/rippled-api/admin-rippled-methods/status-and-debugging-methods/peers.md @@ -377,7 +377,7 @@ The response follows the [standard format][], with a successful result containin | `cluster` | Object | Summary of other `rippled` servers in the same cluster, if [configured as a cluster](clustering.html). [New in: rippled 0.30.1][] | | `peers` | Array | Array of peer objects. | -Each field of the `cluster` object is the public key of that `rippled` server's identifying keypair. (This is the same value that that server returns as `pubkey_node` in the [server_info method][].) The contents of that field are an object with the following fields: +Each field of the `cluster` object is the public key of that `rippled` server's identifying key pair. (This is the same value that that server returns as `pubkey_node` in the [server_info method][].) The contents of that field are an object with the following fields: | `Field` | Type | Description | |:--------|:-------|:----------------------------------------------------------| diff --git a/content/references/rippled-api/ledger-data-formats/ledger-object-types/accountroot.md b/content/references/rippled-api/ledger-data-formats/ledger-object-types/accountroot.md index 3d9d765bc5..5c286613e8 100644 --- a/content/references/rippled-api/ledger-data-formats/ledger-object-types/accountroot.md +++ b/content/references/rippled-api/ledger-data-formats/ledger-object-types/accountroot.md @@ -42,7 +42,7 @@ The `AccountRoot` object has the following fields: | `Domain` | String | VariableLength | _(Optional)_ A domain associated with this account. In JSON, this is the hexadecimal for the ASCII representation of the domain. | | `EmailHash` | String | Hash128 | _(Optional)_ The md5 hash of an email address. Clients can use this to look up an avatar through services such as [Gravatar](https://en.gravatar.com/). | | `MessageKey` | String | VariableLength | _(Optional)_ A public key that may be used to send encrypted messages to this account. In JSON, uses hexadecimal. No more than 33 bytes. | -| `RegularKey` | String | AccountID | _(Optional)_ The address of a keypair that can be used to sign transactions for this account instead of the master key. Use a [SetRegularKey transaction][] to change this value. | +| `RegularKey` | String | AccountID | _(Optional)_ The address of a [key pair](cryptographic-keys.html) that can be used to sign transactions for this account instead of the master key. Use a [SetRegularKey transaction][] to change this value. | | `TickSize` | Number | UInt8 | _(Optional)_ How many significant digits to use for exchange rates of Offers involving currencies issued by this address. Valid values are `3` to `15`, inclusive. _(Requires the [TickSize amendment][].)_ | | `TransferRate` | Number | UInt32 | _(Optional)_ A [transfer fee](https://ripple.com/knowledge_center/transfer-fees/) to charge other users for sending currency issued by this account to each other. | | `WalletLocator` | String | Hash256 | _(Optional)_ **DEPRECATED**. Do not use. | diff --git a/img/key-address-rels.png b/img/key-address-rels.png index 2312d9cacfc101f2a3f9271f61cf64bc0f45bd01..53e4352e27e32ce3589a9f4c779abe5df23e2fc1 100644 GIT binary patch literal 28428 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

lpnjgq5_A3V{#j0ha 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 literal 23156 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 Date: Mon, 7 Oct 2019 22:23:18 -0700 Subject: [PATCH 02/13] Start key derivation sample code --- .../_code-samples/key-derivation/ed25519.py | 108 ++++++++++++++++++ .../key-derivation/key-derivation.py | 48 ++++++++ 2 files changed, 156 insertions(+) create mode 100644 content/_code-samples/key-derivation/ed25519.py create mode 100644 content/_code-samples/key-derivation/key-derivation.py diff --git a/content/_code-samples/key-derivation/ed25519.py b/content/_code-samples/key-derivation/ed25519.py new file mode 100644 index 0000000000..5fa8e0b6b7 --- /dev/null +++ b/content/_code-samples/key-derivation/ed25519.py @@ -0,0 +1,108 @@ +# Python Implementation from https://ed25519.cr.yp.to/software.html +# Public domain software. This is a reference implementation that +# does not include recommended speed & security optimizations. + +import hashlib + +b = 256 +q = 2**255 - 19 +l = 2**252 + 27742317777372353535851937790883648493 + +def H(m): + return hashlib.sha512(m).digest() + +def expmod(b,e,m): + if e == 0: return 1 + t = expmod(b,e/2,m)**2 % m + if e & 1: t = (t*b) % m + return t + +def inv(x): + return expmod(x,q-2,q) + +d = -121665 * inv(121666) +I = expmod(2,(q-1)/4,q) + +def xrecover(y): + xx = (y*y-1) * inv(d*y*y+1) + x = expmod(xx,(q+3)/8,q) + if (x*x - xx) % q != 0: x = (x*I) % q + if x % 2 != 0: x = q-x + return x + +By = 4 * inv(5) +Bx = xrecover(By) +B = [Bx % q,By % q] + +def edwards(P,Q): + x1 = P[0] + y1 = P[1] + x2 = Q[0] + y2 = Q[1] + x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2) + y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) + return [x3 % q,y3 % q] + +def scalarmult(P,e): + if e == 0: return [0,1] + Q = scalarmult(P,e/2) + Q = edwards(Q,Q) + if e & 1: Q = edwards(Q,P) + return Q + +def encodeint(y): + bits = [(y >> i) & 1 for i in range(b)] + return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + +def encodepoint(P): + x = P[0] + y = P[1] + bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] + return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + +def bit(h,i): + return (ord(h[i/8]) >> (i%8)) & 1 + +def publickey(sk): + h = H(sk) + a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) + A = scalarmult(B,a) + return encodepoint(A) + +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(''.join([h[i] for i 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] + return (-x*x + y*y - 1 - d*x*x*y*y) % 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] + 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") diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py new file mode 100644 index 0000000000..dc5ea4bd7f --- /dev/null +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +# XRPL Key Derivation Code +# Author: rome@ripple.com +# Copyright Ripple 2019 + +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 + +import ed25519 + +def sha512half(buf): + """ + Return the first 256 bits (32 bytes) of a SHA-512 hash. + """ + return sha512(buf).digest()[:32] + +def decode_input(in_string): + """ + Decode a buffer input in one of the formats the XRPL supports and convert + it to a buffer representing the seed to use for key derivation. + Formats include: + - XRPL base58 encoding + - RFC-1751 + - passphrase + - hexadecimal + """ + #TODO: split by format + return seed = seed.encode("UTF-8") + +def derive_ed25519_private_key(seed=None): + """ + Takes a seed (buffer) and outputs a 32-byte private key (buffer). + If seed is not provided, generates a seed at random using the OS-level + random number generator, which should be secure enough for this purpose. + """ + if seed is None: + seed = randbits(32*8).to_bytes(32, byteorder="big") + return sha512half(seed) From 181532654290b45be9dd2e41cb3502cac532d65d Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 9 Oct 2019 19:54:43 -0700 Subject: [PATCH 03/13] Key derivation code (working draft) - read hex, base58, RFC1751, or passphrase - derive Ed25519 keys from seed - derive secp256k1 master key from seed - encode keys to hex or base58 Note: at this time, the RFC1751 implementation is consistent with the original spec but not consistent with rippled's RFC1751 implementation. I don't know why rippled's RFC1751 implementation doesn't match the spec. Maybe something to do with endianness. --- .../_code-samples/key-derivation/RFC1751.py | 374 ++++++++++++++++++ .../key-derivation/base58/LICENSE | 19 + .../key-derivation/base58/base58.py | 177 +++++++++ .../_code-samples/key-derivation/ed25519.py | 29 +- .../key-derivation/key-derivation.py | 239 ++++++++++- .../key-derivation/requirements.txt | 1 + 6 files changed, 809 insertions(+), 30 deletions(-) create mode 100644 content/_code-samples/key-derivation/RFC1751.py create mode 100644 content/_code-samples/key-derivation/base58/LICENSE create mode 100644 content/_code-samples/key-derivation/base58/base58.py mode change 100644 => 100755 content/_code-samples/key-derivation/key-derivation.py create mode 100644 content/_code-samples/key-derivation/requirements.txt diff --git a/content/_code-samples/key-derivation/RFC1751.py b/content/_code-samples/key-derivation/RFC1751.py new file mode 100644 index 0000000000..789289560b --- /dev/null +++ b/content/_code-samples/key-derivation/RFC1751.py @@ -0,0 +1,374 @@ +# Upstream version: +# https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Util/RFC1751.py +# This version has been adapted to Python 3 using the '2to3' utility +# and manually adjusted for better Python 3 compatibility. + +# rfc1751.py : Converts between 128-bit strings and a human-readable +# sequence of words, as defined in RFC1751: "A Convention for +# Human-Readable 128-bit Keys", by Daniel L. McDonald. +# +# Part of the Python Cryptography Toolkit +# +# Written by Andrew M. Kuchling and others +# +# =================================================================== +# The contents of this file are dedicated to the public domain. To +# the extent that dedication to the public domain is not available, +# everyone is granted a worldwide, perpetual, royalty-free, +# non-exclusive license to exercise all rights associated with the +# contents of this file for any purpose whatsoever. +# No rights are reserved. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# =================================================================== + +__revision__ = "$Id$" + + +import binascii +from functools import reduce +from io import BytesIO + +def bchr(s): + return bytes([s]) +def bord(s): + return s + + +binary={0:'0000', 1:'0001', 2:'0010', 3:'0011', 4:'0100', 5:'0101', + 6:'0110', 7:'0111', 8:'1000', 9:'1001', 10:'1010', 11:'1011', + 12:'1100', 13:'1101', 14:'1110', 15:'1111'} + +def _key2bin(s): + "Convert a key into a string of binary digits" + kl=[bord(x) for x in s] + kl=[binary[x>>4]+binary[x&15] for x in kl] + return ''.join(kl) + +def _extract(key, start, length): + """Extract a bitstring(2.x)/bytestring(2.x) from a string of binary digits, and return its + numeric value.""" + k=key[start:start+length] + return reduce(lambda x,y: x*2+ord(y)-48, k, 0) + +def key_to_english (key): + """key_to_english(key:string(2.x)/bytes(3.x)) : string + Transform an arbitrary key into a string containing English words. + The key length must be a multiple of 8. + """ + english='' + for index in range(0, len(key), 8): # Loop over 8-byte subkeys + subkey=key[index:index+8] + # Compute the parity of the key + skbin=_key2bin(subkey) ; p=0 + for i in range(0, 64, 2): p=p+_extract(skbin, i, 2) + # Append parity bits to the subkey + skbin=_key2bin(subkey+bchr((p<<6) & 255)) + for i in range(0, 64, 11): + english=english+wordlist[_extract(skbin, i, 11)]+' ' + + return english[:-1] # Remove the trailing space + +def english_to_key (s): + """english_to_key(string):string(2.x)/bytes(2.x) + Transform a string into a corresponding key. + The string must contain words separated by whitespace; the number + of words must be a multiple of 6. + """ + + L=s.upper().split() ; key=b'' + for index in range(0, len(L), 6): + sublist=L[index:index+6] ; char=9*[0] ; bits=0 + for i in sublist: + index = wordlist.index(i) + shift = (8-(bits+11)%8) %8 + y = index << shift + cl, cc, cr = (y>>16), (y>>8)&0xff, y & 0xff + if (shift>5): + char[bits>>3] = char[bits>>3] | cl + char[(bits>>3)+1] = char[(bits>>3)+1] | cc + char[(bits>>3)+2] = char[(bits>>3)+2] | cr + elif shift>-3: + char[bits>>3] = char[bits>>3] | cc + char[(bits>>3)+1] = char[(bits>>3)+1] | cr + else: char[bits>>3] = char[bits>>3] | cr + bits=bits+11 + subkey=reduce(lambda x,y:x+bchr(y), char, b'') + + # Check the parity of the resulting key + skbin=_key2bin(subkey) + p=0 + for i in range(0, 64, 2): p=p+_extract(skbin, i, 2) + if (p&3) != _extract(skbin, 64, 2): + raise ValueError("Parity error in resulting key") + key=key+subkey[0:8] + return key + +wordlist=[ "A", "ABE", "ACE", "ACT", "AD", "ADA", "ADD", + "AGO", "AID", "AIM", "AIR", "ALL", "ALP", "AM", "AMY", "AN", "ANA", + "AND", "ANN", "ANT", "ANY", "APE", "APS", "APT", "ARC", "ARE", "ARK", + "ARM", "ART", "AS", "ASH", "ASK", "AT", "ATE", "AUG", "AUK", "AVE", + "AWE", "AWK", "AWL", "AWN", "AX", "AYE", "BAD", "BAG", "BAH", "BAM", + "BAN", "BAR", "BAT", "BAY", "BE", "BED", "BEE", "BEG", "BEN", "BET", + "BEY", "BIB", "BID", "BIG", "BIN", "BIT", "BOB", "BOG", "BON", "BOO", + "BOP", "BOW", "BOY", "BUB", "BUD", "BUG", "BUM", "BUN", "BUS", "BUT", + "BUY", "BY", "BYE", "CAB", "CAL", "CAM", "CAN", "CAP", "CAR", "CAT", + "CAW", "COD", "COG", "COL", "CON", "COO", "COP", "COT", "COW", "COY", + "CRY", "CUB", "CUE", "CUP", "CUR", "CUT", "DAB", "DAD", "DAM", "DAN", + "DAR", "DAY", "DEE", "DEL", "DEN", "DES", "DEW", "DID", "DIE", "DIG", + "DIN", "DIP", "DO", "DOE", "DOG", "DON", "DOT", "DOW", "DRY", "DUB", + "DUD", "DUE", "DUG", "DUN", "EAR", "EAT", "ED", "EEL", "EGG", "EGO", + "ELI", "ELK", "ELM", "ELY", "EM", "END", "EST", "ETC", "EVA", "EVE", + "EWE", "EYE", "FAD", "FAN", "FAR", "FAT", "FAY", "FED", "FEE", "FEW", + "FIB", "FIG", "FIN", "FIR", "FIT", "FLO", "FLY", "FOE", "FOG", "FOR", + "FRY", "FUM", "FUN", "FUR", "GAB", "GAD", "GAG", "GAL", "GAM", "GAP", + "GAS", "GAY", "GEE", "GEL", "GEM", "GET", "GIG", "GIL", "GIN", "GO", + "GOT", "GUM", "GUN", "GUS", "GUT", "GUY", "GYM", "GYP", "HA", "HAD", + "HAL", "HAM", "HAN", "HAP", "HAS", "HAT", "HAW", "HAY", "HE", "HEM", + "HEN", "HER", "HEW", "HEY", "HI", "HID", "HIM", "HIP", "HIS", "HIT", + "HO", "HOB", "HOC", "HOE", "HOG", "HOP", "HOT", "HOW", "HUB", "HUE", + "HUG", "HUH", "HUM", "HUT", "I", "ICY", "IDA", "IF", "IKE", "ILL", + "INK", "INN", "IO", "ION", "IQ", "IRA", "IRE", "IRK", "IS", "IT", + "ITS", "IVY", "JAB", "JAG", "JAM", "JAN", "JAR", "JAW", "JAY", "JET", + "JIG", "JIM", "JO", "JOB", "JOE", "JOG", "JOT", "JOY", "JUG", "JUT", + "KAY", "KEG", "KEN", "KEY", "KID", "KIM", "KIN", "KIT", "LA", "LAB", + "LAC", "LAD", "LAG", "LAM", "LAP", "LAW", "LAY", "LEA", "LED", "LEE", + "LEG", "LEN", "LEO", "LET", "LEW", "LID", "LIE", "LIN", "LIP", "LIT", + "LO", "LOB", "LOG", "LOP", "LOS", "LOT", "LOU", "LOW", "LOY", "LUG", + "LYE", "MA", "MAC", "MAD", "MAE", "MAN", "MAO", "MAP", "MAT", "MAW", + "MAY", "ME", "MEG", "MEL", "MEN", "MET", "MEW", "MID", "MIN", "MIT", + "MOB", "MOD", "MOE", "MOO", "MOP", "MOS", "MOT", "MOW", "MUD", "MUG", + "MUM", "MY", "NAB", "NAG", "NAN", "NAP", "NAT", "NAY", "NE", "NED", + "NEE", "NET", "NEW", "NIB", "NIL", "NIP", "NIT", "NO", "NOB", "NOD", + "NON", "NOR", "NOT", "NOV", "NOW", "NU", "NUN", "NUT", "O", "OAF", + "OAK", "OAR", "OAT", "ODD", "ODE", "OF", "OFF", "OFT", "OH", "OIL", + "OK", "OLD", "ON", "ONE", "OR", "ORB", "ORE", "ORR", "OS", "OTT", + "OUR", "OUT", "OVA", "OW", "OWE", "OWL", "OWN", "OX", "PA", "PAD", + "PAL", "PAM", "PAN", "PAP", "PAR", "PAT", "PAW", "PAY", "PEA", "PEG", + "PEN", "PEP", "PER", "PET", "PEW", "PHI", "PI", "PIE", "PIN", "PIT", + "PLY", "PO", "POD", "POE", "POP", "POT", "POW", "PRO", "PRY", "PUB", + "PUG", "PUN", "PUP", "PUT", "QUO", "RAG", "RAM", "RAN", "RAP", "RAT", + "RAW", "RAY", "REB", "RED", "REP", "RET", "RIB", "RID", "RIG", "RIM", + "RIO", "RIP", "ROB", "ROD", "ROE", "RON", "ROT", "ROW", "ROY", "RUB", + "RUE", "RUG", "RUM", "RUN", "RYE", "SAC", "SAD", "SAG", "SAL", "SAM", + "SAN", "SAP", "SAT", "SAW", "SAY", "SEA", "SEC", "SEE", "SEN", "SET", + "SEW", "SHE", "SHY", "SIN", "SIP", "SIR", "SIS", "SIT", "SKI", "SKY", + "SLY", "SO", "SOB", "SOD", "SON", "SOP", "SOW", "SOY", "SPA", "SPY", + "SUB", "SUD", "SUE", "SUM", "SUN", "SUP", "TAB", "TAD", "TAG", "TAN", + "TAP", "TAR", "TEA", "TED", "TEE", "TEN", "THE", "THY", "TIC", "TIE", + "TIM", "TIN", "TIP", "TO", "TOE", "TOG", "TOM", "TON", "TOO", "TOP", + "TOW", "TOY", "TRY", "TUB", "TUG", "TUM", "TUN", "TWO", "UN", "UP", + "US", "USE", "VAN", "VAT", "VET", "VIE", "WAD", "WAG", "WAR", "WAS", + "WAY", "WE", "WEB", "WED", "WEE", "WET", "WHO", "WHY", "WIN", "WIT", + "WOK", "WON", "WOO", "WOW", "WRY", "WU", "YAM", "YAP", "YAW", "YE", + "YEA", "YES", "YET", "YOU", "ABED", "ABEL", "ABET", "ABLE", "ABUT", + "ACHE", "ACID", "ACME", "ACRE", "ACTA", "ACTS", "ADAM", "ADDS", + "ADEN", "AFAR", "AFRO", "AGEE", "AHEM", "AHOY", "AIDA", "AIDE", + "AIDS", "AIRY", "AJAR", "AKIN", "ALAN", "ALEC", "ALGA", "ALIA", + "ALLY", "ALMA", "ALOE", "ALSO", "ALTO", "ALUM", "ALVA", "AMEN", + "AMES", "AMID", "AMMO", "AMOK", "AMOS", "AMRA", "ANDY", "ANEW", + "ANNA", "ANNE", "ANTE", "ANTI", "AQUA", "ARAB", "ARCH", "AREA", + "ARGO", "ARID", "ARMY", "ARTS", "ARTY", "ASIA", "ASKS", "ATOM", + "AUNT", "AURA", "AUTO", "AVER", "AVID", "AVIS", "AVON", "AVOW", + "AWAY", "AWRY", "BABE", "BABY", "BACH", "BACK", "BADE", "BAIL", + "BAIT", "BAKE", "BALD", "BALE", "BALI", "BALK", "BALL", "BALM", + "BAND", "BANE", "BANG", "BANK", "BARB", "BARD", "BARE", "BARK", + "BARN", "BARR", "BASE", "BASH", "BASK", "BASS", "BATE", "BATH", + "BAWD", "BAWL", "BEAD", "BEAK", "BEAM", "BEAN", "BEAR", "BEAT", + "BEAU", "BECK", "BEEF", "BEEN", "BEER", + "BEET", "BELA", "BELL", "BELT", "BEND", "BENT", "BERG", "BERN", + "BERT", "BESS", "BEST", "BETA", "BETH", "BHOY", "BIAS", "BIDE", + "BIEN", "BILE", "BILK", "BILL", "BIND", "BING", "BIRD", "BITE", + "BITS", "BLAB", "BLAT", "BLED", "BLEW", "BLOB", "BLOC", "BLOT", + "BLOW", "BLUE", "BLUM", "BLUR", "BOAR", "BOAT", "BOCA", "BOCK", + "BODE", "BODY", "BOGY", "BOHR", "BOIL", "BOLD", "BOLO", "BOLT", + "BOMB", "BONA", "BOND", "BONE", "BONG", "BONN", "BONY", "BOOK", + "BOOM", "BOON", "BOOT", "BORE", "BORG", "BORN", "BOSE", "BOSS", + "BOTH", "BOUT", "BOWL", "BOYD", "BRAD", "BRAE", "BRAG", "BRAN", + "BRAY", "BRED", "BREW", "BRIG", "BRIM", "BROW", "BUCK", "BUDD", + "BUFF", "BULB", "BULK", "BULL", "BUNK", "BUNT", "BUOY", "BURG", + "BURL", "BURN", "BURR", "BURT", "BURY", "BUSH", "BUSS", "BUST", + "BUSY", "BYTE", "CADY", "CAFE", "CAGE", "CAIN", "CAKE", "CALF", + "CALL", "CALM", "CAME", "CANE", "CANT", "CARD", "CARE", "CARL", + "CARR", "CART", "CASE", "CASH", "CASK", "CAST", "CAVE", "CEIL", + "CELL", "CENT", "CERN", "CHAD", "CHAR", "CHAT", "CHAW", "CHEF", + "CHEN", "CHEW", "CHIC", "CHIN", "CHOU", "CHOW", "CHUB", "CHUG", + "CHUM", "CITE", "CITY", "CLAD", "CLAM", "CLAN", "CLAW", "CLAY", + "CLOD", "CLOG", "CLOT", "CLUB", "CLUE", "COAL", "COAT", "COCA", + "COCK", "COCO", "CODA", "CODE", "CODY", "COED", "COIL", "COIN", + "COKE", "COLA", "COLD", "COLT", "COMA", "COMB", "COME", "COOK", + "COOL", "COON", "COOT", "CORD", "CORE", "CORK", "CORN", "COST", + "COVE", "COWL", "CRAB", "CRAG", "CRAM", "CRAY", "CREW", "CRIB", + "CROW", "CRUD", "CUBA", "CUBE", "CUFF", "CULL", "CULT", "CUNY", + "CURB", "CURD", "CURE", "CURL", "CURT", "CUTS", "DADE", "DALE", + "DAME", "DANA", "DANE", "DANG", "DANK", "DARE", "DARK", "DARN", + "DART", "DASH", "DATA", "DATE", "DAVE", "DAVY", "DAWN", "DAYS", + "DEAD", "DEAF", "DEAL", "DEAN", "DEAR", "DEBT", "DECK", "DEED", + "DEEM", "DEER", "DEFT", "DEFY", "DELL", "DENT", "DENY", "DESK", + "DIAL", "DICE", "DIED", "DIET", "DIME", "DINE", "DING", "DINT", + "DIRE", "DIRT", "DISC", "DISH", "DISK", "DIVE", "DOCK", "DOES", + "DOLE", "DOLL", "DOLT", "DOME", "DONE", "DOOM", "DOOR", "DORA", + "DOSE", "DOTE", "DOUG", "DOUR", "DOVE", "DOWN", "DRAB", "DRAG", + "DRAM", "DRAW", "DREW", "DRUB", "DRUG", "DRUM", "DUAL", "DUCK", + "DUCT", "DUEL", "DUET", "DUKE", "DULL", "DUMB", "DUNE", "DUNK", + "DUSK", "DUST", "DUTY", "EACH", "EARL", "EARN", "EASE", "EAST", + "EASY", "EBEN", "ECHO", "EDDY", "EDEN", "EDGE", "EDGY", "EDIT", + "EDNA", "EGAN", "ELAN", "ELBA", "ELLA", "ELSE", "EMIL", "EMIT", + "EMMA", "ENDS", "ERIC", "EROS", "EVEN", "EVER", "EVIL", "EYED", + "FACE", "FACT", "FADE", "FAIL", "FAIN", "FAIR", "FAKE", "FALL", + "FAME", "FANG", "FARM", "FAST", "FATE", "FAWN", "FEAR", "FEAT", + "FEED", "FEEL", "FEET", "FELL", "FELT", "FEND", "FERN", "FEST", + "FEUD", "FIEF", "FIGS", "FILE", "FILL", "FILM", "FIND", "FINE", + "FINK", "FIRE", "FIRM", "FISH", "FISK", "FIST", "FITS", "FIVE", + "FLAG", "FLAK", "FLAM", "FLAT", "FLAW", "FLEA", "FLED", "FLEW", + "FLIT", "FLOC", "FLOG", "FLOW", "FLUB", "FLUE", "FOAL", "FOAM", + "FOGY", "FOIL", "FOLD", "FOLK", "FOND", "FONT", "FOOD", "FOOL", + "FOOT", "FORD", "FORE", "FORK", "FORM", "FORT", "FOSS", "FOUL", + "FOUR", "FOWL", "FRAU", "FRAY", "FRED", "FREE", "FRET", "FREY", + "FROG", "FROM", "FUEL", "FULL", "FUME", "FUND", "FUNK", "FURY", + "FUSE", "FUSS", "GAFF", "GAGE", "GAIL", "GAIN", "GAIT", "GALA", + "GALE", "GALL", "GALT", "GAME", "GANG", "GARB", "GARY", "GASH", + "GATE", "GAUL", "GAUR", "GAVE", "GAWK", "GEAR", "GELD", "GENE", + "GENT", "GERM", "GETS", "GIBE", "GIFT", "GILD", "GILL", "GILT", + "GINA", "GIRD", "GIRL", "GIST", "GIVE", "GLAD", "GLEE", "GLEN", + "GLIB", "GLOB", "GLOM", "GLOW", "GLUE", "GLUM", "GLUT", "GOAD", + "GOAL", "GOAT", "GOER", "GOES", "GOLD", "GOLF", "GONE", "GONG", + "GOOD", "GOOF", "GORE", "GORY", "GOSH", "GOUT", "GOWN", "GRAB", + "GRAD", "GRAY", "GREG", "GREW", "GREY", "GRID", "GRIM", "GRIN", + "GRIT", "GROW", "GRUB", "GULF", "GULL", "GUNK", "GURU", "GUSH", + "GUST", "GWEN", "GWYN", "HAAG", "HAAS", "HACK", "HAIL", "HAIR", + "HALE", "HALF", "HALL", "HALO", "HALT", "HAND", "HANG", "HANK", + "HANS", "HARD", "HARK", "HARM", "HART", "HASH", "HAST", "HATE", + "HATH", "HAUL", "HAVE", "HAWK", "HAYS", "HEAD", "HEAL", "HEAR", + "HEAT", "HEBE", "HECK", "HEED", "HEEL", "HEFT", "HELD", "HELL", + "HELM", "HERB", "HERD", "HERE", "HERO", "HERS", "HESS", "HEWN", + "HICK", "HIDE", "HIGH", "HIKE", "HILL", "HILT", "HIND", "HINT", + "HIRE", "HISS", "HIVE", "HOBO", "HOCK", "HOFF", "HOLD", "HOLE", + "HOLM", "HOLT", "HOME", "HONE", "HONK", "HOOD", "HOOF", "HOOK", + "HOOT", "HORN", "HOSE", "HOST", "HOUR", "HOVE", "HOWE", "HOWL", + "HOYT", "HUCK", "HUED", "HUFF", "HUGE", "HUGH", "HUGO", "HULK", + "HULL", "HUNK", "HUNT", "HURD", "HURL", "HURT", "HUSH", "HYDE", + "HYMN", "IBIS", "ICON", "IDEA", "IDLE", "IFFY", "INCA", "INCH", + "INTO", "IONS", "IOTA", "IOWA", "IRIS", "IRMA", "IRON", "ISLE", + "ITCH", "ITEM", "IVAN", "JACK", "JADE", "JAIL", "JAKE", "JANE", + "JAVA", "JEAN", "JEFF", "JERK", "JESS", "JEST", "JIBE", "JILL", + "JILT", "JIVE", "JOAN", "JOBS", "JOCK", "JOEL", "JOEY", "JOHN", + "JOIN", "JOKE", "JOLT", "JOVE", "JUDD", "JUDE", "JUDO", "JUDY", + "JUJU", "JUKE", "JULY", "JUNE", "JUNK", "JUNO", "JURY", "JUST", + "JUTE", "KAHN", "KALE", "KANE", "KANT", "KARL", "KATE", "KEEL", + "KEEN", "KENO", "KENT", "KERN", "KERR", "KEYS", "KICK", "KILL", + "KIND", "KING", "KIRK", "KISS", "KITE", "KLAN", "KNEE", "KNEW", + "KNIT", "KNOB", "KNOT", "KNOW", "KOCH", "KONG", "KUDO", "KURD", + "KURT", "KYLE", "LACE", "LACK", "LACY", "LADY", "LAID", "LAIN", + "LAIR", "LAKE", "LAMB", "LAME", "LAND", "LANE", "LANG", "LARD", + "LARK", "LASS", "LAST", "LATE", "LAUD", "LAVA", "LAWN", "LAWS", + "LAYS", "LEAD", "LEAF", "LEAK", "LEAN", "LEAR", "LEEK", "LEER", + "LEFT", "LEND", "LENS", "LENT", "LEON", "LESK", "LESS", "LEST", + "LETS", "LIAR", "LICE", "LICK", "LIED", "LIEN", "LIES", "LIEU", + "LIFE", "LIFT", "LIKE", "LILA", "LILT", "LILY", "LIMA", "LIMB", + "LIME", "LIND", "LINE", "LINK", "LINT", "LION", "LISA", "LIST", + "LIVE", "LOAD", "LOAF", "LOAM", "LOAN", "LOCK", "LOFT", "LOGE", + "LOIS", "LOLA", "LONE", "LONG", "LOOK", "LOON", "LOOT", "LORD", + "LORE", "LOSE", "LOSS", "LOST", "LOUD", "LOVE", "LOWE", "LUCK", + "LUCY", "LUGE", "LUKE", "LULU", "LUND", "LUNG", "LURA", "LURE", + "LURK", "LUSH", "LUST", "LYLE", "LYNN", "LYON", "LYRA", "MACE", + "MADE", "MAGI", "MAID", "MAIL", "MAIN", "MAKE", "MALE", "MALI", + "MALL", "MALT", "MANA", "MANN", "MANY", "MARC", "MARE", "MARK", + "MARS", "MART", "MARY", "MASH", "MASK", "MASS", "MAST", "MATE", + "MATH", "MAUL", "MAYO", "MEAD", "MEAL", "MEAN", "MEAT", "MEEK", + "MEET", "MELD", "MELT", "MEMO", "MEND", "MENU", "MERT", "MESH", + "MESS", "MICE", "MIKE", "MILD", "MILE", "MILK", "MILL", "MILT", + "MIMI", "MIND", "MINE", "MINI", "MINK", "MINT", "MIRE", "MISS", + "MIST", "MITE", "MITT", "MOAN", "MOAT", "MOCK", "MODE", "MOLD", + "MOLE", "MOLL", "MOLT", "MONA", "MONK", "MONT", "MOOD", "MOON", + "MOOR", "MOOT", "MORE", "MORN", "MORT", "MOSS", "MOST", "MOTH", + "MOVE", "MUCH", "MUCK", "MUDD", "MUFF", "MULE", "MULL", "MURK", + "MUSH", "MUST", "MUTE", "MUTT", "MYRA", "MYTH", "NAGY", "NAIL", + "NAIR", "NAME", "NARY", "NASH", "NAVE", "NAVY", "NEAL", "NEAR", + "NEAT", "NECK", "NEED", "NEIL", "NELL", "NEON", "NERO", "NESS", + "NEST", "NEWS", "NEWT", "NIBS", "NICE", "NICK", "NILE", "NINA", + "NINE", "NOAH", "NODE", "NOEL", "NOLL", "NONE", "NOOK", "NOON", + "NORM", "NOSE", "NOTE", "NOUN", "NOVA", "NUDE", "NULL", "NUMB", + "OATH", "OBEY", "OBOE", "ODIN", "OHIO", "OILY", "OINT", "OKAY", + "OLAF", "OLDY", "OLGA", "OLIN", "OMAN", "OMEN", "OMIT", "ONCE", + "ONES", "ONLY", "ONTO", "ONUS", "ORAL", "ORGY", "OSLO", "OTIS", + "OTTO", "OUCH", "OUST", "OUTS", "OVAL", "OVEN", "OVER", "OWLY", + "OWNS", "QUAD", "QUIT", "QUOD", "RACE", "RACK", "RACY", "RAFT", + "RAGE", "RAID", "RAIL", "RAIN", "RAKE", "RANK", "RANT", "RARE", + "RASH", "RATE", "RAVE", "RAYS", "READ", "REAL", "REAM", "REAR", + "RECK", "REED", "REEF", "REEK", "REEL", "REID", "REIN", "RENA", + "REND", "RENT", "REST", "RICE", "RICH", "RICK", "RIDE", "RIFT", + "RILL", "RIME", "RING", "RINK", "RISE", "RISK", "RITE", "ROAD", + "ROAM", "ROAR", "ROBE", "ROCK", "RODE", "ROIL", "ROLL", "ROME", + "ROOD", "ROOF", "ROOK", "ROOM", "ROOT", "ROSA", "ROSE", "ROSS", + "ROSY", "ROTH", "ROUT", "ROVE", "ROWE", "ROWS", "RUBE", "RUBY", + "RUDE", "RUDY", "RUIN", "RULE", "RUNG", "RUNS", "RUNT", "RUSE", + "RUSH", "RUSK", "RUSS", "RUST", "RUTH", "SACK", "SAFE", "SAGE", + "SAID", "SAIL", "SALE", "SALK", "SALT", "SAME", "SAND", "SANE", + "SANG", "SANK", "SARA", "SAUL", "SAVE", "SAYS", "SCAN", "SCAR", + "SCAT", "SCOT", "SEAL", "SEAM", "SEAR", "SEAT", "SEED", "SEEK", + "SEEM", "SEEN", "SEES", "SELF", "SELL", "SEND", "SENT", "SETS", + "SEWN", "SHAG", "SHAM", "SHAW", "SHAY", "SHED", "SHIM", "SHIN", + "SHOD", "SHOE", "SHOT", "SHOW", "SHUN", "SHUT", "SICK", "SIDE", + "SIFT", "SIGH", "SIGN", "SILK", "SILL", "SILO", "SILT", "SINE", + "SING", "SINK", "SIRE", "SITE", "SITS", "SITU", "SKAT", "SKEW", + "SKID", "SKIM", "SKIN", "SKIT", "SLAB", "SLAM", "SLAT", "SLAY", + "SLED", "SLEW", "SLID", "SLIM", "SLIT", "SLOB", "SLOG", "SLOT", + "SLOW", "SLUG", "SLUM", "SLUR", "SMOG", "SMUG", "SNAG", "SNOB", + "SNOW", "SNUB", "SNUG", "SOAK", "SOAR", "SOCK", "SODA", "SOFA", + "SOFT", "SOIL", "SOLD", "SOME", "SONG", "SOON", "SOOT", "SORE", + "SORT", "SOUL", "SOUR", "SOWN", "STAB", "STAG", "STAN", "STAR", + "STAY", "STEM", "STEW", "STIR", "STOW", "STUB", "STUN", "SUCH", + "SUDS", "SUIT", "SULK", "SUMS", "SUNG", "SUNK", "SURE", "SURF", + "SWAB", "SWAG", "SWAM", "SWAN", "SWAT", "SWAY", "SWIM", "SWUM", + "TACK", "TACT", "TAIL", "TAKE", "TALE", "TALK", "TALL", "TANK", + "TASK", "TATE", "TAUT", "TEAL", "TEAM", "TEAR", "TECH", "TEEM", + "TEEN", "TEET", "TELL", "TEND", "TENT", "TERM", "TERN", "TESS", + "TEST", "THAN", "THAT", "THEE", "THEM", "THEN", "THEY", "THIN", + "THIS", "THUD", "THUG", "TICK", "TIDE", "TIDY", "TIED", "TIER", + "TILE", "TILL", "TILT", "TIME", "TINA", "TINE", "TINT", "TINY", + "TIRE", "TOAD", "TOGO", "TOIL", "TOLD", "TOLL", "TONE", "TONG", + "TONY", "TOOK", "TOOL", "TOOT", "TORE", "TORN", "TOTE", "TOUR", + "TOUT", "TOWN", "TRAG", "TRAM", "TRAY", "TREE", "TREK", "TRIG", + "TRIM", "TRIO", "TROD", "TROT", "TROY", "TRUE", "TUBA", "TUBE", + "TUCK", "TUFT", "TUNA", "TUNE", "TUNG", "TURF", "TURN", "TUSK", + "TWIG", "TWIN", "TWIT", "ULAN", "UNIT", "URGE", "USED", "USER", + "USES", "UTAH", "VAIL", "VAIN", "VALE", "VARY", "VASE", "VAST", + "VEAL", "VEDA", "VEIL", "VEIN", "VEND", "VENT", "VERB", "VERY", + "VETO", "VICE", "VIEW", "VINE", "VISE", "VOID", "VOLT", "VOTE", + "WACK", "WADE", "WAGE", "WAIL", "WAIT", "WAKE", "WALE", "WALK", + "WALL", "WALT", "WAND", "WANE", "WANG", "WANT", "WARD", "WARM", + "WARN", "WART", "WASH", "WAST", "WATS", "WATT", "WAVE", "WAVY", + "WAYS", "WEAK", "WEAL", "WEAN", "WEAR", "WEED", "WEEK", "WEIR", + "WELD", "WELL", "WELT", "WENT", "WERE", "WERT", "WEST", "WHAM", + "WHAT", "WHEE", "WHEN", "WHET", "WHOA", "WHOM", "WICK", "WIFE", + "WILD", "WILL", "WIND", "WINE", "WING", "WINK", "WINO", "WIRE", + "WISE", "WISH", "WITH", "WOLF", "WONT", "WOOD", "WOOL", "WORD", + "WORE", "WORK", "WORM", "WORN", "WOVE", "WRIT", "WYNN", "YALE", + "YANG", "YANK", "YARD", "YARN", "YAWL", "YAWN", "YEAH", "YEAR", + "YELL", "YOGA", "YOKE" ] + +if __name__=='__main__': + data = [('EB33F77EE73D4053', 'TIDE ITCH SLOW REIN RULE MOT'), + ('CCAC2AED591056BE4F90FD441C534766', + 'RASH BUSH MILK LOOK BAD BRIM AVID GAFF BAIT ROT POD LOVE'), + ('EFF81F9BFBC65350920CDD7416DE8009', + 'TROD MUTE TAIL WARM CHAR KONG HAAG CITY BORE O TEAL AWL') + ] + + for key, words in data: + print('Trying key', key) + key=binascii.a2b_hex(key) + w2=key_to_english(key) + if w2!=words: + print('key_to_english fails on key', repr(key), ', producing', str(w2)) + k2=english_to_key(words) + if k2!=key: + print('english_to_key fails on key', repr(key), ', producing', repr(k2)) diff --git a/content/_code-samples/key-derivation/base58/LICENSE b/content/_code-samples/key-derivation/base58/LICENSE new file mode 100644 index 0000000000..ca15a778d8 --- /dev/null +++ b/content/_code-samples/key-derivation/base58/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 David Keijser + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/content/_code-samples/key-derivation/base58/base58.py b/content/_code-samples/key-derivation/base58/base58.py new file mode 100644 index 0000000000..9767194b5f --- /dev/null +++ b/content/_code-samples/key-derivation/base58/base58.py @@ -0,0 +1,177 @@ +'''Base58 encoding + +Implementations of Base58 and Base58Check endcodings that are compatible +with the XRP Ledger. +''' + +# This This code is adapted from the module by David Keijser at +# . - rome@ripple.com +# His notes are preserved below: + +# This module is based upon base58 snippets found scattered over many bitcoin +# tools written in python. From what I gather the original source is from a +# forum post by Gavin Andresen, so direct your praise to him. +# This module adds shiny packaging and support for python3. + +from hashlib import sha256 + +__version__ = '1.0.3-xrp' + +# 58 character alphabet used +# alphabet = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' # Bitcoin +alphabet = b'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz' # XRP Ledger + + +if bytes == str: # python2 + iseq, bseq, buffer = ( + lambda s: map(ord, s), + lambda s: ''.join(map(chr, s)), + lambda s: s, + ) +else: # python3 + iseq, bseq, buffer = ( + lambda s: s, + bytes, + lambda s: s.buffer, + ) + + +def scrub_input(v): + if isinstance(v, str) and not isinstance(v, bytes): + v = v.encode('ascii') + + if not isinstance(v, bytes): + raise TypeError( + "a bytes-like object is required (also str), not '%s'" % + type(v).__name__) + + return v + + +def b58encode_int(i, default_one=True): + '''Encode an integer using Base58''' + if not i and default_one: + return alphabet[0:1] + string = b"" + while i: + i, idx = divmod(i, 58) + string = alphabet[idx:idx+1] + string + return string + + +def b58encode(v): + '''Encode a string using Base58''' + + v = scrub_input(v) + + nPad = len(v) + v = v.lstrip(b'\0') + nPad -= len(v) + + p, acc = 1, 0 + for c in iseq(reversed(v)): + acc += p * c + p = p << 8 + + result = b58encode_int(acc, default_one=False) + + return (alphabet[0:1] * nPad + result) + + +def b58decode_int(v): + '''Decode a Base58 encoded string as an integer''' + + v = scrub_input(v) + + decimal = 0 + for char in v: + decimal = decimal * 58 + alphabet.index(char) + return decimal + + +def b58decode(v): + '''Decode a Base58 encoded string''' + + v = scrub_input(v) + + origlen = len(v) + v = v.lstrip(alphabet[0:1]) + newlen = len(v) + + acc = b58decode_int(v) + + result = [] + while acc > 0: + acc, mod = divmod(acc, 256) + result.append(mod) + + return (b'\0' * (origlen - newlen) + bseq(reversed(result))) + + +def b58encode_check(v): + '''Encode a string using Base58 with a 4 character checksum''' + + digest = sha256(sha256(v).digest()).digest() + return b58encode(v + digest[:4]) + + +def b58decode_check(v): + '''Decode and verify the checksum of a Base58 encoded string''' + + result = b58decode(v) + result, check = result[:-4], result[-4:] + digest = sha256(sha256(result).digest()).digest() + + if check != digest[:4]: + raise ValueError("Invalid checksum") + + return result + + +def main(): + '''Base58 encode or decode FILE, or standard input, to standard output.''' + + import sys + import argparse + + stdout = buffer(sys.stdout) + + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument( + 'file', + metavar='FILE', + nargs='?', + type=argparse.FileType('r'), + default='-') + parser.add_argument( + '-d', '--decode', + action='store_true', + help='decode data') + parser.add_argument( + '-c', '--check', + action='store_true', + help='append a checksum before encoding') + + args = parser.parse_args() + fun = { + (False, False): b58encode, + (False, True): b58encode_check, + (True, False): b58decode, + (True, True): b58decode_check + }[(args.decode, args.check)] + + data = buffer(args.file).read() + + try: + result = fun(data) + except Exception as e: + sys.exit(e) + + if not isinstance(result, bytes): + result = result.encode('ascii') + + stdout.write(result) + + +if __name__ == '__main__': + main() diff --git a/content/_code-samples/key-derivation/ed25519.py b/content/_code-samples/key-derivation/ed25519.py index 5fa8e0b6b7..077f25269f 100644 --- a/content/_code-samples/key-derivation/ed25519.py +++ b/content/_code-samples/key-derivation/ed25519.py @@ -1,9 +1,14 @@ # Python Implementation from https://ed25519.cr.yp.to/software.html +# Adjusted to Python 3 syntax by rome@ripple.com # Public domain software. This is a reference implementation that # does not include recommended speed & security optimizations. + import hashlib +def bchr(i): + return bytes([i]) + b = 256 q = 2**255 - 19 l = 2**252 + 27742317777372353535851937790883648493 @@ -13,7 +18,7 @@ def H(m): def expmod(b,e,m): if e == 0: return 1 - t = expmod(b,e/2,m)**2 % m + t = expmod(b,e//2,m)**2 % m if e & 1: t = (t*b) % m return t @@ -21,11 +26,11 @@ def inv(x): return expmod(x,q-2,q) d = -121665 * inv(121666) -I = expmod(2,(q-1)/4,q) +I = expmod(2,(q-1)//4,q) def xrecover(y): xx = (y*y-1) * inv(d*y*y+1) - x = expmod(xx,(q+3)/8,q) + x = expmod(xx,(q+3)//8,q) if (x*x - xx) % q != 0: x = (x*I) % q if x % 2 != 0: x = q-x return x @@ -45,23 +50,23 @@ def edwards(P,Q): def scalarmult(P,e): if e == 0: return [0,1] - Q = scalarmult(P,e/2) + Q = scalarmult(P,e//2) Q = edwards(Q,Q) if e & 1: Q = edwards(Q,P) return Q def encodeint(y): bits = [(y >> i) & 1 for i in range(b)] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + return b''.join([bchr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b//8)]) def encodepoint(P): x = P[0] y = P[1] bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + return b''.join([bchr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b//8)]) def bit(h,i): - return (ord(h[i/8]) >> (i%8)) & 1 + return (h[i//8] >> (i%8)) & 1 def publickey(sk): h = H(sk) @@ -76,7 +81,7 @@ def Hint(m): 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(''.join([h[i] for i in range(b/8,b/4)]) + m) + r = Hint(bytes([h[i] for i 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) @@ -98,11 +103,11 @@ def decodepoint(s): 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]) + 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]) + 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") diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py old mode 100644 new mode 100755 index dc5ea4bd7f..ca0f5f63d5 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -4,8 +4,8 @@ # Author: rome@ripple.com # Copyright Ripple 2019 +import argparse import sys - from hashlib import sha512 if sys.version_info[0] < 3: @@ -16,7 +16,19 @@ elif sys.version_info.minor < 6: else: from secrets import randbits +# import cryptography.hazmat.primitives.asymmetric.ec as ecc +# import cryptography.hazmat.backends.default_backend as default_backend + +from fastecdsa import keys, curve + import ed25519 +import RFC1751 +import base58.base58 as base58 + +XRPL_SEED_PREFIX = b'\x21' +XRPL_PUBKEY_PREFIX = b'\x23' +ED_PREFIX = b'\xed' +SECP_MODULUS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 def sha512half(buf): """ @@ -24,25 +36,216 @@ def sha512half(buf): """ return sha512(buf).digest()[:32] -def decode_input(in_string): +class Seed: """ - Decode a buffer input in one of the formats the XRPL supports and convert - it to a buffer representing the seed to use for key derivation. - Formats include: - - XRPL base58 encoding - - RFC-1751 - - passphrase - - hexadecimal + A 16-byte value used for key derivation. """ - #TODO: split by format - return seed = seed.encode("UTF-8") -def derive_ed25519_private_key(seed=None): + def __init__(self, in_string=None): + """ + 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 + """ + # Keys are lazy-derived later + self._secp256k1_pri = None + self._secp256k1_pub = None + self._ed25519_pri = None + self._ed25519_pub = None + + if in_string is None: + # Generate a new seed randomly from OS-level RNG. + self.bytes = randbits(32*8).to_bytes(32, byteorder="big") + + # 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: + self.bytes = 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): + """ + Returns a string representation of this seed as an RFC-1751 encoded + passphrase. + """ + return RFC1751.key_to_english(self.bytes) + + @property + def ed25519_private_key(self): + """ + Returns a 32-byte Ed25519 private key (bytes). + Saves the calculation for later calls. + """ + if self._ed25519_pri is None: + self._ed25519_pri = sha512half(self.bytes) + return self._ed25519_pri + + @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_private_key)) + return self._ed25519_pub + + @property + def secp256k1_private_key(self): + """ + 32-byte secp256k1 private key (bytes) + """ + if self._secp256k1_pri is None: + self.derive_secp256k1_master_keys() + return self._secp256k1_pri + + @property + def secp256k1_public_key(self): + """ + 33-byte secp256k1 public key (bytes) + """ + if self._secp256k1_pub is None: + self.derive_secp256k1_master_keys() + return self._secp256k1_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_pri_i = secp256k1_private_key_from(self.bytes) + # root_pk_i.to_bytes(32, byteorder="big", signed=False) + root_pub_point = keys.get_public_key(root_pri_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_private_key_from(root_pub_b+fam_b) + inter_pub_point = keys.get_public_key(inter_pk_i, curve.secp256k1) + + # Private keys are just ints, so just add them mod the secp256k1 modulus + master_pri_i = (root_pri_i + inter_pk_i) % SECP_MODULUS + # Public keys are points, so the fastecdsa lib handles adding them + master_pub_point = root_pub_point + inter_pub_point + + self._secp256k1_pri = master_pri_i.to_bytes(32, byteorder="big", signed=False) + self._secp256k1_pub = compress_secp256k1_public(master_pub_point) + + # Saving the full key to make it easier to sign things later + self._secp256k1_full = master_pub_point + + def encode_secp256k1_public_base58(self): + """ + Return the base58-encoded version of the secp256k1 public key. + """ + return base58.b58encode_check(XRPL_PUBKEY_PREFIX + + self.secp256k1_public_key).decode() + +def secp256k1_private_key_from(seed): """ - Takes a seed (buffer) and outputs a 32-byte private key (buffer). - If seed is not provided, generates a seed at random using the OS-level - random number generator, which should be secure enough for this purpose. + Calculate a valid secp256k1 private key by hashing a seed value; + if the result isn't a valid key, increment a seq value and try + again. + + Returns a private key as a 32-byte integer. """ - if seed is None: - seed = randbits(32*8).to_bytes(32, byteorder="big") - return sha512half(seed) + 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 > SECP_MODULUS or h_i == 0: + # Not a valid secp256k1 key + seq += 1 + continue + break + return h_i + +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) + +if __name__ == "__main__": + p = argparse.ArgumentParser() + p.add_argument("secret", 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.") + args = p.parse_args() + + seed = Seed(args.secret) + seed.derive_secp256k1_master_keys() + + print(""" + Seed (base58): {base58} + Seed (hex): {hex} + Seed (RFC-1751): {rfc1751} + Ed25519 Secret Key (hex): {ed25519_secret} + Ed25519 Public Key (hex): {ed25519_public} + secp256k1 Secret Key (hex): {secp256k1_secret} + secp256k1 Public Key (hex): {secp256k1_public} + secp256k1 Public Key (base58): {secp256k1_pub_base58} + """.format( + base58=seed.encode_base58(), + hex=seed.encode_hex(), + rfc1751=seed.encode_rfc1751(), + ed25519_secret=seed.ed25519_private_key.hex().upper(), + ed25519_public=seed.ed25519_public_key.hex().upper(), + secp256k1_secret=seed.secp256k1_private_key.hex().upper(), + secp256k1_public=seed.secp256k1_public_key.hex().upper(), + secp256k1_pub_base58=seed.encode_secp256k1_public_base58(), + )) diff --git a/content/_code-samples/key-derivation/requirements.txt b/content/_code-samples/key-derivation/requirements.txt new file mode 100644 index 0000000000..13975df56d --- /dev/null +++ b/content/_code-samples/key-derivation/requirements.txt @@ -0,0 +1 @@ +fastecdsa==1.7.4 From 28f12b4f25e0ab53d9e0aaa0da91e6b8e57bda5b Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 9 Oct 2019 19:58:21 -0700 Subject: [PATCH 04/13] Key derivation - code cleanup --- content/_code-samples/key-derivation/key-derivation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py index ca0f5f63d5..2a9d0e4ef4 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -16,9 +16,6 @@ elif sys.version_info.minor < 6: else: from secrets import randbits -# import cryptography.hazmat.primitives.asymmetric.ec as ecc -# import cryptography.hazmat.backends.default_backend as default_backend - from fastecdsa import keys, curve import ed25519 @@ -166,14 +163,13 @@ class Seed: """ root_pri_i = secp256k1_private_key_from(self.bytes) - # root_pk_i.to_bytes(32, byteorder="big", signed=False) root_pub_point = keys.get_public_key(root_pri_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_private_key_from(root_pub_b+fam_b) inter_pub_point = keys.get_public_key(inter_pk_i, curve.secp256k1) - # Private keys are just ints, so just add them mod the secp256k1 modulus + # Private keys are ints, so just add them mod the secp256k1 modulus master_pri_i = (root_pri_i + inter_pk_i) % SECP_MODULUS # Public keys are points, so the fastecdsa lib handles adding them master_pub_point = root_pub_point + inter_pub_point From e36b90d1d35e3ff694e96e9acfce46e029242ce3 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 10 Oct 2019 12:18:58 -0700 Subject: [PATCH 05/13] Key derivation: byte order fix for RFC1751 --- .../key-derivation/key-derivation.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py index 2a9d0e4ef4..b2f7c8f879 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -73,7 +73,7 @@ class Seed: try: decoded = RFC1751.english_to_key(in_string) if len(decoded) == 16: - self.bytes = decoded + self.bytes = swap_byte_order(decoded) return else: raise ValueError @@ -114,7 +114,7 @@ class Seed: Returns a string representation of this seed as an RFC-1751 encoded passphrase. """ - return RFC1751.key_to_english(self.bytes) + return RFC1751.key_to_english(swap_byte_order(self.bytes)) @property def ed25519_private_key(self): @@ -218,6 +218,18 @@ def compress_secp256k1_public(point): 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", 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.") From 6e550aff883f14f7bde6e78e577e5a19f3d5dbf8 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 10 Oct 2019 12:32:34 -0700 Subject: [PATCH 06/13] Key derivation code: license/disclaimers --- content/_code-samples/key-derivation/RFC1751.py | 1 + content/_code-samples/key-derivation/ed25519.py | 3 ++- content/_code-samples/key-derivation/key-derivation.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/content/_code-samples/key-derivation/RFC1751.py b/content/_code-samples/key-derivation/RFC1751.py index 789289560b..51acc15aa3 100644 --- a/content/_code-samples/key-derivation/RFC1751.py +++ b/content/_code-samples/key-derivation/RFC1751.py @@ -2,6 +2,7 @@ # https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Util/RFC1751.py # This version has been adapted to Python 3 using the '2to3' utility # and manually adjusted for better Python 3 compatibility. +# Those revisions are released to the public domain. # rfc1751.py : Converts between 128-bit strings and a human-readable # sequence of words, as defined in RFC1751: "A Convention for diff --git a/content/_code-samples/key-derivation/ed25519.py b/content/_code-samples/key-derivation/ed25519.py index 077f25269f..adda08c1e2 100644 --- a/content/_code-samples/key-derivation/ed25519.py +++ b/content/_code-samples/key-derivation/ed25519.py @@ -1,5 +1,6 @@ # Python Implementation from https://ed25519.cr.yp.to/software.html -# Adjusted to Python 3 syntax by rome@ripple.com +# Adjusted to Python 3 syntax by rome@ripple.com. The revisions are +# released to the public domain. # Public domain software. This is a reference implementation that # does not include recommended speed & security optimizations. diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py index b2f7c8f879..9116565001 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -1,8 +1,18 @@ #!/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 7cef1abda79fc422bad9a234ffc2b0b864fab8ae Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 10 Oct 2019 16:20:42 -0700 Subject: [PATCH 07/13] Key derivation: fix RFC-1751, output base58 pubkeys --- .../key-derivation/key-derivation.py | 92 +++++++++++++++---- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py index 9116565001..5526ad4118 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -33,7 +33,8 @@ import RFC1751 import base58.base58 as base58 XRPL_SEED_PREFIX = b'\x21' -XRPL_PUBKEY_PREFIX = b'\x23' +XRPL_ACCT_PUBKEY_PREFIX = b'\x23' +XRPL_VALIDATOR_PUBKEY_PREFIX = b'\x1c' ED_PREFIX = b'\xed' SECP_MODULUS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 @@ -48,7 +49,7 @@ class Seed: A 16-byte value used for key derivation. """ - def __init__(self, in_string=None): + 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. @@ -58,6 +59,7 @@ class Seed: - hexadecimal - passphrase """ + self.correct_rfc1751 = correct_rfc1751 # Keys are lazy-derived later self._secp256k1_pri = None self._secp256k1_pub = None @@ -66,7 +68,8 @@ class Seed: if in_string is None: # Generate a new seed randomly from OS-level RNG. - self.bytes = randbits(32*8).to_bytes(32, byteorder="big") + self.bytes = randbits(16*8).to_bytes(16, byteorder="big") + return # Is it base58? try: @@ -83,7 +86,11 @@ class Seed: try: decoded = RFC1751.english_to_key(in_string) if len(decoded) == 16: - self.bytes = swap_byte_order(decoded) + if correct_rfc1751: + self.bytes = decoded + else: + self.bytes = swap_byte_order(decoded) + return else: raise ValueError @@ -119,12 +126,21 @@ class Seed: """ return self.bytes.hex().upper() - def encode_rfc1751(self): + def encode_rfc1751(self, correct_rfc1751=None): """ Returns a string representation of this seed as an RFC-1751 encoded passphrase. """ - return RFC1751.key_to_english(swap_byte_order(self.bytes)) + # 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_private_key(self): @@ -159,12 +175,22 @@ class Seed: @property def secp256k1_public_key(self): """ - 33-byte secp256k1 public key (bytes) + 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 @@ -186,16 +212,36 @@ class Seed: self._secp256k1_pri = master_pri_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): + def encode_secp256k1_public_base58(self, validator=False): """ Return the base58-encoded version of the secp256k1 public key. """ - return base58.b58encode_check(XRPL_PUBKEY_PREFIX + - self.secp256k1_public_key).decode() + 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_private_key_from(seed): """ @@ -242,28 +288,40 @@ def swap_byte_order(buf): if __name__ == "__main__": p = argparse.ArgumentParser() - p.add_argument("secret", 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.") + 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) + seed = Seed(args.secret, correct_rfc1751=args.unswap) seed.derive_secp256k1_master_keys() print(""" Seed (base58): {base58} Seed (hex): {hex} - Seed (RFC-1751): {rfc1751} - Ed25519 Secret Key (hex): {ed25519_secret} + Seed (true RFC-1751): {rfc1751_true} + Seed (rippled RFC-1751): {rfc1751_rippled} + Ed25519 Private Key (hex): {ed25519_secret} Ed25519 Public Key (hex): {ed25519_public} - secp256k1 Secret Key (hex): {secp256k1_secret} + Ed25519 Public Key (base58 - Account): {ed25519_pub_base58} + secp256k1 Private Key (hex): {secp256k1_secret} secp256k1 Public Key (hex): {secp256k1_public} - secp256k1 Public Key (base58): {secp256k1_pub_base58} + 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=seed.encode_rfc1751(), + rfc1751_true=seed.encode_rfc1751(correct_rfc1751=True), + rfc1751_rippled=seed.encode_rfc1751(correct_rfc1751=False), ed25519_secret=seed.ed25519_private_key.hex().upper(), ed25519_public=seed.ed25519_public_key.hex().upper(), secp256k1_secret=seed.secp256k1_private_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(), )) From db451108ab124b740e97fba3a72b8f2336bd6104 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 10 Oct 2019 16:21:01 -0700 Subject: [PATCH 08/13] Key derivation: correct bytes for secp256k1 pubkey --- .../accounts/cryptographic-keys.md | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md index 3855b77255..e6064d0bcc 100644 --- a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md +++ b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md @@ -120,23 +120,47 @@ In the future, it is likely that the XRP Ledger will need new cryptographic sign The process of deriving a key pair depends on the signing algorithm. In all cases, keys are generated from a _seed_ value that is 16 bytes (128 bits) in length. The seed value can be completely random (recommended) or it can be derived from a specific passphrase by taking the [SHA-512 hash][Hash] and keeping the first 16 bytes (similar to [SHA-512Half][], but keeping only 128 bits instead of 256 bits of the output). +### Sample Code + +The key derivation processes described here are implemented in multiple places and programming languages: + +- In C++ in the `rippled` code base: + - [Seed definition](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/Seed.h) + - [General & Ed25519 key derivation](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/impl/SecretKey.cpp) + - [secp256k1 key derivation](https://github.com/ripple/rippled/blob/develop/src/ripple/crypto/impl/GenerateDeterministicKey.cpp) +- In Python 3 in [this repository's code samples section]({{target.github_forkurl}}/blob/{{target.github_branch}}/content/_code-samples/key-derivation/key-derivation.py). +- In JavaScript in the [`ripple-keypairs`](https://github.com/ripple/ripple-keypairs/) package. + ### Ed25519 Key Derivation [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/protocol/impl/SecretKey.cpp#L203 "Source") +![] + All 32-byte numbers are valid Ed25519 private keys, so Ed25519 private key derivation is a single step: -- Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte private key. +1. Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte private key. -To calculate an Ed25519 public key, use the standard public key derivation for [Ed25519](https://ed25519.cr.yp.to/software.html) to derive the public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) +2. To calculate an Ed25519 public key, use the standard public key derivation for [Ed25519](https://ed25519.cr.yp.to/software.html) to derive the 32-byte public key. + + **Caution:** As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions. + +3. Prefix the 32-byte public key with the single byte `0xED` to indicate an Ed25519 public key, resulting in 33 bytes. + + If you are implementing code to sign transactions, remove the `0xED` prefix and use the 32-byte key for the actual signing process. + +4. When serializing an account public key to [base58][], use the account public key prefix `0x23`. + + Validator ephemeral keys cannot be Ed25519. ### secp256k1 Key Derivation +[[Source]](https://github.com/ripple/rippled/blob/develop/src/ripple/crypto/impl/GenerateDeterministicKey.cpp "Source") -Key derivation for secp256k1 XRP Ledger keys involves more steps than Ed25519 key derivation for a couple reasons: +Key derivation for secp256k1 XRP Ledger account keys involves more steps than Ed25519 key derivation for a couple reasons: - Not all 32-byte numbers are valid secp256k1 private keys. - The XRP Ledger's reference implementation has an unused, incomplete framework for deriving a family of key pairs from a single seed value. -The steps to derive a valid secp256k1 private key from a seed value are as follows: +The steps to derive the XRP Ledger's secp256k1 account key pair from a seed value are as follows: 1. Calculate a "root key pair" from the seed value, as follows: @@ -152,10 +176,20 @@ The steps to derive a valid secp256k1 private key from a seed value are as follo 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the root public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) -2. Derive an intermediate key pair from the root public key you calculated in step 1, as follows: + **Tip:** Validators use this root key pair. If you are calculating a validator's key pair, you can stop here. To distinguish between these two different types of public keys, the [base58][] serialization for validator public keys uses the prefix `0x1c`. + +2. Convert the root public key to its 33-byte compressed form. + + The uncompressed form of any ECDSA public key consists of a pair of 32-byte integers: an X coordinate, and a Y coordinate. The compressed form is just the X coordinate and a one-byte prefix: `0x02` if the Y coordinate is even, or `0x03` if the Y coordinate is odd. + + You can convert an uncompressed public key to the compressed form with the `openssl` commandline tool. For example, if the uncompressed public key is in the file `ec-pub.pem`, you can output the compressed form like this: + + $ openssl ec -in ec-pub.pem -pubin -text -noout -conv_form compressed + +3. Derive an "intermediate key pair" from the compressed root public key you, as follows: 1. Concatenate the following in order, for a total of 40 bytes: - - The root public key (32 bytes) + - The compressed root public key (33 bytes) - `0x00000000000000000000000000000000` (4 bytes of zeroes). (This value was intended to be used to derive different members of the same family, but in practice only the value 0 is used.) - A "key sequence" value (4 bytes), as a big-endian unsigned integer. Use 0 as a starting value for the key sequence. @@ -165,7 +199,19 @@ The steps to derive a valid secp256k1 private key from a seed value are as follo 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the intermediate public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) -3. Calculate the sum of the root public key and the intermediate public key. The result is the master public key. ***TODO: which private key do you use?*** +4. Derive the master public key pair by adding the intermediate public key to the root public key. Similarly, derive the private key by adding the intermediate private key to the root private key. + + - An ECDSA private key is just a very large integer chosen at random, so you can calculate the sum of two private keys by summing them modulo the secp256k1 modulus. + + - An ECDSA public key is a point on the elliptic curve, so you should use a well-established elliptic curve implementation to sum the points. + + **Tip:** You don't need any private keys to derive the master public key. You can do so using only the root public key. + +5. Convert the master public key to its 33-byte compressed form, as before. + +6. When serializing an account's public key to its [base58][] format, use the account public key prefix, `0x23`. + + See [Address Encoding](accounts.html#address-encoding) for information and sample code to convert from an account's public key to its address. ## See Also From 4f6579f1cef79c7c1c860b9673f671f2cb7b1a21 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 10 Oct 2019 17:19:24 -0700 Subject: [PATCH 09/13] Key Derivation polish - Fix address encoding diagram - Add key derivation diagrams - Clean up text for key formatting --- ...-address-rels.uxf => address-encoding.uxf} | 128 +---- .../_img-sources/key-derivation-ed25519.uxf | 136 +++++ .../_img-sources/key-derivation-secp256k1.uxf | 517 ++++++++++++++++++ .../accounts/accounts.md | 4 +- .../accounts/cryptographic-keys.md | 12 +- img/address-encoding.png | Bin 0 -> 21386 bytes img/key-derivation-ed25519.png | Bin 0 -> 17082 bytes img/key-derivation-secp256k1.png | Bin 0 -> 61282 bytes 8 files changed, 692 insertions(+), 105 deletions(-) rename content/_img-sources/{key-address-rels.uxf => address-encoding.uxf} (59%) create mode 100644 content/_img-sources/key-derivation-ed25519.uxf create mode 100644 content/_img-sources/key-derivation-secp256k1.uxf create mode 100644 img/address-encoding.png create mode 100644 img/key-derivation-ed25519.png create mode 100644 img/key-derivation-secp256k1.png diff --git a/content/_img-sources/key-address-rels.uxf b/content/_img-sources/address-encoding.uxf similarity index 59% rename from content/_img-sources/key-address-rels.uxf rename to content/_img-sources/address-encoding.uxf index 9c2823ed9f..cfb0ed671e 100644 --- a/content/_img-sources/key-address-rels.uxf +++ b/content/_img-sources/address-encoding.uxf @@ -4,76 +4,22 @@ UMLObject - 60 - 50 - 110 - 70 - - Passphrase --- -(Optional) -Any string -lt=. - - - - UMLObject - - 320 - 50 - 130 - 70 - - Seed --- -(16 bytes) - - - - Relation - - 160 + 40 70 - 180 - 50 - - lt=<. -SHA512, keep -first 16 bytes - 160.0;20.0;10.0;20.0 - - - UMLObject - - 60 - 180 220 70 - Public Key + Master Public Key -- 33 bytes (secp256k1) 0xED + 32 bytes (Ed25519) - - Relation - - 160 - 80 - 600 - 120 - - lt=<- -Public Key -Derivation - 10.0;100.0;10.0;70.0;580.0;70.0;580.0;10.0;550.0;10.0 - UMLObject - 360 - 270 + 340 + 160 180 80 @@ -85,8 +31,8 @@ Derivation Relation - 270 - 180 + 250 + 70 220 160 @@ -105,8 +51,8 @@ RIPEMD160 of SHA-256 UMLObject - 690 - 270 + 670 + 160 180 80 @@ -119,22 +65,22 @@ Checksum (4 bytes) UMLObject - 60 - 270 + 40 + 160 190 80 Type Prefix -- 0x00 -("r" in Ripple's base58) +("r" in XRPL base58) Relation - 240 - 310 + 220 + 200 140 30 @@ -144,8 +90,8 @@ Checksum (4 bytes) UMLObject - 400 - 400 + 380 + 290 100 70 @@ -157,8 +103,8 @@ Checksum (4 bytes) Relation - 490 - 310 + 470 + 200 120 140 @@ -168,8 +114,8 @@ Checksum (4 bytes) Relation - 530 - 290 + 510 + 180 80 30 @@ -179,8 +125,8 @@ Checksum (4 bytes) UMLState - 590 - 290 + 570 + 180 100 40 @@ -191,8 +137,8 @@ type=sender Relation - 420 - 340 + 400 + 230 130 80 @@ -201,29 +147,15 @@ SHA-256 twice 10.0;60.0;10.0;10.0 - UMLObject + Text - 600 - 50 - 110 - 70 + 40 + 30 + 280 + 30 - Private Key --- -(32 bytes) + *Address Encoding* +style=wordwrap - - Relation - - 440 - 70 - 180 - 50 - - lt=<- -Private Key -Derivation - 160.0;20.0;10.0;20.0 - diff --git a/content/_img-sources/key-derivation-ed25519.uxf b/content/_img-sources/key-derivation-ed25519.uxf new file mode 100644 index 0000000000..6da8a233bc --- /dev/null +++ b/content/_img-sources/key-derivation-ed25519.uxf @@ -0,0 +1,136 @@ + + + 10 + + UMLObject + + 20 + 60 + 120 + 70 + + Passphrase +-- +(Optional) +Any string +lt=. + + + + Relation + + 130 + 80 + 170 + 50 + + lt=.> +SHA-512, keep +first 16 bytes + 10.0;20.0;150.0;20.0 + + + UMLObject + + 280 + 60 + 120 + 70 + + Seed +-- +(16 bytes) + + + + UMLObject + + 170 + 200 + 230 + 70 + + Private Key +-- +(32 bytes) + + + + Relation + + 290 + 120 + 120 + 100 + + lt=<- +SHA-512Half + 10.0;80.0;10.0;10.0 + + + UMLObject + + 170 + 330 + 230 + 70 + + Public Key +-- +(32 bytes) + + + + UMLObject + + 110 + 330 + 60 + 70 + + 0xED +Prefix +-- +(1 byte) + + + + + Relation + + 230 + 260 + 180 + 90 + + lt=-> +Public Key Derivation + 10.0;10.0;10.0;70.0 + + + Relation + + 100 + 400 + 320 + 70 + + lt=.. + +Master Public Key +(33 bytes) + 10.0;10.0;10.0;30.0;300.0;30.0;300.0;10.0 + + + Text + + 20 + 20 + 280 + 30 + + *Ed25519 Key Derivation* +style=wordwrap + + + diff --git a/content/_img-sources/key-derivation-secp256k1.uxf b/content/_img-sources/key-derivation-secp256k1.uxf new file mode 100644 index 0000000000..8d0c64fd27 --- /dev/null +++ b/content/_img-sources/key-derivation-secp256k1.uxf @@ -0,0 +1,517 @@ + + + 10 + + UMLObject + + 50 + 70 + 140 + 70 + + Passphrase +-- +(Optional) +Any string +lt=. + + + + Relation + + 180 + 90 + 180 + 50 + + lt=.> +SHA-512, keep +first 16 bytes + 10.0;20.0;160.0;20.0 + + + UMLObject + + 340 + 70 + 140 + 70 + + Seed +-- +(16 bytes) + + + + UMLObject + + 50 + 280 + 150 + 70 + + Root Private Key +-- +(32 bytes) + + + + Relation + + 110 + 130 + 460 + 170 + + lt=<- +SHA-512Half + 10.0;150.0;10.0;110.0;440.0;110.0;440.0;10.0;400.0;10.0 + + + UMLObject + + 330 + 280 + 150 + 70 + + Root Public Key +-- +(33 bytes +compressed) + + + + Relation + + 190 + 300 + 160 + 50 + + lt=-> +Public Key +Derivation + 10.0;20.0;140.0;20.0 + + + Text + + 50 + 30 + 280 + 30 + + *secp256k1 Key Derivation* +style=wordwrap + + + + UMLSyncBarVertical + + 500 + 110 + 20 + 70 + + template=txt +title=titletext +bg=red + + + + UMLObject + + 370 + 150 + 110 + 70 + + Root Key +Sequence +-- +(4 bytes; +default 0) + + + + Relation + + 470 + 120 + 60 + 30 + + lt=-> + 10.0;10.0;40.0;10.0 + + + Relation + + 470 + 150 + 60 + 30 + + lt=-> + 10.0;10.0;40.0;10.0 + + + UMLObject + + 370 + 440 + 110 + 70 + + Int Key +Sequence +-- +(4 bytes; +default 0) + + + + UMLObject + + 370 + 360 + 110 + 70 + + Family +Number +-- +(4 bytes; +all 0's) + + + + UMLNote + + 140 + 170 + 210 + 50 + + Increment the key sequence and try again if the SHA-512Half doesn't make a valid private key. +bg=yellow +fontsize=10 +style=wordwrap + + + + UMLNote + + 150 + 460 + 210 + 50 + + Increment the key sequence and try again if the SHA-512Half doesn't make a valid private key. +bg=yellow +fontsize=10 +style=wordwrap + + + + Relation + + 140 + 380 + 410 + 220 + + lt=<- +SHA-512Half + 10.0;200.0;10.0;160.0;390.0;160.0;390.0;10.0;370.0;10.0 + + + UMLSyncBarVertical + + 500 + 360 + 20 + 80 + + template=txt +title=titletext +bg=red + + + + Relation + + 470 + 320 + 60 + 80 + + lt=-> + 10.0;10.0;20.0;10.0;20.0;60.0;40.0;60.0 + + + Relation + + 470 + 390 + 60 + 30 + + lt=-> + 10.0;10.0;40.0;10.0 + + + Relation + + 470 + 410 + 60 + 70 + + lt=-> + 10.0;50.0;20.0;50.0;20.0;10.0;40.0;10.0 + + + UMLObject + + 80 + 580 + 150 + 80 + + Intermediate +Private Key +-- +(32 bytes) + + + + + UMLObject + + 330 + 580 + 150 + 80 + + Intermediate +Public Key +-- +(33 bytes +compressed) + + + + Relation + + 220 + 610 + 130 + 50 + + lt=-> +Public Key +Derivation + 10.0;20.0;110.0;20.0 + + + UMLState + + 660 + 330 + 140 + 40 + + Elliptic Curve +Point Add +type=sender + + + + Relation + + 470 + 290 + 210 + 70 + + lt=<- + 190.0;50.0;150.0;50.0;150.0;10.0;10.0;10.0 + + + Relation + + 470 + 350 + 210 + 280 + + lt=<- + 190.0;10.0;150.0;10.0;150.0;260.0;10.0;260.0 + + + UMLObject + + 800 + 320 + 150 + 70 + + Master Public Key +-- +(33 bytes +compressed) + + + + UMLState + + 150 + 700 + 160 + 40 + + Add, Mod +Curve Modulus +type=sender + + + + Relation + + 100 + 650 + 70 + 80 + + lt=<- + 50.0;60.0;10.0;60.0;10.0;10.0 + + + Relation + + 50 + 340 + 120 + 410 + + lt=<- + 100.0;390.0;10.0;390.0;10.0;10.0 + + + UMLObject + + 330 + 690 + 150 + 70 + + Master Private Key +-- +(32 bytes) + + + + Relation + + 300 + 710 + 50 + 30 + + lt=<- + 30.0;10.0;10.0;10.0 + + + Relation + + 470 + 380 + 410 + 380 + + lt=.> +(Public key derivation +yields the same result.) + 10.0;350.0;390.0;350.0;390.0;10.0 + + + UMLNote + + 590 + 230 + 210 + 50 + + Validators use the root key pair. +bg=green +style=wordwrap + + + + Relation + + 200 + 240 + 410 + 60 + + lt=<.. +fg=#25A768 +transparency=0 + 10.0;40.0;390.0;10.0 + + + Relation + + 480 + 260 + 130 + 50 + + lt=<.. +fg=#25A768 +transparency=0 + 10.0;30.0;110.0;10.0 + + + UMLNote + + 630 + 630 + 210 + 50 + + Accounts use the master key pair. +bg=green +style=wordwrap + + + + Relation + + 760 + 380 + 80 + 270 + + lt=<.. +fg=#25A768 +transparency=0 + 60.0;10.0;10.0;250.0 + + + Relation + + 470 + 660 + 180 + 70 + + lt=<.. +fg=#25A768 +transparency=0 + 10.0;50.0;160.0;10.0 + + diff --git a/content/concepts/payment-system-basics/accounts/accounts.md b/content/concepts/payment-system-basics/accounts/accounts.md index e2a31ebfe4..7e24edc466 100644 --- a/content/concepts/payment-system-basics/accounts/accounts.md +++ b/content/concepts/payment-system-basics/accounts/accounts.md @@ -89,11 +89,11 @@ For more information on each of these objects, see the [Ledger Format Reference] [[Source]
](https://github.com/ripple/rippled/blob/35fa20a110e3d43ffc1e9e664fc9017b6f2747ae/src/ripple/protocol/impl/AccountID.cpp#L109-L140 "Source") -XRP Ledger addresses are encoded using [base58](https://en.wikipedia.org/wiki/Base58) with the Ripple _dictionary_: `rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz`. Since the XRP Ledger encodes several types of keys with base58, it prefixes the encoded data with a one-byte "type prefix" (also called a "version prefix") to distinguish them. The type prefix causes addresses to usually start with different letters in base58 format. +XRP Ledger addresses are encoded using [base58](https://en.wikipedia.org/wiki/Base58) with the _dictionary_ `rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz`. Since the XRP Ledger encodes several types of keys with base58, it prefixes the encoded data with a one-byte "type prefix" (also called a "version prefix") to distinguish them. The type prefix causes addresses to usually start with different letters in base58 format. The following diagram shows the relationship between keys and addresses: -![Passphrase → Seed → Private Key → Public Key + Type Prefix → Account ID + Checksum → Address](img/key-address-rels.png) +[![Master Public Key + Type Prefix → Account ID + Checksum → Address](img/address-encoding.png)](img/address-encoding.png) The formula for calculating an XRP Ledger address from a public key is as follows. For the complete example code, see [`encode_address.js`](https://github.com/ripple/ripple-dev-portal/blob/master/content/_code-samples/address_encoding/encode_address.js). For the process of deriving a public key from a passphrase or seed value, see [Key Derivation](cryptographic-keys.html#key-derivation). diff --git a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md index e6064d0bcc..a1e936d396 100644 --- a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md +++ b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md @@ -134,12 +134,12 @@ The key derivation processes described here are implemented in multiple places a ### Ed25519 Key Derivation [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/protocol/impl/SecretKey.cpp#L203 "Source") -![] - -All 32-byte numbers are valid Ed25519 private keys, so Ed25519 private key derivation is a single step: +[![Passphrase → Seed → Private Key → Prefix + Public Key](img/key-derivation-ed25519.png)](img/key-derivation-ed25519.png) 1. Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte private key. + **Tip:** All 32-byte numbers are valid Ed25519 private keys. However, only numbers that are chosen randomly enough are secure enough to be used as private keys. + 2. To calculate an Ed25519 public key, use the standard public key derivation for [Ed25519](https://ed25519.cr.yp.to/software.html) to derive the 32-byte public key. **Caution:** As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions. @@ -155,6 +155,8 @@ All 32-byte numbers are valid Ed25519 private keys, so Ed25519 private key deriv ### secp256k1 Key Derivation [[Source]](https://github.com/ripple/rippled/blob/develop/src/ripple/crypto/impl/GenerateDeterministicKey.cpp "Source") +[![Passphrase → Seed → Root Key Pair → Intermediate Key Pair → Master Key Pair](img/key-derivation-secp256k1.png)](img/key-derivation-secp256k1.png) + Key derivation for secp256k1 XRP Ledger account keys involves more steps than Ed25519 key derivation for a couple reasons: - Not all 32-byte numbers are valid secp256k1 private keys. @@ -201,9 +203,9 @@ The steps to derive the XRP Ledger's secp256k1 account key pair from a seed valu 4. Derive the master public key pair by adding the intermediate public key to the root public key. Similarly, derive the private key by adding the intermediate private key to the root private key. - - An ECDSA private key is just a very large integer chosen at random, so you can calculate the sum of two private keys by summing them modulo the secp256k1 modulus. + - An ECDSA private key is just a very large integer, so you can calculate the sum of two private keys by summing them modulo the secp256k1 modulus. - - An ECDSA public key is a point on the elliptic curve, so you should use a well-established elliptic curve implementation to sum the points. + - An ECDSA public key is a point on the elliptic curve, so you should use elliptic curve math to sum the points. **Tip:** You don't need any private keys to derive the master public key. You can do so using only the root public key. diff --git a/img/address-encoding.png b/img/address-encoding.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4e65ec8047ef1b5549a9ac9db53ef5e2c7a511 GIT binary patch literal 21386 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 uofrP=&;RS2{_}3H6Fr6 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 V;kK1|_&+H~R1`Ikd6#cI_wT-XuohB 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

ArF3Pkj84B^j9{hjDK@o|Eck|{= zZZ0lMOUuCm{eqmF_Ucc?#dgbsc>t0}M@OL_$VM<_WoL)nXbBwb?zWlz`VQ7+zIbM4 z#*&b;iKe2~+c3j&q}V(D{>-aauRf3qHO$@MnvzVN3i|z{GXg}Q z?E&^JE3>)sHNk}5w*^>=irsoOuDTVrNvWySw{PF(;d$QZgMHREgAAnD$h$u_@jQmq zgMI>t-l*A+3Hvr{c1cP9)D$$KsfLEf%>g&&ngh=|&r1w3uT5tk3`U~)Vjmx$+S=NM zg@wK%D%r*s|Et*8G@J^uvTjiCrKGM*2%OEc)WjcO^}Ln&7OC%v0JA2J^r{ zb08^`j*&UBi!u!jjh2=c^jM=;0AwN}BL03jB^x%yLcY$~7wBQ9+JCl}R36Si20n+G zOjkEv{tZdQB7+z@<| zrC8^A{I#GMBFT$EOhU4inVg(#HCDE>HWAyfyE;ZExIEkB51kMy1*#GWyYuN{A;-5L zsjBwU-FxsL14_`@^U8U_`knQUFkP<^bToR-z&*|D(U(Q9BOrohv9Yn)*}ry{hhXJk zy5vaVv6+;U`HC-d-wOtt;{kPjA@{wF&AD$UCnu7Ul2AZiWAF+=165a6W?*8%*CdhD z!}rMN;^t;2gK97OJIJ6%=C8!d9~BU^b?jVRg3o5Z_9Tg=sc)$$d3ldgQr7oLOw_nJ z)llyNraL$|K+TK6B2KSb94?0uEX9aK4i7%h^2|bb7-)Q{|_0*Ao`Keit4weg~@r+Fx zXf$MHWg)qShn@Q~l_Od8^9wKlqh)4hx&!v`lVv0La4kwO%DX+bO4R5xdM%nEbZG6D zK@OGNe1ou#fa1-Jjnne-CMxYsIn=k*$r)imJLoygMMOlNdf40Bzj*ONE*M2|Ag__! zpTilA;KfA?HHd!AUV-7}>|EFobP^L2L(cCYHK|%=GnEh&6cia586Lg^BR?@QF@REv zoe%>X8ygD?i>*hOXq+MKVYvZiFkyx9o`r{rk(Za3yws$V1m1;DEoYC^4@&b(HTrsI z_WV+z1miArB^7l=TN9J?m>3eLkq;s#&R^eN_tRMeun&Erx3||%b|YEBe|viyW<>nf z#EyZsHW?Kal|8EX>Pjr}bO8g+&IF-YQ@bA>H~g6C>0dsX(S}$0cuB@qj{_xYCVwv&$ z6%%qlVto7hHR!_kG=gd@4s{q8I5|H}PaBZa+pJv-Uc%8(v;WcY3id2?Wl=BE8zLfg zJv~aEFdH35bDF}YD>MJ@a}v+~1k;;7q#NL**!}x4dS0JCeR6bk)c5{cmyZdH|5nx( z`l9BKSLm1D6p@pYKYRAfQO|1!!`IszFl||kK;Z&J<-h21%SrVhM+y9V@Ak{U4jlpX(8Aey1=y||EaetJIqMHih zDOCtG!uFME*z+nYSt=Q+1zUg^a7lSOdU}F92b3=Lt&@?5M@7-I=XxpL{Fs%+a9yII zEjfZk_t(k_lS+lRN-B^?c`#)iy&#>Jx46X`SG@K6cgjK1hq&P2>mGi4bmDJ1I^>{! zXFt47(5y3*_msyPHd4^^>EpIA26uy>zZbg)`uo-Lbr^z8U|-;O4YJ`y7s$Btef7G8 z#s7RIDl9B4GV%eT#M7q1g5+z^xSiG~<<3yvUq6Lw8_8?{GRk~o zH0&%`E~w|ZdDrDmfaDugika0Q1r5HSU80HVbfn?X4}XiHU-djN1d&k}540gvO-&zw zje>D~-!&~y|Aj%8T})mDl^dfp#P-A7w-FH;4Yo{Y+-O?pG_8FuPzcU$Zl63Z`HHo~ z7RZQuNt@D8JOAj2hQNiYk+IYd3;@S6INt_10NNqM%4)P^#*4+CPaIG2uM07^?-Teu zIb$8^<1u(|XqQ?_ynpAzS6pIhl`bP!S6BJ{#;jh`IqO^;yC(AkM5TT( z#@vJe3I&|*SPsHoTgESB=#vZ}ezx;#U~9{c=h7<}KZEzPrJi2CM*PL3JX~$y5*Fd` z@GvzU-QwaRE5M(<$8mCq^c>O~HzXt_2?+>vYu(}9G{wChyeE@p(C&DUk)8eYV0+2o z(4jpmE33fz^MZ+yL0}6swbj+tCEl^+n7~-!zOgc^l&M!7rpCZOO-`Mj>;jtp9?1w? z29j3|uX>)^M_z=I7X=e;)fXIXGU*I1pBx`vxNreV>#twGQtMz}HE0rv|Hz@rTAiw^ z1D>v$q3=g`q7BeiLJ~1@i2SVha{ta~i9gkz$F8nu?O_JU?R0&nx$yht(vIypH5ZUf z`2wu0hVnKBffqfPmM-f-yL|zzA!dk|boWPR?9u}qZ92u|6KFR}--VyA45RjBC(IMF zA9`N|%J+Q5vpcJv$fKt&8`BqVv!a9ji=ax4FllILHc?b zfql_WCogOlUI^mh(aKGKFhz#NoUS2EesP-jQG0kC-+PVOx=j#`71>hd?;}0jPO2fl zTJ>QkywC?g7!D&NBed9eO{;qCmgBQKQ)#k9Gb@a<^0(}ptq+#&Ck8h9;{Mj>epF+r zMeuHl63RX-Kx=DjQ&UqII`=p%mBrJcvG0R}>H0GG3U%I8w{L&{{(Xc&UhsTTwNfw` zR})R7Z1K2y+QJzRx0f1mX{a^M=Ofc61q=IrUK9D)5rvVwg+9_69?~*0cF=d6oD}8d zJp!jVW>8O6ke^@cyg3_u`@vIlb4_h+Xgco#5D~mjoJ2*@26)D%O3XtmEAJi>P+EHR z^*z+po%s4y+|a(T>9ftmXNgOpl@DFr^^!mP# zFd6!VhKA0~%{?(T-f1Gzul{2bZOdc5d7!SM^1Y{r&w1mPs+)|A4CoL02LpZbw``iN z`GdD09q!4mW^)~>=M*A<9i^|)Vg?r9@{To6f70jAF~Vq&#r+4{=#qO=7yp6V8y2C{ zF28?l(*Mr~xVi=H3R-t&CjE>KO~Ix%ZCs#s^~qBCn~V%P5giy{lai7IQE$yVGe0k$ zn7qC1Fmp8CKx=TjVZVe8sf4HGvu&RVq+X>#?*a|Ne3-i=g9{7Tf()HsD1jGKcagsy(eWFAo~s5R0~Dn>#uJl866IH>e?T zFrm+HQQfoeOdys$6|AXz-sIzh5}J!5>M!n3oN_?<#Ig*0>2gUy(}K+!@lB9cHe45Az6+v56ut z65`^3q87zu;=qphtI2ddB)?sS9l1f!sTztH)mewc;OTZIQ%i&b(kxYDvslwo8 z&rnQEx9MDHkBr@&QbH|y$$f;ijG&-klzr$w7(6qhfn^dp<=(-;U`ryV?AzH-PSSFR zG|csV#kC;ecws;zZ)Qw-T$z>Gp|df(fE7~emgCL$b|=YcYFH{ zM&p-T^~}F)%wCgZ78dqQPI|)By5*ztcm1ZDFF=04fabjR6?t2!5@V<(8dG-kH9>TZ ztZOrRuySGUH9(?C%O=LgnR`8f>~h|QwbpL`qj_Nu zPxN-#7A7*A?Yz~p`ftg-R)tDgYN0;kioHRHVg>(wKYhO;VQfMIgZ2pK4-_I@t*rGy$I`H{6!8E(YMP;^+9R|t=Ik~l^Wi^aIetuWa<9x5N zUa;PlsfUTg3@Wdhni_xs#RS2b5mc^-y3hdC37jYPHb({>4NWC*^Ho(D>FF{?Sg2CJ z5Ds^SpY|8x+1uH@UmzxBX;L5%b&d{ z8k%z_Fb3)m@XnajzdxWJ=#2bD-2u7~Dt2cB87k*d+CS<8DpY45&eZ+059h1(cMQ?= zvP>>8#a@Gpp?*8>zLzpWMmzAIpWfmRGH~ZPzm^hhZ#0h;2_82zQ*G@r=r;s}gtc-$ zDGxs2AQ0Cp$6=lJXJD4i%*Y@jA<@#+#YXs`&@4ud3cUx1@ z7JvSXii^|H(Q$`a965D-xU1p}@?~x9$xNfKW-(A6{JTJ)v?&E{Jv!UA5L+N$rhzgA znY_~D@Q;pR>n|x@$lrE60RaJ(+-loPG#X-JudlwC{&K;PZvhA{V5aFEW+}yd3XfmD zL~Dy!wHYmG9Urd-?#LxuSokV zg0N|zlKA=fwto#Q1F9AjWM+oP%8`KjB_ZLB144DjW0<+6E!ja6qas-AO}V^UxB>ivQbOv` zmV$$womQotv8(InuW8|(MA28zYv(mIGje3d+kk)-AS+;Uv^cTh`wC2+v*r3^&A#oG zCEK#~g#`k9{6~um34$*7^_C&jp`l{FfE0n?1Sk>iGz(xGI4BZg;-714lNAHQaztq; zN)g4QN!g}Ivn>?>OZDgxzJN}l!K=VPd62R_Jx`$5d$TsTUMtA*D7F6b^1>D^DWM(< zFYi!OQyOUYmT_WoasjVi-MfGPJ`7R7GP}DU6&ttjy>p-hX&)Oq5Q584$x1HbN%l{! z+J{WXjPiCnI5BDC_RB2$JJj8l4EQ-&bH;e0Ug)jD9Gk<>9^eKTO}RrleSLkQ z-lMima`Kswh1#{^8Q1wh9f8&ijWxWG#A#*N(AaokeH}$zBru&{c-oMvF_)Ya9UoOb*BAHB2uC!Itcw6wT?%+K%b?SU5WvDrk#@yYw|EsfS|t~{{! zFTivOH8?9j5RR8WOTIjvZ+!8et0?_lPlJn z;2~rvQQR@AkZCV|vSr}03@q)XYa1N&%R}V8NuFC;8UbDwMO%f11?55owH_|KfJD_U zD1X$%q-Bxe)1?*0O)hjL}prPMcDH$8n zh*%sS?C5@W=H9cK+q`^iYk~n}KrK$|Z$jQ!L105DP zsjx3!z8F9)V*;??SF$%|QwQ812rJRt77_pR{!(7%zc*bhI--<;`<9{M*Z15{10e^B zA#}Ye@IS#6xlyZkRZ&h3pMNumQp_wPHC0?zc6qw~HB;PL3(Pz^UJ`askYG!2Cg(P8 z!{4$SrThkj#L3a`ZtqKk-B-^7e+Fk_?WBRq%Z*e|1pfy^>`y_DgRn==YvYv=AZ2xR z2wlb(&~(}~^9Z5PzytBGj8}3>=+?M~|ABF;$)1?gB%lJrf4-TTkBZ7GQG(_MuQl1K zPPKDzh;E&yu7=iT?yY)Mc2g6VNFusg-`U!6xH&FqZg}=M}QGO-xX7 zaXq>#f#MNHOU(KC`9Zcu2|jTJx>dCC0n)b6@Z*s{b&C_Sgph_#dt(vF$g&B@KDNgG z&n>v#W2is+`t)fwrv(B zCu3+xN$OV!g3uBds186MbNGr~BjGaZO_mT$1HuW5s8wFvYeW-D3jyu_2puvv0NIf} zFF!DSha4uELm0$ABMbsjZUFY`p@73*%uTaf0Kfu4ul~>0wl6Q zFQC1%lc1KE=;lo;3yav`;LVkhPhctAu-zP=mu9;sWA*s4-S*F37_R`x6cf#f7kiyi zx4{3mAmwK7tIaPiJ~sGx*Y=~;z52&j$E#gR0_C+~l@1~^_J5{Ky1J4$t^26tmX;>f z*8Z4pwTXyeNz!=m0BRfgD6l?js(&r;>A_OPvk4c3 z_bU!8hWJk<7^hq9*Kl#iEQY*N@d`B9jRvxlm^?Hz>a50^5sK?W-CAN|f%yuit<^o> zC~(cj)Ih^i0UJ{X2PcPDmL-#KIHPaz7ykA5skem<3ll^tiVfpBr=Pk7UCh&dhp{A7 zYZ-TvTxS>6v!Z9IFAe|ev9a-U=y^jd{IFkQ`JN?1CP1BmzqFdQTDYu4Fl&>Y>1R?_ z=;-JaLatv0ULPMF0porSa`*Agy}iAGeg$s7w4@|;8xS&TN8kld1BZ0FdwN1aASWxE z15%77@LvDsCs!>-=qlKu9yB%es-$m^7B%{Rg{k)s^?Oy-XF8wy2UPL!GL%~i>M*xg zEq1<9{ATbl`zf$I^x>mM+pqD;C2wzKYQwp20`m(J3Bs$(zn)4jw#l(RlaO%rqEQHr z{`3YTv^DtG!Xj%(bkxL8O>8m>b_)k%t#sHk8Cax~Q2xTk!(k=X3r`Fxp{U~bx_o3_&Z+S`zLh<&>g{|NA z*MEr8h1@GX%FDg>ORhUV^DTyK92KXjAv9gbj?Nuv!UclZi?3f#2|M>3hAK)jYx8J0 zFL`-2UAU0Hw#K89@#o-p*I>!M-0}wAjiq|zx8~Y*rqG9W2=HE9xpc>Fz2vs?4Arhg zHovQbx`&56VR#I`ipfF?Q$mN#WKCH9pDBPM`jqLF4 z4!IwUcD&Udz@mGy5|x|(K9u~iY>|=g$cQ1w6X_p+R%5?xm6vd3atc*FR#wKpVJ|IR zQuwApbVhOpcD+KDgyC)@!=9s5Ok_3ahmU zvH~OB>~WIO^}oual0YLGdMxRm>0eJ>mInnF;@+0u9mJ$2u6-K|dLlGnjjZzi{^(!7 zvXLo?+IbkfYF1qN7_TLvL!(9zDYkJUZM=e;X&EeAU9YM46I3 zm-EWcQNT-#a(;x#*hJNgW!gAI`fXn6;dK2)b#+-oTpGpOVFtzQ{Am?@u5Dp@0?(aW zmoJC^v=9G@i0GOh(nmk|Q+hD>VIQBEp_u3E(ay7+~wQMY>3G+E3#;=359t1loBKv1qCS1tk*!uUm46B zdxuBYqS-{_3|a{o3jjWXvB3ZBTVN=YQ5x2N@&6)Fqh?HDNKX0u*>&QxBaAWtPrz#n zOwVEl*v|W!n!xtv+g1jc5H#jJs`Cj!p0eIZB;|qAz|fEp?h0&c?XrM(0ow*e&z`LZ z$4O^gmtl)0)6aI=->SzBHCU1}dtiq2^km_<m8uz)!s=2w@s03f{(WBIY0&sL1MlkX6Mv|ds zbnb^j_5V|j$odU(M6@?Z{b1aRZh&Tg^Tv(ngam~dK+-9U!Y*4FwPzy0QJR}7@@OG) z{R7#^!seW}rsi*{p^y+0sa*_NRIn)6WUNapqmz+3kMJehnwsfaI_4wwO@bDUO?`u3 z%18~ghEjz5QbtH$789Gg!JVBW$as9PEPY=BbVE<>z^ytvnnHWilMM{`BmYy5h#jC(rlqX&r_>kTb*1kJx^sC-mswPDLeuE7!=S z(OueW7h0ZmTVEeUP-8YMEc&QPI5=I|1o3BM;|dPW4_|6jjsTYRlPZjjB^A;6WD=N` zMl*&B4t@KP6QEiEC26YXD?w?L1wIH556@dc(5U6F(Mrc62rljV(OQjpHF_*F4fyxLPG;G z?ORlEBhmSMY<(3$IC8T4;&cUDUNQ0Y0(oKS%Mr?Sx#-!|)y_X4WpDW~Sx?n6tDufw z+*a{I(9vbUF@W@p43+6XQr=XWy#{YAe8<&M?&vfs-1KXI``?NUU0SF8&u1nsZwz=N ziB>uJ9kpEIi@uG69MsN0#DaY~ak^AMTLg8 zwi}ce$B3SR0Y@M;HYCm`bp2S2Cyj$!)O>Vw58wobiF%F~Ycs?)poaJXdB>CAzkdU4 zreB0>Zhis91;U6=NLbt$6-5jOMBco4Rx6&@e9n#d3o|P;Eyv5EnwFD+&?lBy?VYN zQfy%K`oZdnm_oDIxNpY6>9UHIU}(XPJc7d$$dLnz8t(mkx?7mv_F7EKaCOMrE2hKk6`b6XN=<;boZF~w{qVg^r_Y_qQ7f)`9<7` z>(NCHGRqSZnoBM2G2uzD2%U9y+Iq0wm;Cp<7926H?-@NaV<;OLq)`31DM&TPXDe;4 z%!>4~y*I|c&oG1RD64X6l-9PlzwOcjeGCVOc3|KctKL_KHA^@*^LXiNAJY9tz*MYF z%*p0Acm5ifE)WPcb@k&JUvmiwWh}yq?wEyUbeM4fA8*RxSC$mg?VWesWPd`U2fjYF zpFEGRAP}?#H0tR`#KTNDv$9 z2VIz|aw@1NLdq1{Ubq_#1AhikiX;M7$W<=u1y3U?v_VHR$O@E<>7uphIh196~7l{5RKGXOk=PG2o)3a z%Tf+C@|{=ZpP1gSV_i;+y8qWf!!tQ4i7+0^+bSMe4c0dzqR7a~pxLvNX)55X6y#%N z1+vGn~pwkvF9a(za%1^zSUsWlRtV z=y}(&{u?NmcEy2lcLaRc{wSgw4i!-syPmnf(c&K}D<=WBwTlt@cKO#Y&#hKEL8F-l z@B7R0($YyBbsb->o!8TPiwFbDplMpd_>d4JGjkqTm;N3|Xn&_FsD?aJh4*!U za8sy*28|1`y!N{vI61$E=j8uF_l4PNEO01>y#o{3yLSj#F)|Q)2c$PfOD$TvD9_k8 zluftjo|nE1%BlL_jfd`RS%Ag<-*~dARJ`aE;`aP}#Hx=luA>?f>N1q|!&<$zSLEg8 zqZ=vx{QXNyOW~jwBTKze;-i<~F(dQ@y@!7uDQMR7QNo{a^ApI(#xjMGk?KHzRo}-U z7mPw~h=I-W-I)b_=I>4?ih91~d*BmxV+T54eYUCMf7@`RrEFbXDuaWSjf{+xm3x6I zm$vst0wE;t6Z0>Fc7>BjhE3NA1E+GhFA!keQKwPsvi9IEdv-Pa3?!_ zhQQ0AkjAfbPf`a%-_xf=?cgXh^*46kop2I7>WUZ8U_w8uK4(j6yX+Olf9KBC+PB@n zfuAwY@IV89|BYmO813V`db+a8@{9lQ{-_5&R}KG9q{Ekd4W)G8K1o+hv^Fvl#|evp z6K-d2Xlmk@gAFJve`lNtI{USN<>Mz|oJ^o-`WghBt_6)6uRj916_y?R~+F8R|)B6G)B`k&)m>1p#ObfFMK{l*CbR7n#uN zcmZSr1~MQpuvG@-In@WQyA>>9`#)lB@R>;GKv4u68`!0QorTdcSEsTVw1PciU|&$n zFPbw4r$YOo@K4nq1Ih#+gU4Jmb`nOr4tJz~y07m8OG~EJ|8sxyGMoy&)ZWbSZ>_mi zFFHa^ENlB(GNX&q5G640;lcyZ5YTe3^riX3i0SSwNF*OTIS5-gM0Ty10!(pm)D6FK z9M+eKi77cHg&=YoHZ&Z4NlQ(=a!EJU5}tBrdz*`$9bVKB*9Q&*-}8Efl-cNuNgFQY z9+XU^XVlJ#iz9Q-0_Vib17(mZ?Xpr*R5Ud;GtA(yu2S;l`>LwpcQ?Vz<$Q~xiP5O% z<2^X%qo(BbuT3+(*aWKDQ56O-pCH8vN52wHD^q=8U991(}}AP~|Us3R=Y z4H9k7!TCPg+Rq-WV8K zL8opBqIC58Q9k9Fla!>aq;x9)l#dz^EaBC*y(eMhf!!wj;v_B*G1%L?f4n=j95xMg ztDVbWvp765@_#W^-Z(1;S|2dJ|B2|mz!b$yjl!c-C@C)P8yp0M#$HNF z%G%mGJ{cB-LFgjtU=qIDuYQK5oFmYAz=_Jq#l^+TONU)!w{Z1?jGSB;u^KyUCc-q7 zeV70bPsMaZBLOlpb%}Z*^#?RmsPKs9L04+NGODr<>WR{!bTebvXH^GuF)e?L?}Vzv z3)5?Vk&yVY@|_tH%%YLw9y+_V_J=a-vX(XJf9#^y`eYmme@}Oz6XKXz(S+@Z2Vd59 zD%XRfq_uNrz6Nk=RE$MgQ%fSX@EnBRhR7cpYJHco9qQGcLbzm%Fcdfxh2QgPV*G@l zjw*W__xAmto1lcgD!6-gN2P~|gsoM40)p_uF~f#0C%+HNOU+5u2{Jxn_(KndHdx|q z^d?F(`W=%A#;y9iT}2V7n#MuX!+shGxirZNsSt(K0s4xfS>UARjQ zdeqSw-&DT^YVan<>k2r^JK|jqeKz(@dXeGN@FT&`PIMG>LMlQxpTw8Hp8T<(Os%*? z8m5lBl`UnGH7~u<|3qc!4&EPyzZjB+6j*ifbPuGCGX7M%@dr3%s(6`WDd1Y0jw?Ja zb420Bl>2CC)1}9kB7z8tsl+VACf5b_H;f2W!}niwL>`H(`CiYzkWc%bcD|Ded8F`D zfcoL5?gqWhNl?MzBlpdULg`77wchxji4$vF>i6 zl&dMj8{c_Gtt4&8Z|aiWd@j&&yYGp@0a+7`#^lmA`pY)LATrSzPt&88i*##=nFfk% zsuOz?b3`Kgav2sRW+CFm^p}2zRorvgxrI!FpH{NoU~^&n5TeGgP9~J(loS8xQetMG|{b`AYDQCn>vM$dkA>2eK z3-WOHllcAFQ+-xNFXIFS^`V}Ds0coJGvDW8E8t)>WHm2fsBt~~ldqo8K#HGrIhbC8 z+Iik5Sj(#PsOx~iZfM)yd&}rm3Qu4kjD$a})kjaa&&Ef{Lv}=Z=U`(*f6#NnaeYy1 z{{0>!asp@W!dO#YSWE~hEm!VsrF0CH$1wzkO z=x8rPWH#Q^;7J8g+28ifC8i=nmx(bmvy$>6h`1^F>Y3`Zr&j&Kd|UT!BI9Vpyd;W4 zHh3ahU)-3K;~f;abF4>XyzJC$Gd=q%y+%&jv=s;H9yy+yEo-E($Dp^SgJbl5|85f> zOTFJBL&v>Ta`9w1<$am;GVIgx*HeT({*ggT+4n6<>43B7oI*C6M3U(u^NcgO ze}yWqu8i&^P9^&KPGM(kCq*Z{*dfw8`c$WcN*0_X1~&wS}A1C!tsKlZQrq82eeJJBCku%Ej+JaZsjP-qotIb<*LX=81H}?+>QkE+a5e7#T>2^wqo(8*@_g(~+Cb zLQ;Gx7?)hdUBw9`I%@k{ijU{yw?H z4gS>Y+fGj=2e^9TqS&52tDh}gVY^CJZCU70*4Gq4hkBLZ*VV3ny$fLp?>I^m(nx*d zfN*4X>gQI{?iJC(>J@9M{ubh}xt`TYd{U`K0i;B(Tx zaj3klabr;dJ+0rQbMhiyrQMtQQad4dN|HorcM!$=L%-Mhx=uRm>jXP8vsUYc_8*XpNmo0) zlSmWkOuo4_vC{C3yY%LlCIaKDCzpI848akFBA(FD^zN2lwTqe#Y2c`za6Rs?ygX!P zs@zs)(aFWS>cJ}+ufOf~%+xJ2DsvUqx#XC8l%;kUkl*$p04;Xs+h@)T^NlR_qn z-;aHDiyRgmdoj)@A8YITK>4vr;|E!+C<@-?x-WfL^c8YWOG^_fL*nXH@nvpr1oSx@ ztkTCwOO>35ly$|deSn5Pbv#H-h&hOf$8_}i{cl8iQpXuxWwhm2%|`VpCQ!jNzHKf2 zlvnp|*Yn}OE){DJR<>V?bV4iNgZm)mUcpjoht;*L2PD2P>U!&Vo4QY2Fu)eumirmk zyS%sI5eDZSZGm~f9~&7^r+`93m}vc^Jli}Od>`cZ!cVyTTNx27>lJ}jwxY!MksUtt zMnu9U=+6n_?k0sFU5bb>kmZ&2KkxE+_6o2C#6Eb9QGJgr~(H?W3Oc z0E#qCox-R;=mnV1f3KulBa>VgtnOYEg5|g! zV|SjyJf{zLsGnu_%X3ecsQP3nTg}Hv9Uhm9W>koT+hSgG*>l(OuqR9^y^VdF!oGb~ zZOSTDKAdj3+y{rJX3#Rp|5;VDeJ{7Uk>i+A_6p1EzKoS4n?tn)S*r!Qd&}fyjjpx~ zzx-oOH6HUQL>{s$p-vg#B-tb>3d}e)GCVUgp~p(}c*b+{K5EiG+lr!CsnZe&H`#!= z@S2B`zKngf*;%V?J9lep)1N|Gpya#wt9?N*F z9-I}x3n<F!gH)u@e z99lnwyG(EZAphrvc{m)8%5-krWsDt-VB!%pSC_ZqUERPdvh>}Ve%SBRK2`M9@;Teq zlPfGx_fTcSRu*Mdz1n}x?84Lb6Jwiq1l9D0Dq|r{UBaiD@~a=c%WDxtK1w9C^{Bdm z)X7~zi+Si(bhg24=Tt!LfVl-|yl056j*b;DGYS&Zb6N`Nc?HVd8pb!6&z)1uGg=#4 z+z=#eaJ5D72$rY+>E!IE{O9(J4MRgt;JBYft}HB+J{`E@uJmQuD=$}DrAqk5m|^Gy z^7VU`h+)ImgjaZut>jk&u4anC`<~aVQwca?3&x*9PL0_^;EhE_$Hrv03DJ8r3X{q) zNTWqDj;jMZ|8P%NN%6ciY$-A$Y?z1~V_QeV{J7tMIUH9J8sof<{JWaP;=JoJnAuH& z2$zQW@)yv-S_*^3 zdY$g7!}i8&e|~?+TYNwXWf;aGRBnwsNI76))!|Zq1Gndw2jXuA%kE!z$53VL>iKCc zCvQlRU7P0rV(m=>v0U5kag_!nb&Cw4jLDdY%!SO6xe%3*WKL$KP-!q`ibP78hloOG zFczVVNruQ&ndkqy*n2prjZJkH}-$6D(sJFCmSS+XeVVtv4p z=f?e}f@LN_YpgGr?L5FA=+l$EUK_WEMOdGzZ1CS78oo*D>g;qk8lk{Opi@s_bDvYU z6JJoY4f<`aW8^9{VQcC4a_^!rt;vJNq3ADnbXL` zx=p`v2VB~aJEj0J-Z@8zyKRoFbt-!vdFR!n=8R#1j{z%(mEXLk0cq&5X@pOdr$Fq& z`R+_lqsq@iPmEU9o0WHyAQ|<%uDn@dOxu^XfSo~>QnoH*>N5YV&YNE!%%{8^E?w%% zKC|(e(pr+5N85UAg~_`ml6zBAqoP6!bIv=KnTAZ!H6F+M9Ot2>=|1=Af#p7dcR5?g zMCrX4)-A3#H`9_0Q{@ocy6LY6|3Wsj`SDoA4l}RrpFeL>ZaaLm@YffabtORygG&*S zzTqO`#lIxN4)RqEMReG<9rqmEG-R9@$k*QLFe`Q7M#~MUr?wO2x2>klp8KsMlURP? z7&9ouwpB|`)~wSveg)cG!#N}NTJq}Py!bFIaxI16817!B(Je5xV7ZNE5mf|fecmE@z!7KJS20LPW(BM@3n4(^Xp4a7c;wx zSL+za5;%VqGUcC?dwb+#j#_(s@Ct!Xv^ntOUJh z(#@?P>!*sAmc9NcSNyJ*zxFXxF5G2)vd_Q#yzQoXo!GICtLM*X3Hm0pYIL3cT=-IM zyyMwKh1m0DxeC;;c#c4E*00)-+F1YW#|KB37}$Q(K6A|E*gG?`c9%Yh_EWS>r={Uw zBky-eA%G6+y9~?prrC zT>$yjNc$4Hb7r@fe9m1B?EYjp_9jlRZFe1uuML;(NqW$uaEF{cHGT+0{e)9>hR03m zCdTut_i-~7Hu+a~w5|Nl;ctt7&**Ne|$sq|M(>T5qa1i5)XIwB_jtj*Xj&YxkMQuc&-GD~?EM^T)QV$+cV2 zZM-h7-5aE=wBymK9aW^kIcOjSV!cuN=A$M;}9!j+MHU zG2s(*u7vsHsFXL$pU>jsXLM*)TxSn|$5SHQjrs?;Jq?J0j>PztA+$oDKf8W?V_>lD`|iuD$B?egG2rEr%kd=$^GxI7NdVWUOa#1 zLB;sv2dxXlmJc?=mk^iZ5rtRb1ri z_A=@c$M2ADgA!8{DP-8+&zG@B%8^G*`rMAzUvJn9-kZ%YApp)StgME-U&B_RAwR|c zu1MiKm+B@lHgFalh3jU55F?ucyFCXLue8^vN2-v&{UB+~{|~lK=sojpAINjzKk(9J z5DLaV5Hp>GoO&Q}KHvDb?#Bb)4@m;dOYE{?bpyg1^88u4Cxq*Tb z9jyZgxD|lbLOrTBg+>$o_U(@zJ*tfnQ$MK+g}{+Y{7!bNo$X^x~{@$Zz36BN#GK&l-LCWJz-MJd->l50f9KKx^zCU`@&`=zB^1vH~ zfX$}}<+saKTVGg*_V@QA8>mLK*|`m0Pb6WYii?~^5|_)MnVAUx@0uC&ZcjI0y%1#p z?Qn<^dAdLDBmi11Kf)!50t~g2KW@YCt+A}^A+YSbY|B>F{%Ea9n*Kj@_aLI*_X{i$ z;mJsGBgQTcq7l}UDc3;-(N1ngD-21mT_f*qxeSSeTIDEY0fE%a+(IUYDCoBYep6BT z_2vexKdIIu2qGRxAS!p>;m_s#m)yp|Rq{s3E83?iuzmw!E;5DO>n?sHijXA-q51{R zsBMgltx#Nn2mWQD%i%hNN55Mj=^XFg`l;KttEPr;&z_W$e;RHIR#sLa-CH3xCVe;N z5Zieg>IurPGr-j=&%<~K^0%46|E<8eGlq4AdSl*f*;4YDTnr8M?o-K+NZsoX~n>hoNB&);iDrSC6Nfi@*-4jI}k2Cs$ThZf|Qt?_Jt+%J%I|Iv3qfLhp`k z_uO}9nz07|^okk%jyT~W1u+J@(7WtK=&R6ahW~1|avSBnE58hwJdXS8NFhl{Had+aq4d1rJ!&qc&3X91<#iML&390M2RV|r1BqH z_~y+E%foRLdfN@#N;fLHYHzOm%+Aj3$9QPnJ~MKK?*UZX!me+q0yA&hc7Jp2Z4GX> zwz06QeZQyK!_*UVW469>VBzXoKdGe6*AJ5`%3B{e9<$8M_vCTOXhveI$CWPUIfo*9 z{jMVKv2u)jcuRITy48-xRRt@BbA9+u;T7j}Nk6ugvuMgdL8YDd%o1$}#+@fq#A_QGojFi^)dWo~lR(=mJh2__c-VOx87+M(wC+?GH+Osv=)2qW4EUt zWuVpN{&m*eu!U2}k|dvsj1b5$gcxaZurctap^nbssfD4;(tPPs)h|#iE=a*k2Qr|- z!a{gkpvH(4IFgz@mwfZdGwWqjZ#`{kQDsB*ESn^UZ@)IbT#Jvz?=)RIPo*xrZToh` zlP@o{9|%!VEK%o$f7pa4PH7ZfRJ7@FihlZ}W~0TNx4No%zFaB}#tP0*uCokYtiD_R zP(^|4%~!%pK>lN>x&HbodwoCEHk^V=wWP$vsO=&L;cc5U1=RyILoh^h;xc;1P`3HeVjxBpq zL-*{Ikgmuei=jXWHi&$^l&&UR7^%XUC|&3n9vT)F{cGN$m-8g(U1{Vs^a>INr5)dS z54M$hs0r_X&Y-^@|6qKUMc&A`J>B*D8wyEd6?Jvms1q9c$0Quw0!-J4(h^CIi+#(@0m+do9}3_0YhOeskTGByf~-EkVJQ+`?U}GU#rOAvL%i*ViryF&QZa6 zle+Mw4Tp5B<~$(vdO*Jal4TdHnVUmXgef;pakS9M?_YytCV_S!&3aj_gvC!u&CjYf zsw~31-zM9SRC}!z8lnTu6D%QwF9T zBDRGbT4$6UTTY!h!@*S|AX1Lep$`mIc&zgQ&6;riRt%F6F?54AFGY_^iyD=7Gwt@5kR;dB)7@#W=* zpM3CTuU|~L9yYweVYLVx7g-Qn+DnoGT|tL9O>>|8biOUetwp< zQHM^Rhv^+7W0hy$y4nE~8^LjWm zu(|s6>s|Zyg?t+8DrI9~iHnHvU)+=sO$4UP{X-g(T$IYGd(+wipi-+12QyasQ$oD(UwT=2z-3l5SWaW5h*?oRlyvy>LNbii22b zi0xvu&U|~8Rc#;mYka)NSQ3<`&=2{7q)ivUl+(K1;VUbDI8=F+w?aSFfSOJAzOB)& za+cM)lUj!1K z+Ia(oFWD~&3zZZVUlW`5oA!0(1G$9N>1C`2dUoA3{TlhZ5CjWiFbL82MLu}+uG`tssi*x?i-71lmb|STtI8}a4)_X3vumryI8Uq0aJE}%Cy&jG-SgiX9 zCHZYunKt-p#jVx08<4EF!r#KK2i}e0B_O|X&oJFdAgd#<#}y}Y*spy@YhFo7yzMYT zeZQ0~pIpu$8tu{Vy^B)G_cypd?-eA)1Y! zK3OMu?RfI?HhFGuf4|O1_HX)`qs&pXp?DiQb)N3-&mA&4ePA&S#rltvM}6Qs3@eEX z9=YaaYx}QV`)~?Ia9;3q&S%hh!q>r92DvT=-*fN=Km%S;yh<0SlW*LJTh-{q2_4@} zR8&LXUTyZA8s2t8G2Dt!v9kI}iT~R9pLJ02Yv0}){`TWXuoLfSglqXlu(gJh&K-j} zJx$2Bdp|zP70>-lEq_AYMDJ2`xTNOm?rNL!a58HB30;3}{1mKD)H&D!dN2M*kGkrooVJ3{KUfz|m%LTLI4T3=|%M^Y)PA8c-~sNg^e z8AwxRy4`JyvNlfC0JC)o>+Z6V>u9|6shv5~gS5RwTA$RKw^2>FnYAk8*SU4i-(I7j zebE&fv;QD$y4~UIFDtUQb)FB$|LJZ*p2A-?p_KC#uGn`>6-R0^RNqFyrt@WGSC^cy za8`PRww|71GTjZuwYuu&o8E2Pj=(av0bZJl#vHN0=0ew)6}hPH(uz<#aMmtRTmSMV zeN~6$rA2=&<`e-?$lmdkA?PQ6w#XQOL+!@(>-P=9#r4nc-@7X;bJ$yW3T8N?nEcuk z-hc&;eBq<(4b*F{+r0ftS{^g)5fH!|J5lT*=Bdi8ARIu%>j7q9jy`SMTU~INdNn{) z+5SP7D@3!vzRS+uzRPsltE7GRk=smMUTwRWpI+rF1HIpSZsH)Ucj4d)OFYn(s_$F2 zpdc;p|M5e7g&zVk(>$`pa+1bZey%cDQke@KR;Np1A=SF!ThEFhnV$-g>bv&+$oL_b z)!$F^mLry6L*8U?2)fB0do)IrS5mU*hl<0=ldSXR_g|m8nnhk9O*5$US5ota{P!NP z?H|Ajq$15>@~8e>rJC@xOR`eGD^bg22f>T1te7RShjeSVkv}QzzIKPT?DZ}3T?pgJ ziRKo%Iy;|6E2-$?cvOXGvPrwu7xUD73sB+19oKjjCWIwRb3?{>pNKHeu|Xnt(7D2vi-%|b@sK8zI1xvMjaI&#GK7BHHt6?x z#T8>7t7j>)KYYd@VsRZ|`{he1Ph)8_bBVbsHm_f0Qe&?gOMRDNs-ROR?KKnZBc}ft zoFcFg3!Hl2HQR;k=)NOFAKx+4>UHS(tO#N=6>Rt~9HT&$sFmC|`b2Hh+fJMDLw=qw z$dmp^ZDV?4{c9N$1=_{Obd=k!A53g8v-^%isCdIAr?W)!lEG%r(I&mC8>kc4Lm)dht`1s;Xzr7o4h~4j<(FKPfEF8~XHaXgfm^Q_+nGx+ z(GK1^3$?}RY(g9lJ4uTQ%pOR*OPmAK%W;oGgfd)%UTwPMbf&xN_5*>fda*(Ytkjtn z@1molVde6$f+FH3Um2d9n&O>9;AcZ_&}RX>KvylQxaYVZY~NLW{1~Pkr>3UH{S}_F zg;bluLwCa^8B{jp-f`?@BZ3saF}k9@tPi@oV?RtclR6|jg>rb0{D`mLT5I}+Gf8J~ z?Uin84X+<@K>JTs1kBFQ=dztk>&msFk@EQSw)x&ohxa?EwComaOA^|=qMJT{M!bME zYLqJcdLNy3;4-2(wrO`6!)X0nHIJ-p{J{!Cc=HY4VG^^!O`=f^W?%qDhv3{8_|5j) z+JsEYWgMuJ;+tnXAUnxd|u(-GAePc zbr4q2kv50dQ=ZO_vgL;9ha0O~p3j#Mm>ApJ!(8WUnK4HjXIZbS#^t1YKknwkiz0-t zj9SgX$#=VPy>AIXoqj);bs4=sk9Il5pyX!zxPifLAl+!jpd2$s4YNMdN zcE{Z!Y~|sW?}U&$vvEd*nwPw2(}~{nraX*U&e)FKjJwwU)Pu9(5$0Uiz6X#`w-odL zCuIk_mBy1AP86}jFug6UpP~ZRV<2}vZnn(Hh_k4n0k8EApexuzLaaojn4<~0c+Mkbo zI68TE!!D}mXjFG;7ccsgH!|Cdg2G3*)qn=v=dQ@K^0L3rvqk!if5@+lRZx3oJABnq z^m~Iri;6)xiGP~S!^v}7S1J8j^*?`Vgrp80M4Gn%CzlSTwQ|As|E%UmSk#=8kcGoO z5-bl7$>}nQ!Ykc)RbGzI&$8tQGp}sTE7+QACI_~bOEVZ67;q!%k`6m6`0eYUNO7>b zVEA+C7%?q@CDOcgV{Y5-G905`tJ>S~F7%QYe>MSw+EeI#?uCPk%cI>#-8?=V4Iw5@ zG`vvDsOku*h2{2rO{eAe$uqBVy=bzt&xXatsAZ6UjM|4Q#cBB=Pr;7gE1MM-cq;x+ zgG9DH*gkv3=|>;@Mc{FjxLBVx-)i-s;b}gcbW)Bbz1aP!cSFGWF!FI6E-9m$cf_8E z6-qf^#x3wsXoIdQNVBjKxq9_#;qFi2v^SYax4MtY`%4}3o8^DPVdyKx8kdlj6})Kt9YTDb{wyTIVZFYIQj15MYDI5A}-m;3iGIn1(+HKdsw zcOPU-LbTea=x}WncSw-sG*FyqW2H8Sh!gLnK(T-+<}$@4`6&c!-_h|4aGqU)!ABka zRUu11eq^n;BO>S)M|3Fq%hj@8-`Dh7?$L4h+ImOD0ai5_sUS8Aj9J{a<@KT&BM}Td zh;1^frek1mz^PY7@|5|g^VX%m7Wl;urnRKB*1A9zP0bipeV6KGc1gi9ryjCRU0WJ# zM9Ff*Q3gR25uVvn4u*o%9t;dYSr0fyi8BP?Is7_L$x^=22(E#tKD0P}$8#-t-Oejo zsX9`RT$H_LhZ)Z-68{3G@9=T(+ zGeIy_tQi16JG!_`OiqpzdHNlXIHv8__ihcwawsLPF)8(lQ~QH%ZoZaZVfPm6m%TQ% z^GE?;b1z~T>3@d%*P$-aT!3uC4+HHL26?Z z_d?AJ9m&W!jY6!+ph4()IikOfqCY?iLYkE9?0x%Nh4js}wYP=|?%aPU3V#efN`@ry`XeuZ<$7VZMhxrM-KLV~;RJYq z2}|JkwkSA~{qw1Zegs3@15muXd-s>@rj-W{zIn9%)08zC9F;k(ykWV$!b3MR)6<)P znQ&u&lycfN1zHu=zcM7iiiKeX1)Bo4Yu^X2xIXiqX9JVx9!`dh_AV@!iFK*ppfUsd zdzCfQV_jX%=|YGy&|SoG8rkh6YaJlJaC?J1VdtaKXoJcQMK4AWoU*svx!VE)|}+ml~Qq0Ny(YT@Ju&^?oVGiCqrEkWBhJ70KM`cO3*b zV`db%=lh->xODVoDH-)f(&lrMEh>M3*0)|b1>Og_ZRsIwBoVfts4h9AnzlC>S9 z*dQ~tK;iOTc5~5hpKC1C%GN4bMaT5?L`M%!Y#@HrmjWQ_9J}R*rnfUOGZ(*l74hwFPzJYe2(n4%V@NA@-*SCrzY zoV6S*EFQ&l(3>m(NeI!u(0uSsTDQRe|zRGz_SXYH|t=qUoR78Yn-)VC3x%d@; zh<${L;$1xr85sjb8QV4ofcTIM&2X#hMv(7!wm}+yU(UA#DNwIhTxc$PXr6Tg`=v{l zP?y6MEaf4&)!DPf3x8^IE$c#K4j+H+X$|wvj0{2JGeF%@^+B1Q_UswNLOb;Pu~&^v z$^TOSGVHl~|Ni~gujNBjI1|dBg_;L&soz8O&=3nq2|mCuU<$WrqT zes)<;e^C%G9=QKR5R@8^>^!ZZfr``oEx;md{uZHqjXsAVK^z#v3K2JZBJb zgSZ87c#a$i_#V{aYGGNGk&l^8lI$By(Mn9n% z#K@%JYK07i(CUo-pFgXa?uL_Ncf<`@O(bm({{|~d;T_znzOX~{o#_s|_aib8h&rN1 zZJI)I&hzJIH4M#<@i-0C@78%eGvn6JC?VFN@bv3|iGOT4dD^ZNpGS5JbVi(9Wuv2` z!|!p8=yr;XL0Xr>8$FHSm7AY$DcI-K9x1t*(bdUGx}K3g&AZn2b_A+d(DR{&3U`7I!}U4Lod}!n402^9rR>yWk^=gQeuIJ2u)c$i zruIFY3gI{y`?aI!V$cE2{x=y1kuJR>HW4M-k=W-!e#X;@j3C9w+CPikl{kqGmr!u zCxK8E&M7S9w6f`Z#+m(DX^0uD@c zh4leVpPF#@_wPB5Jx3(PzfDov&7-SU&n|f>H1?po@YxqO&Cjq5VbtU48;G!Qxu}Lw zyZv0)z6$q(!I3zI<>%f&%C}?#?l^E3!wI|!6n?2@WoFtN8AWy6Mx4gERh4l*0Ce}o zix+KeC)z~2Mz(qdNv`dqWn@&f+T|5O+f=6y>+5~i^}9-v!yYt#R>~<*2u{yogIj=^ z*KZ|B1vrwL(-w{x>*~UmcPkrPmrXY&RlT4&beQ0dT8Gjnkiy{|_l>os9di2~Xt_^6 z++yg54ikg8!?)MK%>k0785bM{el1JEeQs)EVtdfroahG$`cdWpUF&l{PY|vzBKqL8K5{cn9mgc z!6Xg*qBr)$%X(K!6WVscWZ8twOfBX`SkJa7KAl3y!AvX}hi_zJ=Yfgp=zKyypH~L| zj+3(&QOCs+O=l$@xx7uJTMGPS^=tdeWNi7|OZZfTJv>uc`$tA(;OYlF_<4$?BzX;> zcCa7#1q6(#LxBEp)}eAaASxP89n#;^Q_-=ewh!Heync*W(=j$?-oD)e73GBsF!rv! zeU&>ag@vF9*xCcGV-hFnaAg&&ht<0y^k@Ka9(Enpth#1reK91BTsIzYc2-sv%)^6j z-3q6)IDlgF8QMHH2bA)?A(rVL9@bz9D9pZA8%yGSfUS?gNf>~kJqI_*sBh<27^3xS z2Ya%^tKjoS0g~is2FU~i%kmHTg$4(|e#b7~&U*z*oAc|-NlyXT1kjrHA>>)4R=>Jn ziGhpN(GQs*z8EM(U6ZKy1J~Zq`gSzZIY>ABu3k-E+IBa8$mZyM3HTR2$31;zm$d_( zAkn_YC)kh^IKqoE|h^O*Q{Q_@(Ocmzmu5?29n8`0_L} zlV_;dXHk-`Z0yzemX|Il$H2K8eh;`xM638$CvB}bBlw9f9xOmIa3a%B7N0OGuXl~x z61N|gclY)#gEinlhrEX@p zp$JH#z@fW*VPOF!0Q=c3LJJ8Sn!r@+ zYvhf)>@6&snvV&ABSdyP#GGmCxtF$_NyHeQ_eaCc3yD8l^K)y0Jph~=P-14Bcn$+X zFesKW{_`r#cG0f{kp?|w6H`+uL%+VG0pt`LQWO#vN_K~+!1F;8<|Itv9i5%6@`E@V z*z(Z8g~q&8Ec+!<8`B$ugA1+$ni(kHPV=Oj)7RZUNMsk_h48)u4hxo3+LI@kM3V>O zC(Aml0Om~*o{kL-j$oG5(r$O6UH|_AXhLE=Rk)3CGKn9h^6LR6A?&B!M!i!a_|8_* zl1qu?1(k5#C2iIYl%MrJ1L6%94O;0Ggei4(W6VbjTf!tpY6jt5@E3t*F+jqV{`~ri zo;(NcBaa(>EKggA=mF~e4bi6sTkEZ$prqZ?DEtw4@tY)-oK(p_&j`(bAvE~2;Lesf-w-_UvfrE64VY`%E93mjy<@k2Lt8Y zeqA;`P7K^v8xhV6xOcH(o!s@yuu9uASwKilxxq zEMnhE3jOnGhhh2&F|Qd1l9?q-;SH~7;Zpy?NQDgY{RIjx=Pqwr{l#+zNxvw)z%)E! zzmcg^JgR-S>u3jW#D{;~xx*t{ZvsIb*~*zV%x=f6vG03NkH7j*e?QRQvdGFvRdfay z>94f!#z+SMa+x3MpdKPqkkgJPAAgD}x|qetsvyWikNAaWR5*QHey))K% z#IJC5kq4NWL`l_$54?iot(d(m-U7p%RNz79e(m@U^&K!tPYg$y#VT0FvayryJ;&ve zhsQ~2+g6tJjPeuJER=Pye&Qe^{TSb~z@qi?U+^8veSST2oB9kNzekAKa;P56H(g>D$Wpr#@*@b4Z12JHD@a zka^_DlLrlEm!A>V23diwsWxw#E%D_gu8m4|{ZcHg@z4<8_a@%FBl~l3B0O zcBVKopIET&IeS5l^~8<4%1c{)3VSm`#pA-lu#S7ZJGJgvOd#81F$p*>@EcSKUjI3La>vtGiu% z0IYQ~vf+nJ$8Vdg2Aaw4_ph29pPXU+qMyTi>A}*64az|pi$7IHON;WzTbF$fRci9E@fA|l?nZLZjQTWGc zi=T$3A~l2O6bPIzPO6mbK~4q(W4B-CuuOrW8AjFR znjHj;R2jm7qx#eq**0haI^#CraKj-U76e*I3OE>7esjK;^YnuK+t!Zo#F#%{?kZnw z=&!s=H~29pbqykF!ue@!U*Baw(vG@IQw600CttnjoAoz7Me$f9_+jWWd?$jJLmG5E zE?#_o<3<%>#)j#7)s>DwH#*s!xg_Au-P$_4nn;DOdrBlyj8x*n!YUdXk0yu}RaV~r zP?TU28V`3UC_T~ZfvLVC`!NRxVPk0A?kLM&C_H{oh?zEhXq4iJFOkzXxzHeh66~Rt zyPPqwB|=DowH~^)^NuiHLxoX09~``OPH@}@;0LCD)h3%{5bz!vSm^&c$GLU9aGchYyXW>Q1CNrlqMxo>cR)h=E!O^i5WAY3P=((^#3=RDDi-=e^U@}DCy_?ceA|?=e z{pzl$3Yay})fW??d6|Z~hM#`?ED%h@rrih=sPuHVZ#r0Ge zd4kSMcof9B+N%%<4u69F4FRBfHZ^mJ+07aN0a#qa&5bQBMi}J;pjFSn089wX3rROC zE$N%Roez6fbP~`Zs=-0a$w^cauvswQN$7?}{eyi+_oVswc|hP+zlQ9@uZ`gLbyyL; zc5~ms>V-)P#=xMDki0e;BOcpZT0pss(X6VeVV3d0(Bs{-?CE{87p+0hb~@|nIgbhi zScTdnFJzB{@N2;so6kEAAZ7Rht zeMD7CHMgIW;Wjy0UP5XX26c#ub>^MZy_2EJ|Jya9Pf@!FAhj#jGk-DLzEM{KN!`$&{GXcC3 z7MGR%@;CDKz%`SF^F_LQ{9kFXHlLtu5zEr@R08X+G6om2%k+7~@LUYY!FJBe%VP+W z0*~<}mxQeB*UCy28?hUTA{Zsd%p8mMw{AazVT@qeKaIJSgcYSdGEaLNE}7@Kxu}IP z=G6yJ0(&hGy1?Te)&2NZ`fdG$xekTS>9zd-uips#WfpK~%KzozwLJ0UI!T z4X@xI=5)TkADYtuBdUSyh5j!_V=C@d%Z5j)Vb()Gg~}#qD?2ey7ZqD_$$BE}k*$96 z_*8vwuLX`j)Kh?gLfdtjhamRi=u^!LlH zWsq_ECGX8$^Ue4ncw)PD$#ar00?}-A^+$+3%$iemFJ}1r5G0J&VnhdWa~qx8PKK*& zvc$tkIZNyh`8Bj(iOE_%Ssl{h`w(HGL4{17N?w^O>bH6K%70xWzjE~zSgZO zq+AW&>n1g+Bh^O}SO0`8;a132qLR|4y=hZz_;^GCf0PtL2v?xNlaJVSbj7y-yXwj@ z=7jTwCQgRwhZTr;4L0n)wMhr$Bgdzid7Sxzv}uCPH%sCVSNcvX+i>+mI>S4v*)LL2 zubd)CgE!r3BQsV19QG<~*03pE&rNs>!Vemiw_c}EUtU_tDgXMv;@%^~eGwNUq9A_W z%12*~KOFXGc1--}Uq8S_L4eFg>VgQ7^N|gFBr4x2ZR!k!Kjh-tp;Vcj{58s}zv(hP zp4&re;E>78Vt?n8(xksvt?f0^qOWc}w+#$qMb1`ZI1OEGFT9yR4@BdZbc399Ya7G! zHT4gOjJ1A|YbKD*r@paqzBf!uG=mA=d>ayOY;O)nFb?v#NA0-cyHFSW4Vle`zX_Ts zmoefH(RtsH1Dg=$lK0nG;EF1SOFeibtxEs2ytwzH-9mJ{!XVu^3BqhZz;b{n#=ypc z)kr`J5oB#|dWx!h%g_IF0kedf11d)&y9i@vkT(G>ee|rBT`7Z{AOvYl17Pe>LgkE^ zOyS0`{8Lk-yfKEXinq9^2+n7{uqvyy0SRoP6ZIXIy8RfG0mLb4CDj(w4~>ms@N!B5 zIqVa44uX;MMiY<}5(c%SNBbpz18a(!#)TU$@K5gKk0aC3Cv9~&BNeFj5FiiQw9t@CoHTHsRn?`+Q(LwmO9;@GRiOtQ&$g?1)dWu(JArRAdfO ze6=s~ar!NwNa%_&r@N`5LZPpD#asDy&u~R8N8$3}zxS6*1UcrcHZ?Zt^3xZ@s2)Gw zihSD4W00#~Ht@e@EAfk`G52YuVLfVS=Di+}DRsR8W$J^JAEJyayIEd5sznhB7cl3B>qwgcjG{S&WD;p_(*}88;(n3WYE=L? zRnGpa^u?6blfiGpG-MSQ^v}t#X8jMakJ;`A_RnYAe*ZHgT%fPOfJi8C*7J-y0D^0 zMfn(Po+pGF{W4W)*DZ}j%1IplgvP4pYvg~hGaDI3dk$bzVoanZ4A{-hg^@`>j(LpI z^+KWiJpAyk?#bMdL?XhCWbuK|-TsDF#VN|I?LkrJuIc9l!d-MTo2)dH2jGRdc!6>2 zyFA}@%5V>Z4G<~^V~A)+hcw#rPoLgc z{QNoHn8=ATGsi|peFi^qb*HTuB5nE@*6`Dpi-G3dA%qXNNf(O8rQO|Uq@|aX`RV!G zX}DsCu?*(s=0MwrKfQP%fh~&mTT@dLG@(XDMmwe5f{$-UD|<`KN-H~z@PcFnjrB^0 ziXZOxQjlvQH91kX%E+!w#C01+k3L;uPL7Cxzy&y>mLDg+nry)_psR^^wMRG(`ThPx zM)BBC{&gy{HvkWMe^@akqzhhm#lOm_>`ND?;6#quNHr&ik|%I0X3*Yc7QgfLUu?@K zB9IeOs%OdD-CF5hoA!>4X`MW3U+D;^YXS|iyW*egM~*DFe$zUlR#Wi%?MJ@cn#9EX zWp1uSqM#v7r*)5jJ3sO@T^S%7#(!5XRNJTnwr8xLJl27 z=gHVr>U-9^QTXwptrT@n?S3`86uW;aImVF zrM#x*d|!3Ma14&RMv=T7bX8vGL0#>LSXoadvik6E);sI&$aEx<_``MiOP9 zgz%}2+K12q#p$l+xDPHmIh`KM{HKj7kE`IHK5DcQNQGxt@MR#?7{qtR-u^*c+#*!m z)XWF62FnY7uM9alb;=QQRRM9s)~&xx;>yaluCL%t^ATB$yiGiaXy_l#s5ky;z?vUD zefpKzzQxcWBN1_xcil!pJCRsAEb08?j_`rc0$@-jySjR7XiQkv&7C*xWIFM4*!IvE zZ{J8kV7$zH!J2h*QL)VNl47x~_lDH-l?l|OtTyuu;$_U_%OUCQUTgjViv|wVBqq{&*@+D!gGq;9OI2% z#co7d6B z`UrgyLjYiDQ&UjjA3rwrl9%_8?_w%f@^<3YzXoX_r1<{%QmguS=~t<_vMy}>cP z#74n=>NKfzru;{k);n%4&D+-*1Sak6tgNV9Sw>yb4jtksJXpV@YFG4v5__Y;7pr_? z`jNMi-G`2~n4WOLI5wF74_z!NlHYyg*QwHvat|<9+-USMAD^NBwHNMgn_*i!Q@UKN zIQ3G>Rpa&@)`~O`(_(L({S=c%Zr>t&heS6!GI8Wt`FfunPk)B<3dKYX>SUy#e{^hVaD4n0BDyqf&<>`HEG<5M@9qlRaO@et^3_>LQ|$}86_#EI^M<^s z`g&Sjy=u$&cw~*Knh1zLFEy6X@lMb9Xq#Q+A;#!@;lkzuZ0pv{%DUq})f`T=trZt< zH!e6lYf$3rdyp$9QZM-=UUYlDX8ghNv#n!~ACVthvSUZf5SGpz_r^hs7M0OxY-ULtzP zuUynwlVk1XS|tHbYxkqhD$fOV*asS)Et2-bBQ1P6nY@s5Y4%dZQSk zwGI^(Dw9;d};eIczj6e=xqc$j3X|7L%05716d6j8!?POKB`!y?|44;yB~|dQSJ6@=kvGxw@h4;N#jy-Oc4K3MQG${8UzuKWeqJ7fRI=OFOL#)_P zro~0ue8WsZr%MHWNBMcolb2_;FgsJ|fcX2{JI@J-?O-Xct)o7d^yo?K0nKk==A%LR zp5vA&ulpbERZG|~W0^D480&h>32;G1tZ+3$lZyVbe>h0m`mHPsPk;$=-$4ffIr zM?BZ@7;n^CGhtanKW1Ozt~?c}&gm#_$M5POe5U^zZSc3rq4!6Q+_AhLYwxT>$ECgQ zuQy1cw{8^_Z9#+mPL-H#rnM-ai$t+WQa(zMkBqd6|6++$)oz1~JEKQx&$iZec+ zi$3#ly2gW1y_2loOJ2Lg3a~VsZeKFaS$vvsK5>DGPR#)W7n71W#FhBzOD8_5gKbKC zFg1((W2`thqyHg|0#gbn%44;2)-(1m*iTr2!;h|7cfOCSYrZ0TD_}Fs(S)ltn6Iv5 zU+aAO{(<@)gXgLwtPm)anIuly&EXKt8SYVCE}%@m()DY)mOMf#LzX=VBy;^Rxq z-`{!K&~PRy+0oHH>wduz@Y&>nSX%5!a%D4NI$;=g2ghqX#_ z`Amdt413`{AD*&@rqv@Q&4iBhy}x@OT7KQjOVgw)+~`tH@on3dRP`*Hjf)gry+nSZ zwWFgRrz0<_lbF2ZMKLu!{XpzIy?CF!?|l3Bw?WZV1rP+D-KCJ8l7b8WrG%<3h%I9^ibk@fvb#n zhqJF=hXz!xvhF8+mYhi1yiX-@R33Tg9sX4I>(_F$^sJIBpNxkKw_$^d-&r=zqy_u0 zn=d*DOZm!bqaX7n^Y^%Cv}Y-KSHA_H+Kig8?#I7xIf|tpa#!!i2@DuUhNrppPvMQ;}b=4r`h)#6j|QhF~~w!)#$t{n(;xz(Wq}TcYVEHKjz`oIFZvU zHr11?KEB5a4J9PU^@c4jQ%k0P9ij(kmlwuj!$a(ybLhBC);Wz)+zIkb_4Vc0zyGRI z(8&unC-{tM@(y}5rbZsrmfYjn`V=SgthdeY|I*Hqwhxi3f2`IpmvjQ$^8eD! za@S%W5C)WZnH_yaFTkHG5I~YN1-;wi4>?~G>bmN=t&K9@lD9SNUck(Ng@veTz!}Ry z(xY9aLWX{F3%?YgBLfZAw&TS^Un$4fPt<+->j=pgY*RLKw2OM)_67}Vw=Mc%r19{hR?UN+kd1_MEt zMJKAFygX_eSRT~oD9Y0B)vq+PFajD8-MzlBjjNz$usQOS5l%=^=56nPCux`w)d&zH5cQyW z(A$-ZTpLXc@X2y{@k&=2t`>(MJ(n)sG6LRdhAs4km>~~;lpEn6Q8AmEn4o#L#~XfH z8o!8Rl14gtV zU%8Jl@BOk%9|sDL z9b-k>5@C9LbY!bjMVE-ITH9RNMnNK#DDKlX+4+_mKefEYXnJZyn44DVfQ)O}y0CKc zGEI&9r>(xcf1_ysIj!__b9zX`w=SPpIm=rz7Y@7jQ8T~uo*Q&?{rST5<>N9F^f{PL zksrB0)|WN@L5T6L^XdZ8sjS@OoEHT6f-H3@sW{^9^K*v2u=;qKWoPZPX9iUn{Cs7r zcWWP}wM|TJ<48;9#GK!G-{nh3FHZivEBnXjMSYy6#?ko3j-Br<6DuDV8u|&DZDABs zQ)EtmdV_*y`9ZpIG47dH_e`SyM80%#cNe{ok?ojn^lA<}_?x8YNi^zK)<`}wbdpm` z;m1t^??0c9IZv9=e-dxpV0G3Plkr}2X>elj=#LNU;Y#3nE32A#w7)7NFYl2y@nlZ7 z%HSPQ)2OZ8J+@eo;^G7>>@%z{Hkwc@p*$YFwyEQ3D6g-xv%>lFv^iE+BK}i(@r(5^ zon+!E15RnsyF0(X-|_@(l!o0esoB2|7sc#jEQa|N^|fEfROZ)r6oqA)lQk@FJBmiI4&*CD|0AUBPQ4w>(b_)_UG~B ziYm5>RfG8qQWt-2j|K07pwIGl^AnGpaHE62 z=5DF$&eb@!mO@bQ4iZl>kxy|F4ZqV1c0S^a5_KHV9&bn*nK7_?FH^Q=jc*6-LVxVl z7EixE5TjxrE8_7JaTV-)!blrnt_oh7!nWZ24j9WjB^<`y!X%U(lOw#J| zFLyPhfSKPoFlVWp-GC%uUx!&0yJ-Z4gt~IhQWbx!tTc}Vq1&YNZzU9G=bNqtMhivni_jW#%l0ahOW@sURyicI#9U>A%4GSbre2#-CO z4`|H&e~rC&JeF@CKWtnnA*DjestZX*NV1Z>qcSowq9`=1>`{^kp^OG4D}?MsQL@WO zSs^PTNh<?~}gY`@VnA^Ljmxf0B!H9OrQ!$7j3;05bJGjEb5pyNzBv`}Q9!_{oX> z{>r8Rc_QRQ^w~$ryyu={pMiYbfhz|HHe@1|(T)+N>PdB{0!O~b(_a>V2{v?6< zoA=7)RaK?8TZv!R-^e2=BXdUy$|66|(?zb3+--Ddg=13=M(A>1;~J2bXl%qOLn_G& z5blRhAfW@zs{7gE5)y#+RT>7Uzq%e8DhE)HgbmsWreoTgnn|K=gRg-^KwCiw_^^=E zbt@b8M^mC0={CnZ(o6jLY*xmc0u zPD~7T>VJ2?=QSEoD9HVP?cxd<62eK%$^gd7$dkydEfnHmWgWHi-;#+8jL3J@&fQMW z%4%zEjq;RM($>E2?|;zC>sbyd%KI?og$s*!9*;taX8OiW3t{1G1dmU8FSxke6sTAM zeRu`J#AFFtFV{|53iGJbnuji&b_RNV908Wf@Q4lKDqKvN*ld8+I zvaqpXN%w%x5|BXvw4=!VbL@kr`nmrLY_8^1!nAGUM#U@f|CS`riG9`(ari*=U~c(} z3|vv`dg_vK^lH9(6mRqU|JtxnGKlCaG^1a)cCDy{1ezOAtr@4Pq6+1`hGS zlT@--dDX>;VG<~z(T66X&e zJ<`MAvx*}W^3x#L5xcH~MgO5QW4E`ibU;`BW@T5yFIY06lg1y|c1Ya&vE*`(9E>WI z@R}rCduE5M7dKCzBKQv!ww&m>ELXtMT|9?7@N?7v*z)ca)qnlMG}i@z5ChXi=098h zy<+(tE3l50!zQjri^(;-X+1jBxaE~wG<|&^nOxdx;zTunF)Al}z~&_~$A*T`UqdKM zA6=KvSU?G21AsrWfk2}RnG`nk^z^)VVF6wTpx8;9PByP}f*B_22tcwUD&37K)@V!C zK{-h1*-cEsr9ega%e5&KGpOvEAm3 zegHaeIy9z6MpqpvzA8FUTpylj?LB*ID2aFgeiD5H17a}a3k?kgiA%Jm;LL}`Ki_-} zO~ru~8=RPUoR#JM`3<`zEeFK9x|Pv&0$weNszw9utw%tCIBaSPD$hV)pT*-G*J~u$ zm+!l6yzfjLD#~zW!O<-Js2$?U|;mhWLD3`c(eA@Fff4&09ay| z3%`2RHi6{i2C=?df?vR!%u?%M#%gYQVdCHA9P0{%c7!}_JSO`pM(Bx?EH~Z7nMU|Q zsd>`=;>8OrcCKoZsOs#qc;N>|1%gXsBcqze%Qr0$JA<8x4$YGmEYSJ-0U&z-p5g;> zS@>M(nwoB7V(K3s2L*B`PsJ<9y_mZXZFZ*K#;eCB-#R-y;l4am5*@rCmIU|^GLcG9 z^vlppMx*2c&y4Vtg|ZWR8x04t)c!IfSC?l&^?V-hE;OSM>Vno^o}NAmck3>41-cy2 zI7KAC_SSWRHoUTY2ayzs7r_K@CQje4lEk}5-E{JvjJv`vt<#d=v4ETpsEC31s>g-H zgM$&KPlV>7`viF)fhQ#Zl6SPXqpPt~r~(Z-Y&$xdngeT}%6FhUH8e1A!qW17a&lG8 zcdX#t75_DW6c=nA9eGQDMzTi+t}kwwX~gZa*}%l)_W6E1oxI?2vuCF01OrHtoJi1l zA}L!_K<%)JQX}Ig06A@V`}VA@Z6YV3J+cd|q#$*`U~k>=j&=V%`fcPb-WU0x_A<{R zP?d~!Gcz~7eyt)PkO4;h`IxXRV!T2^kK43G3%^zPS5$q*;RCYq3v|@usQQ|)M zu4~N^D4iW-R$?KEuvmGS{SFW`DJNNg0E17U9S0aYdisC`7;kO_B^~e^P!%o^5}__w z&r!O0!sY?>u|;6hVa~eeHwnw^;(YItqkh1F*};t^G`md;|DV_$g@F6SO7(ZuS!8?| zi*$W`uo7}GEZm3&1O)`n=!>IeV*QPx+;>S%@Xt_;xpmQ0w(p-aE9|@6!a`n#GWQ?6 zr91cUKlY{Yn){F8Jt6RWd&pe%`gNhP1n(ihdRm+#FC2D)W4D^fx+tt-g0(CMdVGbG zZ~xgz1fu^ei3*zx4(y*7+4vJ2SS&-ti%l$}D+_9q_pJDs(Q+pH&!>JIuMRP`U=7t} zq+rVI=f-bF)6yK@7A{}3Ty);Ea%B38v!e&+Y8M2E=M^eMN37`w83CG-EPAqrSQDFV zu#WE#!CUvA&-W!y9sBw~(|mc)e!822!h%FYY#T8AkRjd}l^A&6hQ0ur5PSOH&p*cO z9bJ8n2m8X`BaR#!4B`AB{@$%V;R>;x&=a>MLPc0CvJ!g-jwvWu8~8!On&Y4>lIJTH z#O6Tnv0giJxZ33=vG%@Rj}@8KAHNI@DBPe3)uoC0vFfC!V@J*l62Z>$PbpL^xMEPf z|F1jPL-m{M%irzAj2Y)u>AP2zdaU|aujWBwt8_lLyb*0EKGjUNQ~95ttR6kP&xgE@ zs8tiAg`tUf32e{XnO^OGPkAVz(Qo;pi9fSyP8OaZf?+&H zMc|u=5&O@5KIKOpXE`$bdvmx(j#nK)aq2}&{vLQ4{*KqB2^wPObPM7x&duGh7t;j{ zM}AYG5@0a7Djq4Jun87FqQ}aD8&OeW&Lg@P2`trC`D#ElV`Cz(62G>KS42bvazDiq zUf$j)f*x@`2!R7}3ZgAP!8Q zAu`|$>BB6SN(!3W+a*D71@ba+2~XxBrSsa}ZrN~8n0FWnMm%M0$YO{@Pxq{|+<#7S z<(x20UV)ym@o7iLdw1_%e?tga(=_qRJkEGFIj>F3DFc>bmP;L-Fe?uW4OM!J`&6SI z**#Si?$$G#9JFAxoV3x5S6QS;079o80x+Pi7%mbUw0uCR;PMADGBUr$J1(8ZWg!_+ zLfb(QU=EDGQJIAh%@K$3-sioIjSQQi-;O~nu`hK`Ny+6P-ncKToc6??zO)FW(*p$@ zb(xCzdkqyeb-Dzwow}Qs&vdtbD6$kdp^5#^Sr}itIMFWBHJ!^jbH51S75GfAJ9cPF zl;d;aflCuRDn2@}G}cuv((0Po%0Z=#0L6yVy#Dw9><`M*Kec9_Am$W(j_-q~6!DCS zaUSb|gg!8w{D63|UMvQGp<4dq|Nf=M%O}5-k2mU+A344PZky+OM3*>A5ud*Le97R% zz*7xkG$-ysn8Fi#cf&k3Ga%+!=lQOPO}mP7%8U8mqwPnG+*SL3gVgpOr8|23;x*}{YQS`Hn)vJ^*Ep{@v*TN-QD-_|F5{2ijor4&CUTEj9nRd zBBGQAmQ_;-qLTvjuH-`=;s;vF+1CY)a{Tx$K~MOrZCDb>(!(6KPgV(-v*xbc7)%86 z%yZ+!tSq$W&7mJR$=6W4kh#&|g36v^RAd5*5fuk>g zVq>h~a}M$ca~G#a^g8>&pT7Kqo{=$C@#4vo!%#*}V?D}Nxx@w0NFqP%20+zh(2Q+k zJ8?a#f3L^i@%0_^jett&VbSj95RCngvY@U39dMSs9&<(nSZ|sq9`*yvL4d59U3jE$rfB&AFlVee?b4SkG*nsYGefWU( z+Zz_suG6mLHp^SYWJ2|~%>oAbk5&=zxqE=m)i>(y+SswFB>*5ce!=xMQOyC*p2=7W zM*MH+fdFRWW9e_mVc8VO7caCQ@C*Y^EUD;(F!jgahh0mOalWgFJxux-9oDt~t)kvd zTSes#;swwx1#^u30Rccr-??*OY2)-#m-NQOU61p9eKVh&eYuK6I@&GB6Q-18A#$E_ z^CcbmBx~_kT)Wm#Sw-ld&`_sW=a;e)9}eLkNL0CgQk*lARhwZ0OE>=zV?UDwi?!bR zRg2ji8Lgw9r6rlaFU-oe+3YR5=lNz-?od%`uM-$nb5VAb2fjC^aDLz;vlg%yup4d} zxh9jAG2!^qI#EtBKw6OwydcnKO0s>^%=U?ibrqa31Y!m@#xUbOFgKD^XzM?{XLNl0 z$bFDiLcDh!H8($NXSW3t45)C}DkCk?p(6#y$gw;=q$YA&`n2?8Q3_pjjU)PEjCGBS z)EeQ8yn)3HD}gp4f&v4c^XF+n!bF6V#H_aM2L>3V1(amaXEe>R@Hc8ogWqdmXteYZW)xMc zb+ssd5{cL2L)I+v-zhg8DXLgnI{a9!_@k_^G_eX%cC!8bWMA^cu|IjQX$Glr#Qc4K zkLYOqqK1S;-u7(7%JDvie51?Tc<7sg9EIAs&lQaAq^2I{qVdS^@RZ4x5yu$X)6&-n z9qNfkOfpDbmfZaxKXyd@9_2sG<#!vHcC+>tDDw|KC}RR*No>!aV5BMb_DY_YhD(w% zyj3;N#borPeJ!r@t4sHqDwbaI@&dua@erGZ!#SiI1_rlEO5AO2ZB_52nUOEzz|g)k zg6>`?oA}$ireAMHTU^gx`TNuE@7qOA`f9l??ppT~7_CK#7e-H=)1#s`o|)=}|0@Vh zP`L7NcmHlOCumX#%Bu_*yIo^<-Fm>Rl4{&-S{zYk0j6L)FvLg{OY#;N!x6Bfl>b(4 zJ`*Ac@4hf@6(0dx++HHkq{tCPm83kUj7=QY@Ldp%3mv@DwVR8}0z}+mx%1Lz3Oj#P zU)+(l=rmB2D-iI7sZHon4|)-xXH79RB&5#aVmh>oop&@_d&L} zxf4)7jL`^^%6DnLu)SvDH!XD)On-q5Y6cg3xka(R9I7ORjS#N2t48|pY3|k6%Tp;{PfJUzJL%w{ePx$m1)3xI9l*%%5)golkAi|i=kcwg>Au9NOn^)pT`9qs1Z!P}!60>})MwfR(L+hK}og=b1qTJ744TD!60V4@Wx^uby7$pBsO}l$q11sX6&TIiy(s;hTI< z8=K@<60ZN2L@`*?dHi;EKMmX&ST=EvF!dQwH=Tw`{)oUH%BciOOiI)bu$Mz1oapPS z5dOj1!cRq{_U{k8d>LN5Z67{BEOumclpq`&4}hSH;`8r7i`S(_=qNoFL0%iLH_T-% z23H9rX~_M&MW)a2(2$GLPXh-`w$<|b?_@Kc|C(gX z-Jo5reb6Sa?MKQm3cz~^L5lRou;5@zO=y1tB#P1lo}v&;L#QfZ z`X(pOq1iSeR*F;}JP{0=KIm|pH#_t^n@tLrJ1r52rZ?vvt5<%M)a||`&210i(+3$X zbMqxQm{(%a<(T1z1O+%T1&o24y5X6%-2(AFg#PL4z*tVbwbkX=&-yloZ4yPE05q zNdiiYgmhx*Dj|I(!TWU$1H;hb4kM|+cfT;7e~*3mhQ3wH>NvRnU}BDq(F&qmc;xFS zx1fkfQ)OjkMTP&PsO)TE^8m0~(L${~GioU;B^5u2;NwGN^v1Ox$I#u=sqa;ARc7KbphV zHa5_v|IvFmBahkn^M8JgNQ{{>QzW4e5EqyH@S*c&!I3vfGY<;gO16geQ_J&g(vLCs zKF2nT$X@>X8GSZ#Y;%^%cV&lfjs@%#O;V!Y?i?_3UOVf@NU(;OGP#MrC5UkpMeLCE zm=AE=D|y6?ql%?qmEsLbis(jK%FA-c~|`kL@*Y2zJ8FdbGig;HH1Ed@@9{Qa4Ho^{WD^P^^AUc`s=PeVv< zqk3N86#@eO927^qaqnE;xl+iR)a;zeN_;|^_3K(kE(Sz~KQ}kG#Jlw4_6-`B^S*NR zcz%<;pbAdNW4UW*eSC<3n4O)S=cv(8kr+eT58~2Lz@>E*7}N*Z$-ca}_#$YIfc9ha z$bL3e2V3%b2UjFZ2-(4oK-mT@kOVw#_CO27bpR%3Ez-guC%Vtod8=uH#&(Ho+4*ZT zWJ;qme=ubnJ^s0QD`jE_5d)}HoUK_+Lu}>Jm%(+w7AfQ>ub@y_=+Ff_@*@yRToHpu z)$W3WtBMMZgZc5}<bLQI&|m zP}V(_H8ocw??y&e{#*@5iGc@h2kE3Jhg84cG_-6xX*l-f+4QcP@7yum7_7ecNNDe% zi(N0Aot{1mIX=Lbv83&IUik2#lBVX^{I};U%O_H&7vsGsz$>A941DAJ_dlVkXg3R; z8mr|(WQPz|VdulSF|AEhH(xHFNB}1qN_->*|5^D<$9#Q!hoY*9`8^?zKP^I!elZf| zrre?hA<%3|FzEA58@{dy`DJWCIcr}yslR9ve3bQ*R~ zc2-u%GHuQS^vw4YKW|uUesAX^xyD6$YXYjArI2Ks zolTc0HP-vgW00QnMls-^$=dTSt*5nOJI_4WIyBN>W7XhBnn8J{n;Ux1d?2M&2BlFB z3#)5rY^OG0wF?m@PP#XkUhFjs)u?}64ef{@GBG4LJH+}VPJHA(`qL-(`b*w=>nDgz zm&VMOJfN=j`AkON(vY+Q0gHcDRtDAQivU*KG&!m{_DnS6=;6=c8Zd>bdy_AInM;36 zZH58?@lLwMQT2epia@REy^cHOl3pnN43?gj>S)&XOm*wnJ%2Zl-f?-GZ3t&VAs6%m zYK6$-%+%w`6Df4)qRE|Ti?)dppcq&V(Ticxnu}nl$m(IR|h17V^H%&L>V9d zC@bskjT;pPS;z8Jwop4pZo{=Ti<}^-v(9q54P9k8dh708P`AAn7u-dw;7Eb>+_=gW z@env8U#~GW=)poLNtIEkC%~wfg88Zi1O`#cfECGtTaihjJr<8o&vkr|M?w%$M+~)H zYvFrPPpcUu1n)0$H@bZ3e@^Q&HAu_9dr0gi%P8c7nV(0a_qv{y)%MI8O=T@PKF$IQ zfS;glmEVCqw(YZPir3v3qvUli@D}2#h<&FKVCS)SvW>qrC4a(q?|F=UhF@YL*W(cK z{t1vEA+?acnNRsdLavxv6{)N*y<=sDvrfV2jF}mhM(^>yj+thkOB9vT@$i@fiwk3K z=MHgmIwco$DTMYJU8&qKfcD*S@1Ob-!puT`PYe-~dd2+U(=ox8FTdY)ux3NyoeQx~ zj`lf#%SqOEww~bSX=b;AToBxWwlgN}ICB07dtzk2M%=@5NB{<}MyoLuBc^a#ecwzi z9zg*(pNe?WDUyzk~Wb3H@D)TAV* zPnE0eXj9m0JqRZg$D4qa7xD0%K&;;V2og382?r9&P9>ADk88X-tULQvi~5TUVZpp7 z{XVI6lR5i=niK2y?c#Hkj=Q3_BMh{?Ge)~th%+il< z#53O{?F}HEusV{kZ~o%rNhP^8)U=3bDP}%F6dMH)HY0{B!2Tcn@BcyFA5FKeWqL zs-j;_U-()8$E0pou8N%&jc4^bg(?72>JB>3^oi;rPw}X7kl|D(XeJoeCyPmd#;0ro z(LaxY(2n)Jz|_Eaq>222-HM>SJTg4)9@xvSaG#V^m{fS_u|P$(kVjj^$Ag+C0^_Gy z5rA|v)28st2zS3}Ch|*%>|7%JqKW6P7);01Loe@wt1B>o^!v8rrSv<=BPk%DX=vz% zMwh8u;M|;tre-}74xqBYe}4)KJRYOePZfvN@#G5H@Qw5h z3{<}K#=tl%33%_BL@wQnQsO1fnBEq-X% z{Bf(L(wF1Uska+se}+zHnr87PYgw7B<;~p~{B?ez$ZOc4?Sn>9&|~|E@hsuq$O9=} zR<9zn1}QrjITt4;E5uf_b+NJ;vFaA8e!RAI=Doc43GXAGt?6v37fhZCJD>WzH(zx8 zK(g=5&vS*QpM)b1JyP=6$q>CiT3J@hbNbhpk6}z&tu5Wj2Ja18PSBx}+j(cpH_Sd=3xmHkGBwFA$1hk0D>+U^?YhgMacPYF zq0lYJmf6$|#07yIDZlYAJ85}Q%iYGp64XyEc(d>7!kVc^X(qxhbw3o}`t?&EONeI-FQN81)0+={aE_0*gdZ^-jzx6e0A|$y{h1ry^~q~Ys;MU2Ojyz z{n7fWU9A)r=V|L$IY1iXF|s%RwV&&cSA71x)dTJv3e``-lSP>_MLOy@b=Nv6lNyVW@&X}qvZe1U}tn;#X0`v?+3ztOdybRRHAn6AM zV#II|z%^AB&%OVx$LmKU+SYD>DxroAoDC#api&2ouOf)sfBpJ(pJR6odmO_%76@)d z)j|-;%F3$vpuk}DVbJ;q}^USwsytYD_A)O(-Y5&<>hm{6vmj z3XHo9g}8N69Mc}%V}&p6YnRkYZ$qC}Y3*;D=gObX8&6wqi?JVK+$l+=K8d+L^*kWb zVZvG`eB%w}8|pvRGqxIxj*LFbTVfCH?&H&!lD+%G-JIK72Fvb=0jRB455%6t)Nli zRU9&)eLZpDrmBejrzs9{apDn5WQ?5Y9qjm(V-ZB^gBP$FR0f=O&?O$r`lk!E7ZU9C?Aj`6gRW z^2DxFUQPapT1lPe~?8Wk<>c?+6;~&3xd_q;VqddpGCsYyB!gnLkr( zlCWY4I-h@HGQIESW`SO#KQv1(C9_GwbSxps?wW>jru#n)&_;)1H6?X+w8qV!9J@5m zs6rEN>gBVi<5z!o)!;;{#Gxodp+LUK_Lz5hLB|-E%od|*F&pw18pC^{4`g!fVX8Hl z^3wq2!aMSQ@~7hV^WXP=*Ax=sPCdo6`+EpU{iYftfyJYxX$lZZFx$Pj=l8@dMGQ_w zu^ZR}+FDwJi`(S9Bx{9EJq=+N>^ESC)@T^`^&RAWR}$K7eUXp>t_Eg0s7cV=v&x+$ z251di#pQQ#(h#X>g?RLJCYcv*6d$N#Z*M=V)N*oZVKiZUshiJRv=(ppWBxT)9cfK^&OO8+#J|&hs}}yVT+Qc}jI6g*$NhWv zIqV)AK458eu6Q`9)H1KrC9W-Dq+*w%pI?9N$>yn`&19AB9U2_E>a;0#$A_QAs5%uU ze%^?kFVf=gYE59U5KJ;V&yHKY90z!W_Le5SH{Y6R6{b7U(PZ8VgaOP^7K;bTD)T#r zlGz9P`>!ytsr1(u>J{sbbB!0BYmOh{iFFAYX`oT<{X5C*y;TFgAS`BEVPJ9 z0Zl}oW3(9nCegLl1pZxLzYTzrSrJi zFp7eH@g5NoR)|L)P>Qd-|IL_Ofi48(aLFAgBSUr4bhns0t>a+K-X|H@(uvOth=h{8_j`00I{;vYXfD6x%` zGrXS~_X-ii z%n7+vE`45=|7g z()OzLWZv(_leepS!g(}@fAw?Tdb5|`rls!M&HU0E1snJaEN;iP#NMdbF}l0CESgX!f@!4Zf7A)Dj*^3YG`AT&(RA}`?FpV)4xa7x}8a>brFykvx*uG&q zLrm5cO_7n7z7_=sEMI2E0gc-lojpf{q>8SZsQt+28RgeTI7Rwn*HImrWfq$kaZ6bE zxXuq(N7uEbn_4Z+&8Bz#KGpP{OG?4iW?N&>@pbHd(l3loHmuV;CmgwoN#g}wUv0`MRhroN%7aVp3Vaf;k(yb@0 z&P*B4oUym8Od`oDmer2&g~T-hg|7Q9ArEiR{sg0?%-e7)XYtO=nwqrIHZ5IY- zmuZ)iUV9a_0?d6_e@_ktLCY*(CS9(!spXvMc=1g93=d08dr1esa78qk+KJkhhj`!g zTyVYlw@C+n_X$|OmUx}5{A{|m0W znyzv$D&7lH%M;(mCVOlCq9d`S1-TSX>im4a?>+Er0hXe~Y6gUd&dol9L*9J9*uO8X z$t@UZ@FR$D%dn;bY!~$&edw zpG?)Bh(G2$WzKgkyGd)JOhHhV-F6G_WwlGVXcTyDx5iAcG%JZEn9E$8e!oB7eL6p^ zS4h78?^24RY_0re^Feg2Pg0!uVsW(l=VwZ#6oM~Fxb)=%IO`i328j*LqBqc@rRU;~ zQ`mH_!YTf_&P0LlnW(lTyNf&htoZsO^IZyy8GGFs0h4$Ca~iXk-uq0Y8lv}&7iBrv zvn1NYu51vvf}fks=VGE`Ts+(F#T@&cS#I_}$Ctlee8ef7|Ez_ zB%M$oq{^1Z(Bp<@_GIVF1GXpI;yyl)F*`GNMmNv(EDObm^||Yc1hnOTw_emW zyz7~rGpa0a&fI?gSs~NG#((}aKV654-uF@A#Qe{K-SPZsyDcvPA!e~M$J3(Z#X`O{ zw?6GN-psxzfI2k(IgS)9)u|cA75tC+I1@kegog z&ku!bqM;$4H!}F#$XSep8(F^EL>;Qe-Q}SHy>FsFw>JEGrbR*>`ahq@F~9bY{@pcJ zjR)*vL0y8WyZjp9m}&ic@}wAa6n$&z1()cfP7fR=9O6sF;~S`Cxp#$DCvNR6=&P zO)ihi+b=h2u`q5$SwKr4mHdp1G3YVrnV5v^>e5Ga1jCZ!NY>C~J1^&770Mf|C8Eb>`*Sgs?qw6+-ZbSCM zg`4CQ7cW$bQHn!2)FM2^T_GRWzvDYYg9Z<+!3@u~v8{T?P4<7%rRtLWeVki#Jt2X# zb^A7}U77bv&QEmj*JzL3b3*E9z~{!>s;BPz&e8#kt3$_k=w7BVEiZI-q+t9W?t4st>KSnXH2iW@Dsxd$Rpl3V<$M)7lKC$NX2`jPZ5TeKO3$qhcK6vmVzZ` zUglAt$OH4Aj+&aZCsQ^owWw>MpJYsUFiLvOh}`@DF9-N&Av8~}GSodL`bk#-kGpsX z>u-%!T-Jf-=CyB#^=SFJy*3-X?7p}#lUq_U3pW>7Nxp)p-Qq4Lx0Zu&+krYJqufki zR#A^t4}ak^NQ!sBar&r-hm^?^pdozz%-wi%foTDz5hEkEsE`AnfXMfxjm_TMv977m zQ-x01w~VN z-%Clgb7M!6cfE6ny#mSN6lAD~KOq=+qt^Q8PhSk zg|_FZi-5a`2{hjGAaPtyf}hPDrYP`AKqUwN1Zb2^%*+~mDVHSOrv`CVaO?dFB_f@S zsFsosVRzM80h~y_WA9mataA+tr_fbJc~%A{W;@8mZL}$y2!GS=gcT6W{jF`NB_6Lj z)ubxnb!bMwbH}a4Uqjf)91U*nNZUd|k*$N0=)=!TwG%*@eg2$!rDTs|x8yJJS}DHq zkEJn!z8H0X|BB~Jf6o5;j7g7-v_`t>7TWV@QJ`sviIWmUi4jHBTPFaIzScv%uEean zyE}F|f9|1!f6GQr&a(~<1kNtw5~=9KsYurtithY+j;iU6{vVKon2Z&*C~bhHuO ziPniK!{a7#?w=Q`KZ`Cb3ESAJu>R5fkxwe)qM(=*P&?;F78ZUB8)rbgaf?S@cGtXp z3#LJd%NXZpbTJj9B=Bl-J#wc=vFiwiY1HNch6&&^4cjeM-XWnOY2 zFbrBz;sZ@f@!YA3hAo%ImUn8(n1wd;5j)ytgnX(hQTdZAygjTx8cD!iuo_-rh5} zz{-Ng=w&M=H1;5dbhvoOZ{CaGZJ`XbFxdcIzc-LO;_2*E_YNgkBw66ySy;=I zSqtMFB~?P{&KKlj(-YZ_CArXxI(U`=2>Rhy4bmk&Yp7^%nLmC;f<<}cbY)dlpv%)C z@i6u{;R>=MY>Xn+v_20qZSIBFP5RE!-Y_-;#VxeACbTL6+ugbidSH_lw>p9SQB-`1 zZl|2b^wpzs@Mvx*SD&c*0>w_-7|hq^lOR&3iq64ZLNW##OxxWaDyr{HQJ@|ER---% z@EHsM!?*3Pl*mX==ROGjV8kN#)Y$-@mfZ;obwZ*a0YKa7MABdimX@{4X#^en&^(8f zbDB$)>VzhZ7KfI^xVs3mHmF5DtFRoQ$NV~gz8iwW4o;U|Rla%Sh%(SZAn0>D1Z_Q< zsr`+;13z+a5;8m<5KKu;14{wXybaDl?IlipMfC#N&njJlY(J`ZZD>&i*$j$!aBfal zmM*Nf4QO=b-2gq?zm1xBVH9O9^*(bylEcu;&+p(o2zXpMyAI%)PRD+UZ@(qMTX9rq z@CHhJNa78OcRW8N(zu$2=85|*XE8Ccz}c5e+!%bFJg&fKvuN6YP9bm7)LE$=w|oBu z6utk&W`@47?SpO0fJm=ToH+G^<4f0y9u8y`BIr^h7 zU-AxdU0U>BEV8w#k85m7p!yjTV<8v7I50Avs&n^ITAC58qEYGzj&hwExL#0zxp*Pu zN}KKc@*UcFd@sMlTxSivHo;%V6Us_pJVc;5i;7a&?Pz87YBWVcHfQ~y!VGdHoYT(< zm7=Q@oPVD4bxCn?(=pIWHciyT^x>f)4bM3=g0?L1AjOTZe6Gp|B)9HX`_yz_aW%tM zafs|qO$j~H|JiPX2y+27a^_X0s^Y+7^xD~H7g+{8f%6ZMeLJE|n zRqQTJTV3RwXH}Jyc6v=mhN)B0Iu;bxxyc70VCAmZNnVAS{o&0UAvw9@mZ`N9QEXU@ z4BD=qx!ZufK>ZXjP05<3?wbt`2i#VYii(JD-7wN;e;Y*TmV`T`ANNz~nAdl{eJi|s zcc>uQUqoCSKAYaD>LRUMPusv(RKBD_R#JUoy-&bx?YzoOEiM|zrSXn4&pz(h0o{XK z2>BeXgR@L=b9w}T9UdaX;lr)hV#%oRY3e!AI?{dJEKlRSp2+G9gc(&FozZaLrFII$ z{&k#k-7VfGQt=jf-1WY@3U9PKYYtw)5{(QoT7R5KI2|jg#gr3Rgu-yjgux!%e1*YqPakZ>R za2YMc2(`4ezkbN()c*^A)G(YP%t7PhpGbx~(93M!BcmHaRg#m?di=Z5OxzBM!VSs( zhdEeT)!u#q%y_zQS92T(AgOM3wu4t*6Ba%ZtD~BC{n$jlU+EBSu^qU=%aL2jsQjKT zK_xRz`Y>HIidq*eY;D!lQT()a@U^u31m#o^LW`Ll0o5H}lYh+CcIO9pGZ$b--C zzR`J7->W$uRKsVqy^)EZT)=amI}dS(PM&JwHiP+&@FAhgYHZZuYV?fn9>@!gf6h0Q zVfGomdfh?EuIc^zp4=0v`udxBA~{5B=kss-&&^lb?K z_`P?9sn-N0Yi_viD=*9uB%CTc#6iv=G9akX+8$m=;=IkBlaG^Bol2_fbfn>4AUQ z?SEtS&A(24ZDD$bruOMyaFN3+X8iPQ46F8ftetZ9P#(f%)~V&Jn6Z=`0P>UkR1r3a z^}Xt(kKD!nbv)$QJKw+v1|j*5XyKm8tFI>I1sMh_m+Txut6R|RR`}Ge5@A8K%s)Pk z=(IVS^Zq>ep&oEr^zm|amAG40Ts#8>fV0oOt9kZ)K`zTbsy=C{(=j%q1 zXP|KW&ha{jY&?Tp_gYVbrDhVPqw{HHb@kx#a=E3Ssj(zJG8uX*KxfHS&@Fw=m*ogSG#+>Y zCagq{#jRT5^n98{7J-={S6H30m#|n*mqM5=2UAEfZEzbuTy$n>KZQ+U2Eqmey}O|pkm8? z(yCS8=h?2Ri{fpGW19&Bt_|D2!d@Qf9wV*5 z$BuZ08!GWS`wlzmh8L1JZg6m@&B_ZcB((mzxI19I-P&r2m1X{8QADTcE~Byh(0k(_ zvP}32{qspq70wT-N7R%CZ^ZP0$A9=t#Vw#qGR;a2L`0rcrb#h<_v_qQF+Bze8KjwN z!O!6diA{8@rw;-ze%A_I?l(Q{whf@zC*_g^Jn)1}{HBA-wJ&xMc{%O-^_v}kcNfEO zlKltL%(Hh%KQ4`4UDb3WtY(KW5$)&yfrxv0$@bioln^wwR!%2WK>*N{QDL`t;f<2Y z6K>yBzWOwCDDTzxYbYq|R&x*q1ht$iY3E8mzsAa5rBbc05-?lg{EBKsO%*p{5r5yD zean^-z>dw$dwF~O)>-MfkozeM%mN@Bvw}L~SI4rl{gZ*5r+PIU?9lBmgVCFh`3-}M zB9Axat5Ap-Z@NfAp#a(9gUYY>*l@LiU)FK2VSV+jgojx^wZrJZ6q6V9(&My-`};3I zE}@(hqTF`l>T2?zjbw7{^DXX^SPlQSRwObT&T<2HB!M^pC@`e#eXcaRUKGnG5ELRe z-A@X*k*w~;%EvCEHc1;;Ng>tvdJeZ@^}>%5@6-6#^hD{)UdvOe<$%9%1=H>CTlQbD z2IOFx(bDYV{@@QYU0ePfXJfnVni<#fa_`fYLk8<37)$A0G!73(^`<0iTD5J3Bh`=Bh-a7Ce!EV58#nnYr(rFRdME z;W6{6{FJbe(DA3v{5Nl)gDv}D^7$Z95|EdtknMs8s-FK;b@k~|Qhm_S*$qbp`73rU zoL-x4Ek$XguwBDG$+}PZr+a>D&I7%u&-%#V@2Y+dT?=lm3RN@Uzd(WlSv^JjSnQJX z3$6-Tv)z6=N1SqfqGSHfDY6gM(i?%oEwc1zT%j4<-P9Dpln@(RQ#Qy<7~V2hW_Ij( zAYS`GLsK(-cX6&|x_2iceh{w!eun5iFmv9ZV!b&+c#X^>zTC9~^`cHKZE|d)6f+CU zYLsk?Vn}pf$Yi5h`X3d(2T5H@ExDw+PCqS*pe^_3AuPd@JZqErCwlfizI26bV zM?#dnLCiifQ^reXeSnaNlh$DN$1VU%*R4(paN$<-Qul?|gYh1UCp1Ryz`Q&8_MYJL zXDKPe5CTA2CLLX-G@0xXk;xbNxOu1?QJu^yz3DgQm_HWS8dZ) zrnc|>wy?5ZE!)bs_dH~QDSvJ%EC52HJK^0q=&|R_LTF&hOyara%PUB(pZvOsx9EO4 z&jYzu1>@vL4_+mI6jGDn{jRNPHN$~-nV}+SkU|O>BiL`y)zZ_~S5Ae1%MP0upOXWv z!Du4{V2Gs{Z)sV-oHOMk8X?gYP zvax6|iw+TaEI#~o)5pxlhUd&uni*azM~@8^m*$sjd|vWzh&SF&FlfZqdusJ-X?o0X z09tdUPwxT+yF2Z0kNUo{8R2Lwn)~==??j05Wr5}>2?+9?%aZRg*Tk1 zV#qu^Hz+k3@2NU`1qpw%hR7KW_yvFgQO~%w@vLOBA~AzcX2%@WPbtptqRXu&zsY|X z^zD#p4ClQ_I%p6NzvWSP#^tt`IU~wC{p>Gwbu5k0iHhuB?l0>!UlMi#-oM* z4D51a`7lT5t;3DniUi~pF!y6Q>wZ36qknXtghS^$2n*yE96A2tPu!h5S~On$uV_b4 z)VT#E(%hVP!W2Sq53fL|r1dcaa||S3uKSSUksnENsmaOBdD?CQa@UnUonSg;-11h9 zik6uzKsAoi`Qd@k0kYBTr^|zfG&F&v22BVL4vY=R0)U%eyg`1|DKc$}Dtc}LzOEIN zCg0Q22aA<8T!ZBOU?@M~vxVX4HGh9fJ5#>Z6b4QqyZHH=KYaMysoLbU)B zXV=AlE;Ya_O10o^2D$}!1((!yb=Q9ojNRr2iW72B>&NhIkotWfkHjC?cR!$)OxI~F zl>$F$q&xZfpE%EZ@072)4TRN-r(vbF=b9I zZ$KCkeHGxTu{STIUEV3zbUs{NUR!s09k!sb(0c^osfvb%-8?+2s#ZU3JHfO<^j5w< zBVXjHMMVrKMb6Gma8E~Xi-LkVupgZ&U?V7)(GkaYz!bcAKyi%`-=fiGvcSi9Y(fA|2mK&k?7nid9MI4CuB2Gwr5m&qXg>x-`{XM|{ h-{1THJdv}^66XfCgU9XjUMdkMP*(;SOu-`H{{XOQ;Wz*Q literal 0 HcmV?d00001 From 4ed720a903e44f7cd18e3c37b37ba9073464ed80 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Fri, 11 Oct 2019 12:44:49 -0700 Subject: [PATCH 10/13] Key derivation: consistent terminology --- .../key-derivation/key-derivation.py | 50 +++++++-------- .../_img-sources/key-derivation-secp256k1.uxf | 4 +- .../transaction-malleability.md | 4 +- .../accounts/cryptographic-keys.md | 60 ++++++++++-------- .../key-generation-methods/wallet_propose.md | 7 +- .../api-conventions/serialization.md | 4 +- img/key-derivation-secp256k1.png | Bin 61282 -> 61189 bytes 7 files changed, 67 insertions(+), 62 deletions(-) diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key-derivation.py index 5526ad4118..de9d394c20 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key-derivation.py @@ -61,9 +61,9 @@ class Seed: """ self.correct_rfc1751 = correct_rfc1751 # Keys are lazy-derived later - self._secp256k1_pri = None + self._secp256k1_sec = None self._secp256k1_pub = None - self._ed25519_pri = None + self._ed25519_sec = None self._ed25519_pub = None if in_string is None: @@ -143,14 +143,14 @@ class Seed: return RFC1751.key_to_english(buf) @property - def ed25519_private_key(self): + def ed25519_secret_key(self): """ - Returns a 32-byte Ed25519 private key (bytes). + Returns a 32-byte Ed25519 secret key (bytes). Saves the calculation for later calls. """ - if self._ed25519_pri is None: - self._ed25519_pri = sha512half(self.bytes) - return self._ed25519_pri + if self._ed25519_sec is None: + self._ed25519_sec = sha512half(self.bytes) + return self._ed25519_sec @property def ed25519_public_key(self): @@ -160,17 +160,17 @@ class Seed: """ if self._ed25519_pub is None: self._ed25519_pub = (ED_PREFIX + - ed25519.publickey(self.ed25519_private_key)) + ed25519.publickey(self.ed25519_secret_key)) return self._ed25519_pub @property - def secp256k1_private_key(self): + def secp256k1_secret_key(self): """ - 32-byte secp256k1 private key (bytes) + 32-byte secp256k1 secret key (bytes) """ - if self._secp256k1_pri is None: + if self._secp256k1_sec is None: self.derive_secp256k1_master_keys() - return self._secp256k1_pri + return self._secp256k1_sec @property def secp256k1_public_key(self): @@ -198,19 +198,19 @@ class Seed: Saves the values to the object for later reference. """ - root_pri_i = secp256k1_private_key_from(self.bytes) - root_pub_point = keys.get_public_key(root_pri_i, curve.secp256k1) + 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_private_key_from(root_pub_b+fam_b) + inter_pk_i = secp256k1_secret_key_from(root_pub_b+fam_b) inter_pub_point = keys.get_public_key(inter_pk_i, curve.secp256k1) - # Private keys are ints, so just add them mod the secp256k1 modulus - master_pri_i = (root_pri_i + inter_pk_i) % SECP_MODULUS + # Secret keys are ints, so just add them mod the secp256k1 group order + master_sec_i = (root_sec_i + inter_pk_i) % SECP_MODULUS # Public keys are points, so the fastecdsa lib handles adding them master_pub_point = root_pub_point + inter_pub_point - self._secp256k1_pri = master_pri_i.to_bytes(32, byteorder="big", signed=False) + 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 @@ -243,13 +243,13 @@ class Seed: return base58.b58encode_check(prefix + self.ed25519_public_key).decode() -def secp256k1_private_key_from(seed): +def secp256k1_secret_key_from(seed): """ - Calculate a valid secp256k1 private key by hashing a seed value; + 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 private key as a 32-byte integer. + Returns a secret key as a 32-byte integer. """ seq = 0 while True: @@ -304,10 +304,10 @@ if __name__ == "__main__": Seed (hex): {hex} Seed (true RFC-1751): {rfc1751_true} Seed (rippled RFC-1751): {rfc1751_rippled} - Ed25519 Private Key (hex): {ed25519_secret} + Ed25519 Secret Key (hex): {ed25519_secret} Ed25519 Public Key (hex): {ed25519_public} Ed25519 Public Key (base58 - Account): {ed25519_pub_base58} - secp256k1 Private Key (hex): {secp256k1_secret} + 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} @@ -316,9 +316,9 @@ if __name__ == "__main__": hex=seed.encode_hex(), rfc1751_true=seed.encode_rfc1751(correct_rfc1751=True), rfc1751_rippled=seed.encode_rfc1751(correct_rfc1751=False), - ed25519_secret=seed.ed25519_private_key.hex().upper(), + ed25519_secret=seed.ed25519_secret_key.hex().upper(), ed25519_public=seed.ed25519_public_key.hex().upper(), - secp256k1_secret=seed.secp256k1_private_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( diff --git a/content/_img-sources/key-derivation-secp256k1.uxf b/content/_img-sources/key-derivation-secp256k1.uxf index 8d0c64fd27..d8eaca4bf5 100644 --- a/content/_img-sources/key-derivation-secp256k1.uxf +++ b/content/_img-sources/key-derivation-secp256k1.uxf @@ -372,8 +372,8 @@ compressed) 160 40 - Add, Mod -Curve Modulus + Add, Modulo +Group Order type=sender diff --git a/content/concepts/consensus-network/transaction-malleability.md b/content/concepts/consensus-network/transaction-malleability.md index aaf6a4d328..860cdba071 100644 --- a/content/concepts/consensus-network/transaction-malleability.md +++ b/content/concepts/consensus-network/transaction-malleability.md @@ -33,11 +33,11 @@ To be "canonical", signatures created with the ECDSA algorithm and secp256k1 cur - The signature must be properly [DER-encoded data](https://en.wikipedia.org/wiki/X.690#DER_encoding). - The signature must not have any padding bytes outside the DER-encoded data. -- The signature's component integers must not be negative, and they must not be larger than the secp256k1 modulus. +- The signature's component integers must not be negative, and they must not be larger than the secp256k1 group order. Generally speaking, any standard ECDSA implementation handles these requirements automatically. However, with secp256k1, those requirements are insufficient to prevent malleability. Thus, the XRP Ledger has a concept of "fully canonical" signatures which do not have the same problem. -An ECDSA signature consists of two integers, called R and S. The secp256k1 modulus, called N, is a constant value for all secp256k1 signatures. Specifically, N is the value `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141`. For any given signature `(R,S)`, the signature `(R, N-S)` (that is, using N minus S in place of S) is also valid. +An ECDSA signature consists of two integers, called R and S. The secp256k1 _group order_, called N, is a constant value for all secp256k1 signatures. Specifically, N is the value `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141`. For any given signature `(R,S)`, the signature `(R, N-S)` (that is, using N minus S in place of S) is also valid. Thus, to have _fully_ canonical signatures, one must choose which of the two possibilities is preferred and declare the other to be invalid. The creators of the XRP Ledger decided arbitrarily to prefer the _smaller_ of the two possible values, `S` or `N-S`. A transaction is considered _fully canonical_ if it uses the preferred (smaller) value of `S`, and follows all the normal rules for being canonical. diff --git a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md index a1e936d396..5d054fa8aa 100644 --- a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md +++ b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md @@ -1,10 +1,10 @@ # Cryptographic Keys -In the XRP Ledger, a digital signature proves that a transaction is authorized to do a specific set of actions. Only signed transactions can be submitted to the network and included in a validated ledger. +In the XRP Ledger, a digital signature proves that a [transaction](transaction-basics.html) is authorized to do a specific set of actions. Only signed transactions can be submitted to the network and included in a validated ledger. Every digital signature is based on a cryptographic key pair associated with the transaction's sending account. A key pair may be generated using any of the XRP Ledger's supported [cryptographic signing algorithms](#signing-algorithms). A key pair can be used as [master key pair](#master-key-pair), [regular key pair](#regular-key-pair) or a member of a [signer list](multi-signing.html), regardless of what algorithm was used to generate it. -**Warning:** It is important to maintain proper security over your private keys. Digital signatures are the only way of verifying to the XRP Ledger that you are authorized to send a transaction, and there is no privileged administrator who can undo or reverse any transaction that has been applied to the ledger. If someone else knows the private key of your XRP Ledger account, that person can create digital signatures to authorize any transaction the same as you could. +**Warning:** It is important to maintain proper security over your secret keys. Digital signatures are the only way of verifying to the XRP Ledger that you are authorized to send a transaction, and there is no privileged administrator who can undo or reverse any transaction that has been applied to the ledger. If someone else knows the secret key of your XRP Ledger account, that person can create digital signatures to authorize any transaction the same as you could. ## Generating Keys @@ -26,15 +26,21 @@ You generate a key pair using the [`wallet_propose`](wallet_propose.html) method } ``` -The response contains a key pair (a private key and a public key, in various formats) as well as an `account_id`. +The response contains a key pair (a seed and a public key, in various formats) as well as an `account_id`. -**Private Key** +**Seed** -The `master_key`, `master_seed`, and `master_seed_hex` are the private key in various formats, all of which can be used to sign transactions. Despite being prefixed with `master_`, these keys are not necessarily the master keys for an account. In this context, the `master_` prefix refers more to the keys' role as private keys. The `master_seed` is the master seed from which all other information about this account is derived. +A _seed_ value is a compact value that is used to [derive](#key-derivation) the actual secret key (and public key) for an account. The `master_key`, `master_seed`, and `master_seed_hex` all represent the same seed value, in various formats. Any of these formats can be used to [sign transactions](transaction-basics.html#signing-and-submitting-transactions) in the [`rippled` APIs](rippled-api.html) and some [other XRPL software](software-ecosystem.html). Despite being prefixed with `master_`, the keys this seed represents are not necessarily the master keys for an account; you can use a key pair as a regular key or a member of a multi-signing list as well. + +Because the seed value is the basis for all the other information of an account, you must protect it very carefully. Anyone who has knows an address's seed value effectively has full control over that address. + +**Secret Key** + +The `wallet_propose` response does not explicitly list the secret key value, also called a _private key_. Software that can sign transactions is expected to [derive the secret key](#key-derivation) from the seed value. **Public Key** -The `public_key` and `public_key_hex` are the public key in various formats, with the `public_key_hex` being the public key corresponding to the private key that signed the transaction. Both the `public_key` and `public_key_hex` are directly derived from the `master_seed`. +The `public_key` and `public_key_hex` both represent the same public key value. The public key is derived from the secret key as part of key derivation. The public key makes it possible to verify the authenticity of a transaction signature, but not to create more signatures. **account_id** @@ -55,7 +61,7 @@ The field `key_type` indicates what [cryptographic signing algorithm](#signing-a ## Master Key Pair -The master key pair is composed of a private key and a public key. In addition to being able to sign all transactions that a regular key pair can, the master key pair's private key is the only key that can be used to perform the following actions: +The master key pair is composed of a secret key and a public key. In addition to being able to sign all transactions that a regular key pair can, the master key pair's secret key is the only key that can be used to perform the following actions: * [Disable the master public key](accountset.html). @@ -63,20 +69,20 @@ The master key pair is composed of a private key and a public key. In addition t * Send a cost-0 [key reset transaction](transaction-cost.html#key-reset-transaction). -The master key pair for an account is generated in the same [`wallet_propose`](wallet_propose.html) response as the `account_id` of the account the master key pair is authorized to sign transactions for. Because the master key pair is generated in the same response, it is [intrinsically related](accounts.html#address-encoding) to the `account_id`, which is derived from the `public_key_hex`. +The master key pair for an account is generated in the same [`wallet_propose`](wallet_propose.html) response as the `account_id` of the account the master key pair is authorized to sign transactions for. Because the master key pair is generated in the same response, it is [intrinsically related](accounts.html#address-encoding) to the address, which is derived from the public key. -This is as opposed to a regular key pair, which is also generated using the `wallet_propose` method, but which must be explicitly assigned as a regular key pair to an account. Because a regular key pair is explicitly assigned, it is not intrinsically related to the `account_id` of the account it is authorized to sign transactions for. For more information, see [Regular Key Pair](#regular-key-pair). +This is as opposed to a regular key pair, which is also generated using the `wallet_propose` method, but which must be explicitly assigned as a regular key pair to an account. Because a regular key pair is explicitly assigned, it is not intrinsically related to the address of the account it is authorized to sign transactions for. For more information, see [Regular Key Pair](#regular-key-pair). -**Caution:** A master key pair cannot be changed, but it can be disabled. This means that if your master private key is compromised, rather than change it, you must [disable it](accountset.html). +**Caution:** A master key pair cannot be changed, but it can be disabled. This means that if your master seed or secret key is compromised, rather than change it, you must [disable it](accountset.html). Because a master key pair cannot be changed and can only disabled in the event of a compromise, this is a compelling reason to keep your master key pair offline and set up a regular key pair to sign transactions from your account instead. -Keeping your master key pair offline means not putting your master private key somewhere malicious actors can get access to it. For example, this can mean keeping it on an air-gapped machine that never connects to the internet, on a piece of paper stored in a safe, or in general, not within reach of a computer program that interacts with the internet at large. Ideally, a master key pair is used only on the most trusted of devices and for emergencies only, such as to change a regular key pair in the event of a possible or actual compromise. +Keeping your master key pair offline means not putting your master secret key anywhere that malicious actors can get access to it. For example, this can mean keeping it on an air-gapped machine that never connects to the internet, on a piece of paper stored in a safe, or in general, not within reach of a computer program that interacts with the internet at large. Ideally, a master key pair is used only on the most trusted of devices and for emergencies only, such as to change a regular key pair in the event of a possible or actual compromise. ## Regular Key Pair -The XRP Ledger allows an account to authorize a secondary key pair, called a _regular key pair_, to sign future transactions, while keeping your master key pair offline. If the private key of a regular key pair is compromised, you can remove or replace it without changing the rest of your account and re-establishing its relationships to other accounts. You can also rotate a regular key pair proactively. (Neither of those things is possible for the master key pair of an account, which is intrinsically linked to the account's address.) +The XRP Ledger allows an account to authorize a secondary key pair, called a _regular key pair_, to sign future transactions, while keeping your master key pair offline. If the seed or secret key of a regular key pair is compromised, you can remove or replace the key pair without changing the rest of your account. This saves the trouble of re-establishing the account's settings and relationships to other accounts. You can also rotate a regular key pair proactively. (Neither of those things is possible for the master key pair of an account, which is intrinsically linked to the account's address.) You generate a key pair to use as a regular key pair using the [`wallet_propose`](wallet_propose.html) method. However, unlike with a [master key pair](#master-key-pair), which is generated alongside and intrinsically related to the `account_id` of an account it supports, you must explicitly create the relationship between a regular key pair and the account you want it to sign transactions for. You use the [`SetRegularKey`](setregularkey.html) method to assign a regular key pair to an account. @@ -84,19 +90,19 @@ For a tutorial on assigning a regular key pair, see [Assign a Regular Key Pair]( After you assign a regular key pair to an account, the account has two key pairs associated with it: -* A master key pair that is intrinsically related to the account's `account_id` and which you keep offline. +* A master key pair that is intrinsically related to the account's `account_id` and which you should keep offline. * A regular key pair that you've explicitly assigned to the account and which you use to sign transactions for the account. You can assign one regular key pair to an account and use it to sign all transactions, except for the ones reserved for the [master key pair](#master-key-pair). -You can remove or change a regular key pair at any time. This means that if a regular private key is compromised (but the master private key is not), you can regain control of your account by simply removing or changing the regular key pair. +You can remove or change a regular key pair at any time. This means that if a regular secret key is compromised (but the master secret key is not), you can regain control of your account by simply removing or changing the regular key pair. For a tutorial on changing or removing a regular key pair, see [Assign a Regular Key Pair](assign-a-regular-key-pair.html). ## Signing Algorithms -Cryptographic key pairs are always tied to a specific signing algorithm, which defines the mathematical relationships between the private key and the public key. Cryptographic signing algorithms have the property that, given the current state of cryptographic techniques, it is "easy" to use a private key to calculate a matching public key, but it is effectively impossible to compute a matching private key by starting from a public key. +Cryptographic key pairs are always tied to a specific signing algorithm, which defines the mathematical relationships between the secret key and the public key. Cryptographic signing algorithms have the property that, given the current state of cryptographic techniques, it is "easy" to use a secret key to calculate a matching public key, but it is effectively impossible to compute a matching secret key by starting from a public key. The XRP Ledger supports the following cryptographic signing algorithms: @@ -134,11 +140,11 @@ The key derivation processes described here are implemented in multiple places a ### Ed25519 Key Derivation [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/protocol/impl/SecretKey.cpp#L203 "Source") -[![Passphrase → Seed → Private Key → Prefix + Public Key](img/key-derivation-ed25519.png)](img/key-derivation-ed25519.png) +[![Passphrase → Seed → Secret Key → Prefix + Public Key](img/key-derivation-ed25519.png)](img/key-derivation-ed25519.png) -1. Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte private key. +1. Calculate the [SHA-512Half][] of the seed value. The result is the 32-byte secret key. - **Tip:** All 32-byte numbers are valid Ed25519 private keys. However, only numbers that are chosen randomly enough are secure enough to be used as private keys. + **Tip:** All 32-byte numbers are valid Ed25519 secret keys. However, only numbers that are chosen randomly enough are secure enough to be used as secret keys. 2. To calculate an Ed25519 public key, use the standard public key derivation for [Ed25519](https://ed25519.cr.yp.to/software.html) to derive the 32-byte public key. @@ -159,7 +165,7 @@ The key derivation processes described here are implemented in multiple places a Key derivation for secp256k1 XRP Ledger account keys involves more steps than Ed25519 key derivation for a couple reasons: -- Not all 32-byte numbers are valid secp256k1 private keys. +- Not all 32-byte numbers are valid secp256k1 secret keys. - The XRP Ledger's reference implementation has an unused, incomplete framework for deriving a family of key pairs from a single seed value. The steps to derive the XRP Ledger's secp256k1 account key pair from a seed value are as follows: @@ -172,11 +178,11 @@ The steps to derive the XRP Ledger's secp256k1 account key pair from a seed valu 2. Calculate the [SHA-512Half][] of the concatenated (seed+root sequence) value. - 3. If the result is not a valid secp265k1 private key, increment the root sequence by 1 and start over. [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/crypto/impl/GenerateDeterministicKey.cpp#L103 "Source") + 3. If the result is not a valid secp265k1 secret key, increment the root sequence by 1 and start over. [[Source]](https://github.com/ripple/rippled/blob/fc7ecd672a3b9748bfea52ce65996e324553c05f/src/ripple/crypto/impl/GenerateDeterministicKey.cpp#L103 "Source") - A valid secp256k1 key must not be zero, and it must be numerically less than the _secp256k1 modulus_ (also called the "group order"). The secp256k1 modulus is the constant value `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141`. + A valid secp256k1 key must not be zero, and it must be numerically less than the _secp256k1 group order_. The secp256k1 group order is the constant value `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141`. - 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the root public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) + 4. With a valid secp256k1 secret key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the root public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) **Tip:** Validators use this root key pair. If you are calculating a validator's key pair, you can stop here. To distinguish between these two different types of public keys, the [base58][] serialization for validator public keys uses the prefix `0x1c`. @@ -197,18 +203,16 @@ The steps to derive the XRP Ledger's secp256k1 account key pair from a seed valu 2. Calculate the [SHA-512Half][] of the concatenated value. - 3. If the result is not a valid secp265k1 private key, increment the key sequence by 1 and restart deriving the account's intermediate key pair. + 3. If the result is not a valid secp265k1 secret key, increment the key sequence by 1 and restart deriving the account's intermediate key pair. - 4. With a valid secp256k1 private key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the intermediate public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) + 4. With a valid secp256k1 secret key, use the standard ECDSA public key derivation with the secp256k1 curve to derive the intermediate public key. (As always with cryptographic algorithms, use a standard, well-known, publicly-audited implementation whenever possible. For example, [OpenSSL](https://www.openssl.org/) has implementations of core Ed25519 and secp256k1 functions.) -4. Derive the master public key pair by adding the intermediate public key to the root public key. Similarly, derive the private key by adding the intermediate private key to the root private key. +4. Derive the master public key pair by adding the intermediate public key to the root public key. Similarly, derive the secret key by adding the intermediate secret key to the root secret key. - - An ECDSA private key is just a very large integer, so you can calculate the sum of two private keys by summing them modulo the secp256k1 modulus. + - An ECDSA secret key is just a very large integer, so you can calculate the sum of two secret keys by summing them modulo the secp256k1 group order. - An ECDSA public key is a point on the elliptic curve, so you should use elliptic curve math to sum the points. - **Tip:** You don't need any private keys to derive the master public key. You can do so using only the root public key. - 5. Convert the master public key to its 33-byte compressed form, as before. 6. When serializing an account's public key to its [base58][] format, use the account public key prefix, `0x23`. diff --git a/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md b/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md index cb685b9d0f..5087990c6d 100644 --- a/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md +++ b/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md @@ -72,14 +72,14 @@ The request can contain the following parameters: | `Field` | Type | Description | |:-------------|:-------|:-----------------------------------------------------| -| `key_type` | String | Which elliptic curve to use for this key pair. Valid values are `ed25519` and `secp256k1` (all lower case). Defaults to `secp256k1`. | +| `key_type` | String |Which [signing algorithm](cryptographic-keys.html#signing-algorithms) to use to derive this key pair. Valid values are `ed25519` and `secp256k1` (all lower case). The default is `secp256k1`. | | `passphrase` | String | _(Optional)_ Generate a key pair and address from this seed value. This value can be formatted in [hexadecimal][], the XRP Ledger's [base58][] format, [RFC-1751][], or as an arbitrary string. Cannot be used with `seed` or `seed_hex`. | | `seed` | String | _(Optional)_ Generate the key pair and address from this seed value in the XRP Ledger's [base58][]-encoded format. Cannot be used with `passphrase` or `seed_hex`. | | `seed_hex` | String | _(Optional)_ Generate the key pair and address from this seed value in [hexadecimal][] format. Cannot be used with `passphrase` or `seed`. | You must provide **at most one** of the following fields: `passphrase`, `seed`, or `seed_hex`. If you omit all three, `rippled` uses a random seed. -**Note:** [Ed25519](https://ed25519.cr.yp.to/) support is experimental. The commandline version of this command cannot generate Ed25519 keys. +**Note:** The commandline version of this command cannot generate [Ed25519](https://ed25519.cr.yp.to/) keys. #### Specifying a Seed @@ -167,9 +167,10 @@ The response follows the [standard format][], with a successful result containin | `Field` | Type | Description | |:------------------|:-------|:------------------------------------------------| +| `key_type` | String | Which [signing algorithm](cryptographic-keys.html#signing-algorithms) was used to derive this key pair. Valid values are `ed25519` and `secp256k1` (all lower case). | | `master_seed` | String | This is the private key of the key pair. The master seed from which all other information about this account is derived, in the XRP Ledger's [base58][] encoded string format. Typically, you use the key in this format to sign transactions. | | `master_seed_hex` | String | The master seed, in hex format. A simple, widely-supported way to represent the private key. Can be used to sign transactions. | -| `master_key` | String | The master seed, in [RFC 1751](http://tools.ietf.org/html/rfc1751) format. An easier to remember, easier-to-write-down version of the private key. Can be used to sign transactions. | +| `master_key` | String | The master seed, in [RFC-1751][] format. An easier to remember, easier-to-write-down version of the private key. Can be used to sign transactions. **Note:** The `rippled` implementation reverses the byte order of the key after decoding from RFC-1751 and before encoding it to RFC-1751; if you read or write keys for use with the XRP Ledger using a different RFC-1751 implementation, you must do the same to be compatible with `rippled`'s RFC-1751 encoding. | | `account_id` | String | The [Address][] of the account in the XRP Ledger's [base58][] format. This is not the public key, but a hash-of-a-hash of it. It also has a checksum so a typo almost certainly results in an invalid address rather than a valid, but different address. This is the primary identifier of an account in the XRP Ledger. You tell people this to get paid, and use it in transactions to indicate who you are and who you're paying, trusting, and so forth. [Multi-signing lists](multi-signing.html) also use these to identify other signers. | | `public_key` | String | The public key of the key pair, in the XRP Ledger's [base58][] encoded string format. Derived from the `master_seed`. | | `public_key_hex` | String | This is the public key of the key pair, in hexadecimal. Derived from the `master_seed`. To validate the signature on a transaction, `rippled` needs this public key. That's why the format for a signed transaction includes the public key in the `SigningPubKey` field. | diff --git a/content/references/rippled-api/api-conventions/serialization.md b/content/references/rippled-api/api-conventions/serialization.md index 6a6f8f2528..86ab571ddc 100644 --- a/content/references/rippled-api/api-conventions/serialization.md +++ b/content/references/rippled-api/api-conventions/serialization.md @@ -1,7 +1,7 @@ # Serialization Format [[Source]
](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/impl/STObject.cpp#L696-L718 "Source") -This page describes the XRP Ledger's canonical binary format for transactions and other data. This binary format is necessary to create and verify digital signatures of those transactions' contents, and is also used in other places. The [rippled APIs](rippled-api.html) typically use JSON to communicate with client applications. However, JSON is unsuitable as a format for serializing transactions for being digitally signed, because JSON can represent the same data in many different but equivalent ways. +This page describes the XRP Ledger's canonical binary format for transactions and other data. This binary format is necessary to create and verify digital signatures of those transactions' contents, and is also used in other places including in the [peer-to-peer communications between servers](peer-protocol.html). The [`rippled` APIs](rippled-api.html) typically use JSON to communicate with client applications. However, JSON is unsuitable as a format for serializing transactions for being digitally signed, because JSON can represent the same data in many different but equivalent ways. The process of serializing a transaction from JSON or any other representation into their canonical binary format can be summarized with these steps: @@ -9,7 +9,7 @@ The process of serializing a transaction from JSON or any other representation i The [Transaction Formats Reference](transaction-formats.html) defines the required and optional fields for XRP Ledger transactions. - **Note:** The `SigningPubKey` must also be provided at this step. When signing, you can derive this key from the secret key that is provided for signing. + **Note:** The `SigningPubKey` must also be provided at this step. When signing, you can [derive this key](cryptographic-keys.html#key-derivation) from the secret key that is provided for signing. 2. Convert each field's data into its ["internal" binary format](#internal-format). diff --git a/img/key-derivation-secp256k1.png b/img/key-derivation-secp256k1.png index 149167da36f5df4dd1717ca83907d27d126d2654..b89e1cb2d438feb41a741f7449a582a4a8a4382c 100644 GIT binary patch delta 15050 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
OF8o6h{c(f)ck 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 From 32cc7522bddb01d031d4a5714285f77894b8ef8c Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Fri, 11 Oct 2019 12:50:42 -0700 Subject: [PATCH 11/13] wallet_propose: mark RFC-1751 format deprecated, per @nbougalis --- .../key-generation-methods/wallet_propose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md b/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md index 5087990c6d..6d3f9df947 100644 --- a/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md +++ b/content/references/rippled-api/admin-rippled-methods/key-generation-methods/wallet_propose.md @@ -170,7 +170,7 @@ The response follows the [standard format][], with a successful result containin | `key_type` | String | Which [signing algorithm](cryptographic-keys.html#signing-algorithms) was used to derive this key pair. Valid values are `ed25519` and `secp256k1` (all lower case). | | `master_seed` | String | This is the private key of the key pair. The master seed from which all other information about this account is derived, in the XRP Ledger's [base58][] encoded string format. Typically, you use the key in this format to sign transactions. | | `master_seed_hex` | String | The master seed, in hex format. A simple, widely-supported way to represent the private key. Can be used to sign transactions. | -| `master_key` | String | The master seed, in [RFC-1751][] format. An easier to remember, easier-to-write-down version of the private key. Can be used to sign transactions. **Note:** The `rippled` implementation reverses the byte order of the key after decoding from RFC-1751 and before encoding it to RFC-1751; if you read or write keys for use with the XRP Ledger using a different RFC-1751 implementation, you must do the same to be compatible with `rippled`'s RFC-1751 encoding. | +| `master_key` | String | **DEPRECATED** The master seed, in [RFC-1751][] format. An easier to remember, easier-to-write-down version of the private key. Can be used to sign transactions. **Note:** The `rippled` implementation reverses the byte order of the key after decoding from RFC-1751 and before encoding it to RFC-1751; if you read or write keys for use with the XRP Ledger using a different RFC-1751 implementation, you must do the same to be compatible with `rippled`'s RFC-1751 encoding. | | `account_id` | String | The [Address][] of the account in the XRP Ledger's [base58][] format. This is not the public key, but a hash-of-a-hash of it. It also has a checksum so a typo almost certainly results in an invalid address rather than a valid, but different address. This is the primary identifier of an account in the XRP Ledger. You tell people this to get paid, and use it in transactions to indicate who you are and who you're paying, trusting, and so forth. [Multi-signing lists](multi-signing.html) also use these to identify other signers. | | `public_key` | String | The public key of the key pair, in the XRP Ledger's [base58][] encoded string format. Derived from the `master_seed`. | | `public_key_hex` | String | This is the public key of the key pair, in hexadecimal. Derived from the `master_seed`. To validate the signature on a transaction, `rippled` needs this public key. That's why the format for a signed transaction includes the public key in the `SigningPubKey` field. | From 5012fcb81b3f56a6ecce793be3a9ebdc4ccb248d Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Mon, 14 Oct 2019 17:02:36 -0700 Subject: [PATCH 12/13] Key derivation: code cleanup per @seelabs review --- .../{key-derivation.py => key_derivation.py} | 18 ++++++++---------- .../accounts/cryptographic-keys.md | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) rename content/_code-samples/key-derivation/{key-derivation.py => key_derivation.py} (96%) diff --git a/content/_code-samples/key-derivation/key-derivation.py b/content/_code-samples/key-derivation/key_derivation.py similarity index 96% rename from content/_code-samples/key-derivation/key-derivation.py rename to content/_code-samples/key-derivation/key_derivation.py index de9d394c20..48d72067fb 100755 --- a/content/_code-samples/key-derivation/key-derivation.py +++ b/content/_code-samples/key-derivation/key_derivation.py @@ -30,13 +30,12 @@ from fastecdsa import keys, curve import ed25519 import RFC1751 -import base58.base58 as base58 +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' -SECP_MODULUS = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 def sha512half(buf): """ @@ -63,6 +62,7 @@ class Seed: # 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 @@ -202,11 +202,11 @@ class Seed: 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(root_pub_b+fam_b) + 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) % SECP_MODULUS + 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 @@ -256,12 +256,10 @@ def secp256k1_secret_key_from(seed): 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 > SECP_MODULUS or h_i == 0: - # Not a valid secp256k1 key - seq += 1 - continue - break - return h_i + 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): """ diff --git a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md index 5d054fa8aa..1adf4c206b 100644 --- a/content/concepts/payment-system-basics/accounts/cryptographic-keys.md +++ b/content/concepts/payment-system-basics/accounts/cryptographic-keys.md @@ -134,7 +134,7 @@ The key derivation processes described here are implemented in multiple places a - [Seed definition](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/Seed.h) - [General & Ed25519 key derivation](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/impl/SecretKey.cpp) - [secp256k1 key derivation](https://github.com/ripple/rippled/blob/develop/src/ripple/crypto/impl/GenerateDeterministicKey.cpp) -- In Python 3 in [this repository's code samples section]({{target.github_forkurl}}/blob/{{target.github_branch}}/content/_code-samples/key-derivation/key-derivation.py). +- In Python 3 in [this repository's code samples section]({{target.github_forkurl}}/blob/{{target.github_branch}}/content/_code-samples/key-derivation/key_derivation.py). - In JavaScript in the [`ripple-keypairs`](https://github.com/ripple/ripple-keypairs/) package. ### Ed25519 Key Derivation From 836246de05fc71bf1518bc6ea9255c058b6c7426 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Wed, 16 Oct 2019 13:58:11 -0700 Subject: [PATCH 13/13] base58.py typo fixes --- content/_code-samples/key-derivation/base58/base58.py | 4 ++-- content/_code-samples/tx-serialization/base58/base58.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/_code-samples/key-derivation/base58/base58.py b/content/_code-samples/key-derivation/base58/base58.py index 9767194b5f..1eb587e898 100644 --- a/content/_code-samples/key-derivation/base58/base58.py +++ b/content/_code-samples/key-derivation/base58/base58.py @@ -1,10 +1,10 @@ '''Base58 encoding -Implementations of Base58 and Base58Check endcodings that are compatible +Implementations of Base58 and Base58Check encodings that are compatible with the XRP Ledger. ''' -# This This code is adapted from the module by David Keijser at +# This code is adapted from the module by David Keijser at # . - rome@ripple.com # His notes are preserved below: diff --git a/content/_code-samples/tx-serialization/base58/base58.py b/content/_code-samples/tx-serialization/base58/base58.py index 9767194b5f..1eb587e898 100644 --- a/content/_code-samples/tx-serialization/base58/base58.py +++ b/content/_code-samples/tx-serialization/base58/base58.py @@ -1,10 +1,10 @@ '''Base58 encoding -Implementations of Base58 and Base58Check endcodings that are compatible +Implementations of Base58 and Base58Check encodings that are compatible with the XRP Ledger. ''' -# This This code is adapted from the module by David Keijser at +# This code is adapted from the module by David Keijser at # . - rome@ripple.com # His notes are preserved below: