File Coverage

blib/lib/Bitcoin/BIP39.pm
Criterion Covered Total %
statement 70 78 89.7
branch 15 30 50.0
condition 6 19 31.5
subroutine 8 8 100.0
pod 3 3 100.0
total 102 138 73.9


line stmt bran cond sub pod time code
1             package Bitcoin::BIP39;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-01-09'; # DATE
5             our $DIST = 'Bitcoin-BIP39'; # DIST
6             our $VERSION = '0.003'; # VERSION
7              
8 2     2   64615 use 5.010001;
  2         16  
9 2     2   9 use strict;
  2         4  
  2         63  
10 2     2   10 use warnings;
  2         4  
  2         58  
11              
12 2     2   8 use Exporter qw(import);
  2         4  
  2         2466  
13             our @EXPORT_OK = qw(
14             entropy_to_bip39_mnemonic
15             bip39_mnemonic_to_entropy
16             gen_bip39_mnemonic
17             );
18              
19             our %SPEC;
20              
21             my %all_words_cache; # key = module name, value = \@wordlist
22              
23             our %arg_language = (
24             language => {
25             summary => 'Pick which language wordlist to use',
26             schema => ['str*', match=>qr/\A\w{2}(?:-\w+)?\z/],
27             default => 'en',
28             description => <<'_',
29              
30             Will retrieve wordlist from `WordList::::BIP39` Perl module.
31              
32             For Chinese (simplified), use `zh-simplified`. For Chinese (traditional), use
33             `zh-traditional`.
34              
35             _
36             },
37             );
38              
39             our %arg0_mnemonic = (
40             mnemonic => {
41             summary => 'Mnemonic phrase',
42             schema => ['str*'],
43             req => 1,
44             pos => 0,
45             },
46             );
47              
48             our %arg_bits = (
49             bits => {
50             summary => 'Size of entropy in bits',
51             schema => ['posint*', in=>[128, 160, 192, 224, 256]],
52             default => 128,
53             },
54             );
55              
56             our %args_entropy = (
57             entropy => {
58             summary => 'Entropy (binary data)',
59             schema => ['buf*'],
60             },
61             entropy_hex => {
62             summary => 'Entropy (hex-encoded)',
63             schema => ['hexbuf*'],
64             pos => 0,
65             },
66             );
67              
68             our %arg_encoding = (
69             encoding => {
70             schema => ['str', in=>["hex"]],
71             default => 'hex',
72             },
73             );
74              
75             sub _get_all_words {
76 7   50 7   54 my $language = shift // 'en';
77              
78 7 50       79 my ($langcode, $variant) = $language =~ /\A(\w{2})(?:-(\w+))?\z/
79             or die "Invalid language '$language', please specify a ".
80             "2-digit language code";
81 7 50       42 my $mod = "WordList::".uc($langcode).
82             ($variant ? "::".ucfirst(lc($variant)) : "")."::BIP39";
83 7 100       28 if ($all_words_cache{$mod}) {
84 5         16 return $all_words_cache{$mod};
85             }
86 2         12 (my $mod_pm = "$mod.pm") =~ s!::!/!g;
87 2         1090 require $mod_pm;
88 2         4072 return ($all_words_cache{$mod} = [$mod->new->all_words]);
89             }
90              
91             $SPEC{entropy_to_bip39_mnemonic} = {
92             v => 1.1,
93             summary => 'Convert entropy to BIP39 mnemonic phrase',
94             args => {
95             %arg_language,
96             %args_entropy,
97             },
98             args_rels => {
99             req_one => ['entropy', 'entropy_hex'],
100             },
101             result_naked => 1,
102             };
103             sub entropy_to_bip39_mnemonic {
104 4     4 1 3510 require Digest::SHA;
105              
106 4         4182 my %args = @_;
107              
108 4         10 my $entropy;
109 4 100       18 if (defined $args{entropy}) {
    50          
110 3         8 $entropy = $args{entropy};
111             } elsif (defined $args{entropy_hex}) {
112 1         7 $entropy = pack("H*", $args{entropy_hex});
113             } else {
114 0         0 die "Please specify entropy/entropy_hex";
115             }
116              
117 4         16 my $bits = length($entropy) * 8;
118 4 0 33     23 unless ($bits == 128 || $bits == 160 || $bits == 192 ||
      33        
      0        
      0        
119             $bits == 224 || $bits == 256) {
120 0         0 die "Sorry, bits=$bits not yet supported";
121             }
122              
123 4         14 my $bits_chksum = $bits / 32;
124 4         12 my $num_words = ($bits + $bits_chksum) / 11; # in number of words
125              
126 4         20 my $all_words = _get_all_words($args{language});
127              
128 4         37859 my $chksum = Digest::SHA::sha256($entropy);
129              
130 4         56 my $bitstr = unpack("B*", $entropy) . unpack("B$bits_chksum", $chksum);
131             #say "D:bitstr=<$bitstr>";
132 4         14 my @words;
133 4         28 while ($bitstr =~ /(.{11})/g) {
134 48         123 my $index = unpack("n", pack("B*", "00000$1"));
135             #say "D:index = <$index>";
136 48         177 push @words, $all_words->[$index];
137             }
138 4         31 join " ", @words;
139             }
140              
141             $SPEC{bip39_mnemonic_to_entropy} = {
142             v => 1.1,
143             summary => 'Convert BIP39 mnemonic phrase to entropy',
144             args => {
145             %arg0_mnemonic,
146             %arg_language,
147             %arg_encoding,
148             },
149             result_naked => 1,
150             };
151             sub bip39_mnemonic_to_entropy {
152 3     3 1 1657 require Digest::SHA;
153              
154 3         2864 my %args = @_;
155              
156 3         15 my $all_words = _get_all_words($args{language});
157              
158 3         32167 my @words = split /\s+/, lc($args{mnemonic});
159 3         8 my ($bits, $bits_chksum);
160 3 50       13 if (@words == 12) {
    0          
    0          
    0          
    0          
161 3         11 ($bits, $bits_chksum) = (128, 4);
162             } elsif (@words == 15) {
163 0         0 ($bits, $bits_chksum) = (160, 5);
164             } elsif (@words == 18) {
165 0         0 ($bits, $bits_chksum) = (192, 6);
166             } elsif (@words == 21) {
167 0         0 ($bits, $bits_chksum) = (224, 7);
168             } elsif (@words == 24) {
169 0         0 ($bits, $bits_chksum) = (256, 8);
170             } else {
171 0         0 die "Invalid number of words, must be 12/15/18/21/24";
172             }
173              
174 3         6 my @indices;
175             WORD:
176 3         9 for my $word (@words) {
177             # XXX use binary search
178 36         45 for my $idx (0..$#{$all_words}) {
  36         118  
179 50043 100       68090 if ($word eq $all_words->[$idx]) {
180 36         95 push @indices, $idx;
181 36         126 next WORD;
182             }
183             }
184 0         0 die "Word '$word' not found in wordlist";
185             }
186              
187 3         12 my $bitstr = "";
188 3         11 for my $idx (@indices) {
189 36         74 my $b = unpack("B*", pack("n", $idx));
190 36         69 $bitstr .= substr($b, 5);
191             }
192             #say "D:bitstr=<$bitstr>";
193              
194 3         77 my $entropy = pack("B*", substr($bitstr, 0, $bits));
195             #say "D:entropy_hex=", unpack("H*", $entropy);
196 3         9 my $chksum = substr($bitstr, $bits);
197             #say "D:chksum=<$chksum>";
198              
199 3         38 my $real_chksum = Digest::SHA::sha256($entropy);
200             #say "D:real_chksum=", unpack("B$bits_chksum", $real_chksum);
201 3 100       20 unless ($chksum eq unpack("B$bits_chksum", $real_chksum)) {
202 1         27 die "Invalid mnemonic (checksum doesn't match)";
203             }
204              
205 2 100 66     17 if ($args{encoding} && $args{encoding} eq 'hex') {
206 1         20 return unpack("H*", $entropy);
207             } else {
208 1         20 return $entropy;
209             }
210             }
211              
212             $SPEC{gen_bip39_mnemonic} = {
213             v => 1.1,
214             summary => 'Generate BIP39 mnemonic phrase',
215             args => {
216             %arg_language,
217             %arg_bits,
218             },
219             result_naked => 1,
220             };
221             sub gen_bip39_mnemonic {
222 2     2 1 4821 require Bytes::Random::Secure;
223              
224 2         19413 my %args = @_;
225              
226 2   50     17 my $bits = $args{bits} // 128;
227 2 50       11 $bits % 8 and die "Please specify bits that are divisible by 8";
228 2         12 my $entropy = Bytes::Random::Secure::random_bytes($bits / 8);
229              
230             my $mnemonic = entropy_to_bip39_mnemonic(
231             entropy => $entropy,
232 2         759316071 (language => $args{language}) x !!defined($args{language}),
233             );
234              
235             return {
236 2         29 mnemonic => $mnemonic,
237             entropy_hex => unpack("H*", $entropy),
238             };
239             }
240              
241             1;
242             # ABSTRACT: A BIP39 implementation in Perl
243              
244             __END__