deleted: N-of-N-Seed-Splitting,-Theory-and-Practice.md
new file: N‐of‐N-Seed-Splitting,-Theory-and-Practice.md
parent
545d92705b
commit
7f8851adc2
2 changed files with 239 additions and 119 deletions
|
|
@ -1,119 +0,0 @@
|
|||
### Contents
|
||||
+ [N-of-N Seed Splitting: A Theoretical Introduction](#a_nn)
|
||||
+ [Seed Splitting with MMGen](#a_ss)
|
||||
|
||||
#### <a name='a_nn'>N-of-N Seed Splitting: A Theoretical Introduction</a>
|
||||
|
||||
The bitwise exclusive-or operation (usually denoted as `XOR`, or “![⊕]”)
|
||||
has interesting properties that make it very useful in cryptography.
|
||||
|
||||
Suppose we have two bytes, *a* and *b*:
|
||||
|
||||
![]["a: 1 0 0 1 0 1 0 0"]
|
||||
|
||||
![]["b: 0 1 0 1 1 1 1 0"]
|
||||
|
||||
To XOR the two bytes, we compare each of their bits. If the bit is the same for
|
||||
both *a* and *b,* the corresponding bit of the result is 0. If it differs, the
|
||||
result is 1:
|
||||
|
||||
![]["a ⊖ b: 1 1 0 0 1 0 1 0"]
|
||||
|
||||
Thus XOR can be thought of logically as “one or the other but not both”, or
|
||||
arithmetically as addition [modulo][wm] 2 without carry, since 1 plus 1 equals 0
|
||||
in base-2 arithmetic.
|
||||
|
||||
As is clear from our above example, switching the order of *a* and *b* has no
|
||||
effect on the result. So XOR, like addition, is commutative:
|
||||
|
||||
![]["a ⊕ b = b ⊕ a"]
|
||||
|
||||
And like addition, grouping has no effect on the result. XOR is associative:
|
||||
|
||||
![]["a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"]
|
||||
|
||||
But unlike addition, XOR has an extra property: *invertibility.* The result can
|
||||
be switched with any of the operands, making XOR sort of like addition and
|
||||
subtraction rolled into one. Thus, if
|
||||
|
||||
![]["a ⊕ b = c"]
|
||||
|
||||
then
|
||||
|
||||
![]["c ⊕ a = b"]
|
||||
|
||||
![]["b ⊕ c = a"]
|
||||
|
||||
and so forth.
|
||||
|
||||
This last property makes XOR very handy for encryption and decryption. Given
|
||||
a plaintext *P* and a random value *r* with the same bit length as *P*, we
|
||||
encrypt *P* by XOR’ing it with *r* to obtain the ciphertext *C:*
|
||||
|
||||
![]["P ⊕ r = C"]
|
||||
|
||||
To decrypt, we just XOR the ciphertext with *r* to recover the plaintext:
|
||||
|
||||
![]["C ⊕ r = P"]
|
||||
|
||||
The randomness of the ciphertext is guaranteed to be no less than that of the
|
||||
random value. Thus if *r* is perfectly random, then *C* is perfectly
|
||||
undecipherable without knowledge of *r.* This is the principle underlying the
|
||||
[one-time pads][otp] used by spies and diplomats before the computer age, as
|
||||
well as modern [stream ciphers][sc].
|
||||
|
||||
To demonstrate how this can be used for seed splitting, all we do is change
|
||||
the names of the variables:
|
||||
|
||||
![]["seed ⊕ share1 = share2"]
|
||||
|
||||
Here *seed* is analogous to *P* in the previous example, *share<sub>1</sub>* is
|
||||
a random value with the same bit length as *seed,* and *share<sub>2</sub>* is
|
||||
the resulting “ciphertext”. Just as the ciphertext reveals nothing about the
|
||||
plaintext in the previous example, *share<sub>2</sub>* reveals nothing about
|
||||
*seed* without knowledge of *share<sub>1</sub>.* And *share<sub>1</sub>,* of
|
||||
course, being a random value, reveals nothing about *seed* either. To recover
|
||||
the seed, we just XOR the two shares. Since XOR is commutative, the order in
|
||||
which we combine them isn’t important:
|
||||
|
||||
![]["share2 ⊕ share1 = seed"]
|
||||
|
||||
Thanks to XOR’s associativity, splits of arbitrary length can be created by
|
||||
using an arbitrary number of random shares:
|
||||
|
||||
Perform an *n*-way split:
|
||||
|
||||
![]["seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"]
|
||||
|
||||
Recover the seed:
|
||||
|
||||
![]["share1 ⊕ share2 ... ⊕ shareN = seed"]
|
||||
|
||||
Knowledge of anything less than *n* shares reveals nothing about the seed.
|
||||
|
||||
#### <a name='a_ss'>Seed Splitting with MMGen</a>
|
||||
|
||||
The MMGen wallet implements seed-splitting functionality via the commands
|
||||
`mmgen-seedsplit` and `mmgen-seedjoin`.
|
||||
|
||||
TBD
|
||||
|
||||
[⊕]: https://mmgen.github.io/images/ss/o_xor.svg "⊕"
|
||||
["a: 1 0 0 1 0 1 0 0"]: https://mmgen.github.io/images/ss/byte_a.svg "a: 1 0 0 1 0 1 0 0"
|
||||
["b: 0 1 0 1 1 1 1 0"]: https://mmgen.github.io/images/ss/byte_b.svg "b: 0 1 0 1 1 1 1 0"
|
||||
["a ⊖ b: 1 1 0 0 1 0 1 0"]: https://mmgen.github.io/images/ss/byte_ab.svg "a ⊖ b: 1 1 0 0 1 0 1 0"
|
||||
["a ⊕ b = b ⊕ a"]: https://mmgen.github.io/images/ss/ab-ba.svg "a ⊕ b = b ⊕ a"
|
||||
["a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"]: https://mmgen.github.io/images/ss/abc.svg "a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"
|
||||
["a ⊕ b = c"]: https://mmgen.github.io/images/ss/ab-c.svg "a ⊕ b = c"
|
||||
["c ⊕ a = b"]: https://mmgen.github.io/images/ss/ca-b.svg "c ⊕ a = b"
|
||||
["b ⊕ c = a"]: https://mmgen.github.io/images/ss/bc-a.svg "b ⊕ c = a"
|
||||
["P ⊕ r = C"]: https://mmgen.github.io/images/ss/Pr-C.svg "P ⊕ r = C"
|
||||
["C ⊕ r = P"]: https://mmgen.github.io/images/ss/Cr-P.svg "C ⊕ r = P"
|
||||
["seed ⊕ share1 = share2"]: https://mmgen.github.io/images/ss/ss-enc.svg "seed ⊕ share1 = share2"
|
||||
["share2 ⊕ share1 = seed"]: https://mmgen.github.io/images/ss/ss-dec.svg "share2 ⊕ share1 = seed"
|
||||
["seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"]: https://mmgen.github.io/images/ss/ssN-enc.svg "seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"
|
||||
["share1 ⊕ share2 ... ⊕ shareN = seed"]: https://mmgen.github.io/images/ss/ssN-dec.svg "share1 ⊕ share2 ... ⊕ shareN = seed"
|
||||
|
||||
[wm]: https://en.wikipedia.org/wiki/Modular_arithmetic
|
||||
[otp]: https://en.wikipedia.org/wiki/One-time_pad
|
||||
[sc]: https://en.wikipedia.org/wiki/Stream_cipher
|
||||
239
N‐of‐N-Seed-Splitting,-Theory-and-Practice.md
Normal file
239
N‐of‐N-Seed-Splitting,-Theory-and-Practice.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
## Contents
|
||||
|
||||
+ [N-of-N Seed Splitting: A Theoretical Introduction](#a_nn)
|
||||
- [Deterministic Shares](#a_ds)
|
||||
- [Named Splits](#a_ns)
|
||||
- [Master Shares](#a_ms)
|
||||
+ [Seed Splitting with MMGen](#a_ss)
|
||||
|
||||
### <a name="a_nn">N-of-N Seed Splitting: A Theoretical Introduction</a>
|
||||
|
||||
The bitwise exclusive-or operation (usually denoted as `XOR`, or “![⊕]”)
|
||||
has interesting properties that make it very useful in cryptography.
|
||||
|
||||
Suppose we have two bytes, *a* and *b*:
|
||||
|
||||
![]["a: 1 0 0 1 0 1 0 0"]
|
||||
|
||||
![]["b: 0 1 0 1 1 1 1 0"]
|
||||
|
||||
To XOR the two bytes, we compare each of their bits. If the bit is the same for
|
||||
both *a* and *b,* the corresponding bit of the result is 0. If it differs, the
|
||||
result is 1:
|
||||
|
||||
![]["a ⊖ b: 1 1 0 0 1 0 1 0"]
|
||||
|
||||
Thus XOR can be thought of logically as “one or the other but not both”, or
|
||||
arithmetically as addition [modulo][wm] 2 without carry, since 1 plus 1 equals 0
|
||||
in base-2 arithmetic.
|
||||
|
||||
As is clear from our above example, switching the order of *a* and *b* has no
|
||||
effect on the result. So XOR, like addition, is commutative:
|
||||
|
||||
![]["a ⊕ b = b ⊕ a"]
|
||||
|
||||
And like addition, grouping has no effect on the result. XOR is associative:
|
||||
|
||||
![]["a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"]
|
||||
|
||||
But unlike addition, XOR has an extra property: *invertibility.* The result can
|
||||
be switched with any of the operands, making XOR sort of like addition and
|
||||
subtraction rolled into one. Thus, if
|
||||
|
||||
![]["a ⊕ b = c"]
|
||||
|
||||
then
|
||||
|
||||
![]["c ⊕ a = b"]
|
||||
|
||||
![]["b ⊕ c = a"]
|
||||
|
||||
and so forth.
|
||||
|
||||
This last property makes XOR very handy for encryption and decryption. Given
|
||||
a plaintext *P* and a random value *r* with the same bit length as *P*, we
|
||||
encrypt *P* by XOR’ing it with *r* to obtain the ciphertext *C:*
|
||||
|
||||
![]["P ⊕ r = C"]
|
||||
|
||||
To decrypt, we just XOR the ciphertext with *r* to recover the plaintext:
|
||||
|
||||
![]["C ⊕ r = P"]
|
||||
|
||||
The randomness of the ciphertext is guaranteed to be no less than that of the
|
||||
random value. Thus if *r* is perfectly random, then *C* is perfectly
|
||||
undecipherable, given no knowledge of *r.* This is the principle underlying the
|
||||
[one-time pads][otp] used by spies and diplomats before the computer age, as
|
||||
well as modern [stream ciphers][sc].
|
||||
|
||||
To demonstrate how this can be used for seed splitting, all we do is change
|
||||
the names of the variables:
|
||||
|
||||
![]["seed ⊕ share1 = share2"]
|
||||
|
||||
Here *seed* is analogous to the plaintext *P,* *share*<sub>1</sub> is a random
|
||||
value with the same bit length as *seed,* and *share*<sub>2</sub> is the
|
||||
resulting ciphertext. Just as the *C* reveals nothing about *P* in the previous
|
||||
example, *share*<sub>2</sub> reveals nothing about *seed* without knowledge of
|
||||
*share*<sub>1</sub>. To recover the seed, we just XOR the two shares. Since XOR
|
||||
is commutative, the order in which we combine them isn’t important:
|
||||
|
||||
![]["share2 ⊕ share1 = seed"]
|
||||
|
||||
Thanks to XOR’s associativity, splits of any arbitrary length *n* can be created
|
||||
by using *n*-1 random shares, with the *n*-th share being the result of the XOR
|
||||
operation:
|
||||
|
||||
Perform an *n*-way split:
|
||||
|
||||
![]["seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"]
|
||||
|
||||
Join shares 1 through *n* to recover the seed:
|
||||
|
||||
![]["share1 ⊕ share2 ... ⊕ shareN = seed"]
|
||||
|
||||
Knowledge of any combination of *n*-1 shares reveals nothing about the seed.
|
||||
|
||||
#### <a name="a_ds">Deterministic Shares</a>
|
||||
|
||||
So we’ve seen that the mathematics behind N-of-N seed splitting is basically
|
||||
trivial. In practice, though, there are several issues that need to be
|
||||
resolved. For example, how do we obtain the random values for the shares?
|
||||
An easy solution would be to just use our OS’s random number generator. A
|
||||
better one, however, is to generate the values deterministically. This provides
|
||||
two key advantages: 1) we avoid reliance on OS random numbers, the quality of
|
||||
which can depend on the hardware, operating system and such factors as the lack
|
||||
of a good entropy pool on machines without a network connection; and 2) we gain
|
||||
reproducibility—the ability to identical identical shares repeatedly, and as a
|
||||
consequence, to generate shares independently of each other.
|
||||
|
||||
This latter feature is especially useful. Suppose I want to do a 2-way split
|
||||
of my seed for backup purposes, giving one share to my friend Bob and storing
|
||||
the other share at some location accessible to me but unknown to Bob. With
|
||||
deterministic shares, I can generate Bob’s share now, giving it to Bob, and my
|
||||
own share later, once I’ve determined a good location for its safekeeping. And
|
||||
if either of us loses our share, it can just be regenerated.
|
||||
|
||||
OK, so now that we’re sold on the idea of deterministic shares, how do we
|
||||
generate them? A naïve solution is to just create a cryptographic hash of our
|
||||
seed using the SHA256 algorithm and use that directly as the first share:
|
||||
|
||||
![]["share1 = SHA256(seed)"]
|
||||
|
||||
This would work fine for a 2-way split: assuming SHA256 is secure and our seed
|
||||
is strongly random (and if either assumption is false, we’re in big trouble
|
||||
anyway), then *share*<sub>1</sub> is strongly random and reveals nothing about
|
||||
our seed. And being derived from the seed alone, it’s regenerable on demand by
|
||||
the seed’s owner.
|
||||
|
||||
For *n*-way splits where *n* is greater than two, however, the problem arises
|
||||
of how to generate the additional random values. We might be tempted to just
|
||||
keep hashing:
|
||||
|
||||
![]["share2 = SHA256(share1), share3 = SHA256(share2), ..."]
|
||||
|
||||
But you may have already spotted the problem here: the owner of the first share
|
||||
can generate all the successive shares up to *n*-1. Without the final *n*’th
|
||||
share he can’t recover the seed, but the whole point of having a greater than
|
||||
2-way split has been nullified.
|
||||
|
||||
This example illustrates what happens when we violate the golden rule of the
|
||||
wallet developer: *never derive one secret from another, except from the wallet
|
||||
seed itself.* The seed belongs solely to the wallet’s owner and is never made
|
||||
public, but other secrets might belong to others (including by being
|
||||
accidentally revealed), giving those others access to any secrets that might
|
||||
be derived from the secrets they possess.
|
||||
|
||||
The solution to this problem is to derive the shares directly from the seed, but
|
||||
with an added identifier that’s unique to each share. The [HMAC] algorithm was
|
||||
created just for this purpose:
|
||||
|
||||
![]["share1 = HMAC(seed,'share1'), share2 = HMAC(seed,'share2'), ... shareN-1 = HMAC(seed,'share<N-1>')"]
|
||||
|
||||
Using these deterministic pseudorandom values, we can now split and rejoin our
|
||||
seed in the manner described at the end of the previous section.
|
||||
|
||||
#### <a name="a_ns">Named Splits</a>
|
||||
|
||||
By now it might seem that we’ve laid the necessary theoretical groundwork and
|
||||
can now proceed to the implementation stage. But actually, we’re just getting
|
||||
started.
|
||||
|
||||
Consider the following situation: we’d like to use seed splitting as part of our
|
||||
backup strategy, entrusting shares of our seed with various people we know.
|
||||
Multiple 2-way splits seems like the best approach—if one of our share trustees
|
||||
loses their share, moves to another city, or gets run over by a bus, then we
|
||||
have the others left as a fallback.
|
||||
|
||||
However, we have no mechanism for generating multiple splits: using the
|
||||
deterministic approach outlined above, we can only generate the same set of
|
||||
pseudorandom shares over and over again. The obvious solution here is to give
|
||||
each split a name: for our split with Bob, we’ll add “bob” to each share’s
|
||||
unique identifier, for our split with Alice, we’ll add “alice”, and so forth.
|
||||
To keep things simple for demonstration purposes, we’ll just separate the parts
|
||||
of the identifier with colons:
|
||||
|
||||
![]["share1_bob = HMAC(seed,'bob:share1')"]
|
||||
|
||||
![]["2-way split with Bob: seed ⊕ share1_bob = share2_bob"]
|
||||
|
||||
In addition, we should handle the case where we have multiple identically named splits
|
||||
of different length. To avoid reusing shares between the splits, we’ll add an
|
||||
additional identifier element specifying the total number of shares:
|
||||
|
||||
![]["share1of3_friends = HMAC(seed,'friends:share1:of3') ..."]
|
||||
|
||||
![]["share1of4_friends = HMAC(seed,'friends:share1:of4') ..."]
|
||||
|
||||
![]["Split with 3 friends: seed ⊕ share1of3_friends ⊕ share2of3_friends = share3of3_friends"]
|
||||
|
||||
![]["Split with 4 friends: seed ⊕ share1of4_friends ⊕ share2of4_friends ⊕ share3of4_friends = share4of4_friends"]
|
||||
|
||||
So now we can create multiple named splits with various people and groups, and
|
||||
we’ve ensured that all shares of all possible splits are unique.
|
||||
|
||||
#### <a name="a_ms">Master Shares</a>
|
||||
|
||||
As the number of splits we create grows, the question of how to store our shares
|
||||
becomes especially problematic: each new split means another new share that we
|
||||
have to store somewhere. If there were some mechanism to generate our share of
|
||||
each split from a single master share, this would simplify things enormously for
|
||||
us. The ability to create several such master shares might come in handy too.
|
||||
|
||||
### <a name="a_ss">Seed Splitting with MMGen</a>
|
||||
|
||||
The MMGen wallet implements seed-splitting functionality via the commands
|
||||
`mmgen-seedsplit` and `mmgen-seedjoin`.
|
||||
|
||||
TBD
|
||||
|
||||
[⊕]: https://mmgen.github.io/images/ss/o_xor.svg "⊕"
|
||||
["a: 1 0 0 1 0 1 0 0"]: https://mmgen.github.io/images/ss/byte_a.svg "a: 1 0 0 1 0 1 0 0"
|
||||
["b: 0 1 0 1 1 1 1 0"]: https://mmgen.github.io/images/ss/byte_b.svg "b: 0 1 0 1 1 1 1 0"
|
||||
["a ⊖ b: 1 1 0 0 1 0 1 0"]: https://mmgen.github.io/images/ss/byte_ab.svg "a ⊖ b: 1 1 0 0 1 0 1 0"
|
||||
["a ⊕ b = b ⊕ a"]: https://mmgen.github.io/images/ss/ab-ba.svg "a ⊕ b = b ⊕ a"
|
||||
["a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"]: https://mmgen.github.io/images/ss/abc.svg "a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c"
|
||||
["a ⊕ b = c"]: https://mmgen.github.io/images/ss/ab-c.svg "a ⊕ b = c"
|
||||
["c ⊕ a = b"]: https://mmgen.github.io/images/ss/ca-b.svg "c ⊕ a = b"
|
||||
["b ⊕ c = a"]: https://mmgen.github.io/images/ss/bc-a.svg "b ⊕ c = a"
|
||||
["P ⊕ r = C"]: https://mmgen.github.io/images/ss/Pr-C.svg "P ⊕ r = C"
|
||||
["C ⊕ r = P"]: https://mmgen.github.io/images/ss/Cr-P.svg "C ⊕ r = P"
|
||||
["seed ⊕ share1 = share2"]: https://mmgen.github.io/images/ss/ss-enc.svg "seed ⊕ share1 = share2"
|
||||
["share2 ⊕ share1 = seed"]: https://mmgen.github.io/images/ss/ss-dec.svg "share2 ⊕ share1 = seed"
|
||||
["seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"]: https://mmgen.github.io/images/ss/ssN-enc.svg "seed ⊕ share1 ⊕ share2 ... ⊕ shareN-1 = shareN"
|
||||
["share1 ⊕ share2 ... ⊕ shareN = seed"]: https://mmgen.github.io/images/ss/ssN-dec.svg "share1 ⊕ share2 ... ⊕ shareN = seed"
|
||||
["share1 = SHA256(seed)"]: https://mmgen.github.io/images/ss/sha256.svg "share1 = SHA256(seed)"
|
||||
["share2 = SHA256(share1), share3 = SHA256(share2), ..."]: https://mmgen.github.io/images/ss/sha256b.svg "share2 = SHA256(share1), share3 = SHA256(share2), ..."
|
||||
["share1 = HMAC(seed,'share1'), share2 = HMAC(seed,'share2'), ... shareN-1 = HMAC(seed,'share<N-1>')"]: https://mmgen.github.io/images/ss/hmac.svg "share1 = HMAC(seed,'share1'), share2 = HMAC(seed,'share2'), ... shareN-1 = HMAC(seed,'share<N-1>')"
|
||||
["share1_bob = HMAC(seed,'bob:share1')"]: https://mmgen.github.io/images/ss/bob.svg "share1_bob = HMAC(seed,'bob:share1')"
|
||||
["2-way split with Bob: seed ⊕ share1_bob = share2_bob"]: https://mmgen.github.io/images/ss/bob2.svg "2-way split with Bob: seed ⊕ share1_bob = share2_bob"
|
||||
["share1of3_friends = HMAC(seed,'friends:share1:of3') ..."]: https://mmgen.github.io/images/ss/friends1.svg "share1of3_friends = HMAC(seed,'friends:share1:of3') ..."
|
||||
["share1of4_friends = HMAC(seed,'friends:share1:of4') ..."]: https://mmgen.github.io/images/ss/friends2.svg "share1of4_friends = HMAC(seed,'friends:share1:of4') ..."
|
||||
["Split with 3 friends: seed ⊕ share1of3_friends ⊕ share2of3_friends = share3of3_friends"]: https://mmgen.github.io/images/ss/friends1a.svg "Split with 3 friends: seed ⊕ share1of3_friends ⊕ share2of3_friends = share3of3_friends"
|
||||
["Split with 4 friends: seed ⊕ share1of4_friends ⊕ share2of4_friends ⊕ share3of4_friends = share4of4_friends"]: https://mmgen.github.io/images/ss/friends2a.svg "Split with 4 friends: seed ⊕ share1of4_friends ⊕ share2of4_friends ⊕ share3of4_friends = share4of4_friends"
|
||||
<!-- https://mmgen.github.io/images/ss/ -->
|
||||
|
||||
[HMAC]: https://en.wikipedia.org/wiki/HMAC
|
||||
[wm]: https://en.wikipedia.org/wiki/Modular_arithmetic
|
||||
[otp]: https://en.wikipedia.org/wiki/One-time_pad
|
||||
[sc]: https://en.wikipedia.org/wiki/Stream_cipher
|
||||
Loading…
Add table
Add a link
Reference in a new issue