1- //! Random code phrases like `cozy bountiful dullness quarry icing sixfold `.
1+ //! Random code phrases like `abstract misery favorite ordinary moon talk `.
22//!
33//! These are intended to serve not as secrets, but as representations
44//! of up to 256 bits of entropy (e.g., identifiers, signature scalars)
5- //! that must be readily transmissible over low bandwidth channels, e.g.,
6- //! email, voice, printed or handwritten notes, etc.
5+ //! that must be readily transmissible over low bandwidth channels ( e.g.,
6+ //! email, voice, printed or handwritten notes, etc.).
77
8- use crypto_bigint:: { Limb , NonZero , Random as _, U256 , Wrapping } ;
9- use diceware_wordlists:: EFF_LONG_WORDLIST ;
8+ use crypto_bigint:: { Limb , Random as _, Reciprocal , U256 , Wrapping } ;
109use rand_core:: OsRng ;
1110use thiserror:: Error ;
1211
13- /// 6<sup>5</sup> = 7,776 = five rolls of a six-sided die.
14- pub const WORDLIST_LEN : usize = 7_776 ;
12+ use crate :: wordlist:: { WORDLIST , WORDLIST_LEN } ;
1513
16- /// The constituents of code phrases. We use the EFF Diceware list.
17- pub const WORDLIST : & [ & str ; WORDLIST_LEN ] = & EFF_LONG_WORDLIST ;
14+ /// Entropy is treated as an integer whose base is to be changed
15+ /// to 2048, which gives us indexes into the BIP-39 word list.
16+ /// Since 2048<sup>23</sup> < 2<sup>256</sup> < 2048<sup>24</sup>,
17+ /// 24 words suffice to represent 256 bits with no redundancy.
18+ pub const PHRASE_WORDS_256 : usize = 24 ;
19+
20+ /// With 2048 words, an 8 word code phrase has ~88 bits of entropy,
21+ /// making it suitable for use as a unique, hard-to-guess identifier,
22+ /// but not a secret.
23+ ///
24+ /// Note that the security of the Support Shell protocol (RFD 620) does
25+ /// *not* rely on such identifiers being unguessable; it relies only on
26+ /// the strength of the signatures produced over these phrases.
27+ pub const PHRASE_WORDS_ID : usize = 8 ;
28+
29+ /// The BIP-39 word list contains no punctuation of any kind, so we are
30+ /// free to use ASCII hyphen (`-`) as the default word separator. Decoding
31+ /// will also accept arbitrary ASCII whitespace as word separators.
32+ pub const WORD_SEPARATOR : & str = "-" ;
1833
1934/// Decoding a phrase failed.
2035#[ derive( Debug , Error ) ]
21- #[ error( "invalid code phrase, must be ≤ 20 space-separated words from the EFF Diceware list" ) ]
36+ #[ error( "invalid code phrase, must be ≤ {PHRASE_WORDS_256} words from the BIP-39 list" ) ]
2237pub struct InvalidCodephrase ;
2338
2439/// Look up a word in the word list.
@@ -33,51 +48,51 @@ fn index(word: &str) -> Result<usize, InvalidCodephrase> {
3348 WORDLIST . binary_search ( & word) . map_err ( |_| InvalidCodephrase )
3449}
3550
36- /// Turn 256 bits of entropy into a code phrase.
51+ /// Turn 256 bits of entropy into an un-padded big-endian code phrase.
3752///
38- /// We treat the entropy as a single 256 bit integer and change its base to
39- /// 6<sup>5</sup>, which gives us indexes into the EFF Diceware word list.
40- /// Since 6<sup>5<sup>19</sup></sup> < 2<sup>256</sup> < 6<sup>5<sup>20</sup></sup>,
41- /// 20 words suffice to represent 256 bits with no redundancy. Emits big endian
42- /// phrases with no padding.
53+ /// The choice of whether to pad or not is somewhat tricky, and either
54+ /// path leads to bugs. On the one hand, it's nice to assume that every
55+ /// codephrase is the same length, and that the en/decoding operations
56+ /// are roughly constant time (though these shouldn't be used for secrets,
57+ /// so that shouldn't really matter). On the other hand, padding means
58+ /// we introduce a bias towards zero into the first few words, which
59+ /// is undesirable both for humans consuming those phrases (they look
60+ /// "non-random" if you assemble even a handful of them together),
61+ /// and for truncating them into ID phrases (because there's a bias,
62+ /// so we get fewer bits of entropy if we use the first few words).
63+ /// On the whole, not padding seems preferable, but not a clear win.
4364pub fn codephrase ( value : U256 ) -> Vec < & ' static str > {
44- let b = NonZero :: new ( Limb ( WORDLIST_LEN as u64 ) ) . expect ( "should have some words" ) ;
65+ let b = Reciprocal :: new ( Limb ( WORDLIST_LEN as u64 ) ) . expect ( "should have some words" ) ;
4566 let mut n = value;
4667 let mut r;
47- let mut words = Vec :: new ( ) ;
48- while n >= ( * b) . into ( ) {
49- ( n, r) = n. div_rem_limb ( b) ;
50- words. push ( word ( r. 0 as usize ) ) ; // accumulate little endian
51- }
52-
53- // limbs are little endian
54- if let [ Limb ( l) , Limb ( 0 ) , Limb ( 0 ) , Limb ( 0 ) ] = n. to_limbs ( ) {
55- words. push ( word ( l as usize ) ) ;
56- } else {
57- unreachable ! ( "final remainder should fit in one limb" ) ;
68+ let mut words = Vec :: with_capacity ( PHRASE_WORDS_256 ) ;
69+ while n > U256 :: ZERO {
70+ ( n, r) = n. ct_div_rem_limb_with_reciprocal ( & b) ;
71+ words. push ( word ( r. 0 as usize ) ) ; // accumulate little-endian
5872 }
59-
60- assert ! ( words. len( ) <= 20 , "codephrase should be at most 20 words" ) ;
61- words. reverse ( ) ; // emit big endian
73+ words. reverse ( ) ; // emit big-endian
6274 words
6375}
6476
65- /// Generate a code phrase with 256 bits of entropy.
66- pub fn generate_codephrase ( ) -> String {
67- codephrase ( U256 :: random ( & mut OsRng ) ) . join ( " " )
77+ /// Turn 256 bits of entropy into a reasonably unique code phrase .
78+ pub fn id_phrase ( value : U256 ) -> [ & ' static str ; PHRASE_WORDS_ID ] {
79+ codephrase ( value ) [ .. PHRASE_WORDS_ID ] . try_into ( ) . unwrap ( )
6880}
6981
70- /// Generate a 6 word code phrase suitable for use as an identifier,
71- /// but not a secret (~77.5 bits of entropy).
82+ /// Generate a code phrase for use as an identifier.
7283pub fn generate_id ( ) -> String {
73- codephrase ( U256 :: random ( & mut OsRng ) ) [ .. 6 ] . join ( " " )
84+ id_phrase ( U256 :: random ( & mut OsRng ) ) . join ( WORD_SEPARATOR )
7485}
7586
7687/// Decode a big endian code phrase into 256 bits of entropy.
7788pub fn decode_phrase ( phrase : & str ) -> Result < U256 , InvalidCodephrase > {
7889 let b = Wrapping ( U256 :: from_u64 ( WORDLIST_LEN as u64 ) ) ;
7990 let mut n = Wrapping ( U256 :: ZERO ) ;
80- for word in phrase. split_ascii_whitespace ( ) . take ( 20 ) {
91+ for word in phrase
92+ . replace ( WORD_SEPARATOR , " " )
93+ . split_ascii_whitespace ( )
94+ . take ( PHRASE_WORDS_256 )
95+ {
8196 let i = index ( word) ?;
8297 let r = Wrapping ( U256 :: from_u64 ( i as u64 ) ) ;
8398 n *= b;
@@ -88,14 +103,12 @@ pub fn decode_phrase(phrase: &str) -> Result<U256, InvalidCodephrase> {
88103
89104#[ cfg( test) ]
90105mod test {
91- use std:: collections:: HashSet ;
92-
93- use sha2:: { Digest as _, Sha256 } ;
94-
95106 use super :: * ;
107+ use sha2:: { Digest as _, Sha256 } ;
108+ use std:: collections:: HashSet ;
96109
97110 fn round_trip ( entropy : U256 ) -> String {
98- let codephrase = codephrase ( entropy) . join ( " " ) ;
111+ let codephrase = codephrase ( entropy) . join ( WORD_SEPARATOR ) ;
99112 let decoded = decode_phrase ( & codephrase) . unwrap ( ) ;
100113 assert_eq ! ( entropy, decoded) ;
101114 codephrase
@@ -104,72 +117,84 @@ mod test {
104117 #[ test]
105118 fn trivial_codephrases ( ) {
106119 let b = WORDLIST_LEN as u64 ;
107- assert_eq ! ( round_trip( U256 :: ZERO ) , "abacus " ) ;
108- assert_eq ! ( round_trip( U256 :: ONE ) , "abdomen " ) ;
109- assert_eq ! ( round_trip( U256 :: from_u64( b - 1 ) ) , "zoom " ) ;
110- assert_eq ! ( round_trip( U256 :: from_u64( b) ) , "abdomen abacus " ) ;
111- assert_eq ! ( round_trip( U256 :: from_u64( b + 1 ) ) , "abdomen abdomen " ) ;
120+ assert_eq ! ( round_trip( U256 :: ZERO ) , "" ) ;
121+ assert_eq ! ( round_trip( U256 :: ONE ) , "ability " ) ;
122+ assert_eq ! ( round_trip( U256 :: from_u64( b - 1 ) ) , "zoo " ) ;
123+ assert_eq ! ( round_trip( U256 :: from_u64( b) ) , "ability-abandon " ) ;
124+ assert_eq ! ( round_trip( U256 :: from_u64( b + 1 ) ) , "ability-ability " ) ;
112125 assert_eq ! (
113126 round_trip( U256 :: from_u64( b. pow( 2 ) ) ) ,
114- "abdomen abacus abacus "
127+ "ability-abandon-abandon "
115128 ) ;
116129 assert_eq ! (
117130 round_trip( U256 :: from_u64( b. pow( 3 ) ) ) ,
118- "abdomen abacus abacus abacus "
131+ "ability-abandon-abandon-abandon "
119132 ) ;
120133 assert_eq ! (
121134 round_trip( U256 :: from_u64( b. pow( 3 ) + 1 ) ) ,
122- "abdomen abacus abacus abdomen "
135+ "ability-abandon-abandon-ability "
123136 ) ;
124137 assert_eq ! (
125138 round_trip( U256 :: from_u64( b. pow( 3 ) + b - 1 ) ) ,
126- "abdomen abacus abacus zoom "
139+ "ability-abandon-abandon-zoo "
127140 ) ;
128141 }
129142
130143 #[ test]
131144 fn constant_codephrases ( ) {
132145 assert_eq ! (
133146 round_trip( U256 :: from_be_slice( & Sha256 :: digest( "test phrase one" ) ) ) ,
134- "cozy bountiful dullness quarry icing \
135- sixfold plank armband childish gumminess \
136- ibuprofen unvaried recliner subheader muzzle \
137- map retention excitable dried unclamped "
147+ "abstract-summer-orange-gown-urge-model- \
148+ exact-gorilla-outside-common-this-pepper- \
149+ pear-dust-minimum-black-double-recipe- \
150+ castle-crystal-clog-logic-delay-hamster "
138151 ) ;
139152 assert_eq ! (
140153 round_trip( U256 :: from_be_slice( & Sha256 :: digest( "another test phrase" ) ) ) ,
141- "cork drained obsolete wish service \
142- grout graph caution feel refurnish \
143- cobalt scrap relax identity estranged \
144- repacking underpass guiding tartly impurity "
154+ "abstract-misery-favorite-ordinary-moon-talk- \
155+ write-coffee-digital-slogan-spray-angry- \
156+ once-jazz-random-income-garage-regret-accident- \
157+ file-release-deny-reward-drastic "
145158 ) ;
146159 assert_eq ! (
147160 round_trip( U256 :: from_be_slice( & Sha256 :: digest( "one more for luck!" ) ) ) ,
148- "avenge chemicals gusty ascend unending \
149- gaffe ranking suffix doorknob overpower \
150- pamperer clarify walk unbeaten overcast \
151- pants respect elsewhere angrily distinct "
161+ "able-dismiss-cost-scheme-amazing-slogan- \
162+ service-current-protect-feed-length-text- \
163+ cruise-wisdom-beauty-angle-regret-truck- \
164+ prosper-album-decline-wheel-pause-legend "
152165 ) ;
153166 }
154167
155168 #[ test]
156169 fn random_codephrases ( ) {
157170 let mut seen = HashSet :: new ( ) ;
171+
172+ // Although this test is probabilistic, you should be able to set
173+ // the iteration count arbitrarily high and never see a failure
174+ // (though you may run out of memory or time). The current value
175+ // was chosen to run in < .1 seconds on a modest development system.
158176 for _ in 0 ..1000 {
159177 let nonce = U256 :: random ( & mut OsRng ) ;
160178 let codephrase = round_trip ( nonce) ;
161- assert ! ( [ 19 , 20 ] . contains( & codephrase. split_ascii_whitespace( ) . count( ) ) ) ;
162- assert ! ( seen. insert( codephrase) ) ;
179+ let n = codephrase. split ( WORD_SEPARATOR ) . count ( ) ;
180+
181+ // Because we're using un-padded phrases, we have to guess at a
182+ // lower bound for the number of words here. 12 should be safe;
183+ // if you've hit that, congratulations! You've found 256 bits
184+ // of "entropy" whose top half is all 0! Realistically, phrases
185+ // with 22 words are easy to find, 21 much harder. Below that,
186+ // you should be mining for ₿ instead of running this test.
187+ assert ! ( n > 12 && n <= 24 , "{codephrase} has {n} words" ) ;
188+
189+ // If this assertion fails, congratulations are again in order:
190+ // you've found two distinct sets of 256 bits of "entropy" that
191+ // are exactly the same! Either something is deeply wrong with
192+ // the universe or our understanding of it, this code is broken,
193+ // or your OS RNG is not so R after all.
194+ assert ! (
195+ seen. insert( codephrase. clone( ) ) ,
196+ "duplicate codephrase {codephrase}"
197+ ) ;
163198 }
164199 }
165-
166- #[ test]
167- fn verify_wordlist ( ) {
168- assert_eq ! ( WORDLIST_LEN , 6_usize . pow( 5 ) ) ;
169- assert_eq ! ( WORDLIST_LEN , WORDLIST . len( ) ) ;
170- assert_eq ! ( HashSet :: from( * WORDLIST ) . len( ) , WORDLIST . len( ) ) ;
171- assert ! ( WORDLIST . iter( ) . all( |word| {
172- word. len( ) >= 3 && word. chars( ) . all( |c| c. is_ascii_lowercase( ) || c == '-' )
173- } ) ) ;
174- }
175200}
0 commit comments