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)
55//! that must be readily transmissible over low bandwidth channels, e.g.,
66//! email, voice, printed or handwritten notes, etc.
77
88use crypto_bigint:: { Limb , NonZero , Random as _, U256 , Wrapping } ;
9- use diceware_wordlists:: EFF_LONG_WORDLIST ;
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 MAX_PHRASE_WORDS : usize = 24 ;
19+
20+ /// With 2048 words, an 8 word code phrase has ~88 bits of entropy,
21+ /// making it suitable for use as an identifier, but not a secret.
22+ pub const ID_PHRASE_WORDS : usize = 8 ;
23+
24+ /// The BIP-39 word list contains no punctuation of any kind, so we are
25+ /// free to use ASCII hyphen (`-`) as the default word separator. We will
26+ /// also accept ASCII whitespace as word separators during decoding.
27+ pub const WORD_SEPARATOR : & str = "-" ;
1828
1929/// Decoding a phrase failed.
2030#[ derive( Debug , Error ) ]
21- #[ error( "invalid code phrase, must be ≤ 20 space-separated words from the EFF Diceware list" ) ]
31+ #[ error( "invalid code phrase, must be ≤ {MAX_PHRASE_WORDS} words from the BIP-39 list" ) ]
2232pub struct InvalidCodephrase ;
2333
2434/// Look up a word in the word list.
@@ -35,11 +45,9 @@ fn index(word: &str) -> Result<usize, InvalidCodephrase> {
3545
3646/// Turn 256 bits of entropy into a code phrase.
3747///
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.
48+ /// We emit big endian phrases with no padding. This implementation makes
49+ /// no assumptions about the value of [`WORDLIST_LEN`]; in particular, it
50+ /// does not assume that it is a power of 2.
4351pub fn codephrase ( value : U256 ) -> Vec < & ' static str > {
4452 let b = NonZero :: new ( Limb ( WORDLIST_LEN as u64 ) ) . expect ( "should have some words" ) ;
4553 let mut n = value;
@@ -57,27 +65,30 @@ pub fn codephrase(value: U256) -> Vec<&'static str> {
5765 unreachable ! ( "final remainder should fit in one limb" ) ;
5866 }
5967
60- assert ! ( words. len( ) <= 20 , "codephrase should be at most 20 words " ) ;
68+ assert ! ( words. len( ) <= MAX_PHRASE_WORDS , "codephrase is too long " ) ;
6169 words. reverse ( ) ; // emit big endian
6270 words
6371}
6472
6573/// Generate a code phrase with 256 bits of entropy.
6674pub fn generate_codephrase ( ) -> String {
67- codephrase ( U256 :: random ( & mut OsRng ) ) . join ( " " )
75+ codephrase ( U256 :: random ( & mut OsRng ) ) . join ( WORD_SEPARATOR )
6876}
6977
70- /// Generate a 6 word code phrase suitable for use as an identifier,
71- /// but not a secret (~77.5 bits of entropy).
78+ /// Generate a code phrase for use as an identifier.
7279pub fn generate_id ( ) -> String {
73- codephrase ( U256 :: random ( & mut OsRng ) ) [ ..6 ] . join ( " " )
80+ codephrase ( U256 :: random ( & mut OsRng ) ) [ ..ID_PHRASE_WORDS ] . join ( WORD_SEPARATOR )
7481}
7582
7683/// Decode a big endian code phrase into 256 bits of entropy.
7784pub fn decode_phrase ( phrase : & str ) -> Result < U256 , InvalidCodephrase > {
7885 let b = Wrapping ( U256 :: from_u64 ( WORDLIST_LEN as u64 ) ) ;
7986 let mut n = Wrapping ( U256 :: ZERO ) ;
80- for word in phrase. split_ascii_whitespace ( ) . take ( 20 ) {
87+ for word in phrase
88+ . replace ( WORD_SEPARATOR , " " )
89+ . split_ascii_whitespace ( )
90+ . take ( MAX_PHRASE_WORDS )
91+ {
8192 let i = index ( word) ?;
8293 let r = Wrapping ( U256 :: from_u64 ( i as u64 ) ) ;
8394 n *= b;
@@ -95,7 +106,7 @@ mod test {
95106 use super :: * ;
96107
97108 fn round_trip ( entropy : U256 ) -> String {
98- let codephrase = codephrase ( entropy) . join ( " " ) ;
109+ let codephrase = codephrase ( entropy) . join ( WORD_SEPARATOR ) ;
99110 let decoded = decode_phrase ( & codephrase) . unwrap ( ) ;
100111 assert_eq ! ( entropy, decoded) ;
101112 codephrase
@@ -104,51 +115,51 @@ mod test {
104115 #[ test]
105116 fn trivial_codephrases ( ) {
106117 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 " ) ;
118+ assert_eq ! ( round_trip( U256 :: ZERO ) , "abandon " ) ;
119+ assert_eq ! ( round_trip( U256 :: ONE ) , "ability " ) ;
120+ assert_eq ! ( round_trip( U256 :: from_u64( b - 1 ) ) , "zoo " ) ;
121+ assert_eq ! ( round_trip( U256 :: from_u64( b) ) , "ability-abandon " ) ;
122+ assert_eq ! ( round_trip( U256 :: from_u64( b + 1 ) ) , "ability-ability " ) ;
112123 assert_eq ! (
113124 round_trip( U256 :: from_u64( b. pow( 2 ) ) ) ,
114- "abdomen abacus abacus "
125+ "ability-abandon-abandon "
115126 ) ;
116127 assert_eq ! (
117128 round_trip( U256 :: from_u64( b. pow( 3 ) ) ) ,
118- "abdomen abacus abacus abacus "
129+ "ability-abandon-abandon-abandon "
119130 ) ;
120131 assert_eq ! (
121132 round_trip( U256 :: from_u64( b. pow( 3 ) + 1 ) ) ,
122- "abdomen abacus abacus abdomen "
133+ "ability-abandon-abandon-ability "
123134 ) ;
124135 assert_eq ! (
125136 round_trip( U256 :: from_u64( b. pow( 3 ) + b - 1 ) ) ,
126- "abdomen abacus abacus zoom "
137+ "ability-abandon-abandon-zoo "
127138 ) ;
128139 }
129140
130141 #[ test]
131142 fn constant_codephrases ( ) {
132143 assert_eq ! (
133144 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 "
145+ "abstract-summer-orange-gown-urge-model- \
146+ exact-gorilla-outside-common-this-pepper- \
147+ pear-dust-minimum-black-double-recipe- \
148+ castle-crystal-clog-logic-delay-hamster "
138149 ) ;
139150 assert_eq ! (
140151 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 "
152+ "abstract-misery-favorite-ordinary-moon-talk- \
153+ write-coffee-digital-slogan-spray-angry- \
154+ once-jazz-random-income-garage-regret-accident- \
155+ file-release-deny-reward-drastic "
145156 ) ;
146157 assert_eq ! (
147158 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 "
159+ "able-dismiss-cost-scheme-amazing-slogan- \
160+ service-current-protect-feed-length-text- \
161+ cruise-wisdom-beauty-angle-regret-truck- \
162+ prosper-album-decline-wheel-pause-legend "
152163 ) ;
153164 }
154165
@@ -158,18 +169,8 @@ mod test {
158169 for _ in 0 ..1000 {
159170 let nonce = U256 :: random ( & mut OsRng ) ;
160171 let codephrase = round_trip ( nonce) ;
161- assert ! ( [ 19 , 20 ] . contains( & codephrase. split_ascii_whitespace ( ) . count( ) ) ) ;
172+ assert ! ( [ 23 , 24 ] . contains( & codephrase. split ( WORD_SEPARATOR ) . count( ) ) ) ;
162173 assert ! ( seen. insert( codephrase) ) ;
163174 }
164175 }
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- }
175176}
0 commit comments