| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Crypt::Age::Keys; |
|
2
|
|
|
|
|
|
|
our $VERSION = '0.001'; |
|
3
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:GETTY'; |
|
4
|
|
|
|
|
|
|
# ABSTRACT: Key generation and Bech32 encoding for age encryption |
|
5
|
|
|
|
|
|
|
|
|
6
|
5
|
|
|
5
|
|
157510
|
use Moo; |
|
|
5
|
|
|
|
|
9778
|
|
|
|
5
|
|
|
|
|
28
|
|
|
7
|
5
|
|
|
5
|
|
3201
|
use Carp qw(croak); |
|
|
5
|
|
|
|
|
12
|
|
|
|
5
|
|
|
|
|
246
|
|
|
8
|
5
|
|
|
5
|
|
2139
|
use Crypt::PK::X25519; |
|
|
5
|
|
|
|
|
74513
|
|
|
|
5
|
|
|
|
|
249
|
|
|
9
|
5
|
|
|
5
|
|
2260
|
use namespace::clean; |
|
|
5
|
|
|
|
|
74911
|
|
|
|
5
|
|
|
|
|
61
|
|
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
# Bech32 character set |
|
13
|
|
|
|
|
|
|
my $BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; |
|
14
|
|
|
|
|
|
|
my %BECH32_CHAR_TO_VAL = map { substr($BECH32_CHARSET, $_, 1) => $_ } 0..31; |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Human-readable parts |
|
17
|
|
|
|
|
|
|
my $HRP_PUBLIC = 'age'; |
|
18
|
|
|
|
|
|
|
my $HRP_SECRET = 'age-secret-key-'; |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
sub generate_keypair { |
|
21
|
18
|
|
|
18
|
1
|
802726
|
my ($class) = @_; |
|
22
|
|
|
|
|
|
|
|
|
23
|
18
|
|
|
|
|
117
|
my $pk = Crypt::PK::X25519->new; |
|
24
|
18
|
|
|
|
|
42370
|
$pk->generate_key; |
|
25
|
|
|
|
|
|
|
|
|
26
|
18
|
|
|
|
|
272
|
my $secret_bytes = $pk->export_key_raw('private'); |
|
27
|
18
|
|
|
|
|
96
|
my $public_bytes = $pk->export_key_raw('public'); |
|
28
|
|
|
|
|
|
|
|
|
29
|
18
|
|
|
|
|
132
|
my $public_key = $class->encode_public_key($public_bytes); |
|
30
|
18
|
|
|
|
|
93
|
my $secret_key = $class->encode_secret_key($secret_bytes); |
|
31
|
|
|
|
|
|
|
|
|
32
|
18
|
|
|
|
|
345
|
return ($public_key, $secret_key); |
|
33
|
|
|
|
|
|
|
} |
|
34
|
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
sub encode_public_key { |
|
37
|
21
|
|
|
21
|
1
|
5774
|
my ($class, $bytes) = @_; |
|
38
|
21
|
100
|
|
|
|
332
|
croak "Public key must be 32 bytes" unless length($bytes) == 32; |
|
39
|
20
|
|
|
|
|
91
|
return $class->bech32_encode($HRP_PUBLIC, $bytes); |
|
40
|
|
|
|
|
|
|
} |
|
41
|
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
sub decode_public_key { |
|
44
|
17
|
|
|
17
|
1
|
885
|
my ($class, $encoded) = @_; |
|
45
|
17
|
|
|
|
|
63
|
my ($hrp, $bytes) = $class->bech32_decode($encoded); |
|
46
|
16
|
50
|
|
|
|
127
|
croak "Invalid public key HRP: expected '$HRP_PUBLIC', got '$hrp'" |
|
47
|
|
|
|
|
|
|
unless lc($hrp) eq $HRP_PUBLIC; |
|
48
|
16
|
50
|
|
|
|
54
|
croak "Invalid public key length" unless length($bytes) == 32; |
|
49
|
16
|
|
|
|
|
68
|
return $bytes; |
|
50
|
|
|
|
|
|
|
} |
|
51
|
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
sub encode_secret_key { |
|
54
|
19
|
|
|
19
|
1
|
1042
|
my ($class, $bytes) = @_; |
|
55
|
19
|
50
|
|
|
|
102
|
croak "Secret key must be 32 bytes" unless length($bytes) == 32; |
|
56
|
19
|
|
|
|
|
63
|
return uc($class->bech32_encode($HRP_SECRET, $bytes)); |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
sub decode_secret_key { |
|
61
|
15
|
|
|
15
|
1
|
43
|
my ($class, $encoded) = @_; |
|
62
|
15
|
|
|
|
|
61
|
my ($hrp, $bytes) = $class->bech32_decode($encoded); |
|
63
|
15
|
50
|
|
|
|
66
|
croak "Invalid secret key HRP: expected '$HRP_SECRET', got '$hrp'" |
|
64
|
|
|
|
|
|
|
unless lc($hrp) eq $HRP_SECRET; |
|
65
|
15
|
50
|
|
|
|
44
|
croak "Invalid secret key length" unless length($bytes) == 32; |
|
66
|
15
|
|
|
|
|
54
|
return $bytes; |
|
67
|
|
|
|
|
|
|
} |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
sub public_key_from_secret { |
|
71
|
1
|
|
|
1
|
1
|
12
|
my ($class, $secret_key) = @_; |
|
72
|
1
|
|
|
|
|
7
|
my $secret_bytes = $class->decode_secret_key($secret_key); |
|
73
|
1
|
|
|
|
|
12
|
my $pk = Crypt::PK::X25519->new; |
|
74
|
1
|
|
|
|
|
101
|
$pk->import_key_raw($secret_bytes, 'private'); |
|
75
|
1
|
|
|
|
|
2388
|
my $public_bytes = $pk->export_key_raw('public'); |
|
76
|
1
|
|
|
|
|
7
|
return $class->encode_public_key($public_bytes); |
|
77
|
|
|
|
|
|
|
} |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
# Bech32 implementation (BIP-173) |
|
81
|
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
sub bech32_polymod { |
|
83
|
72
|
|
|
72
|
0
|
176
|
my ($values) = @_; |
|
84
|
72
|
|
|
|
|
148
|
my @GEN = (0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3); |
|
85
|
72
|
|
|
|
|
122
|
my $chk = 1; |
|
86
|
72
|
|
|
|
|
145
|
for my $v (@$values) { |
|
87
|
5384
|
|
|
|
|
7093
|
my $b = $chk >> 25; |
|
88
|
5384
|
|
|
|
|
7736
|
$chk = (($chk & 0x1ffffff) << 5) ^ $v; |
|
89
|
5384
|
|
|
|
|
7649
|
for my $i (0..4) { |
|
90
|
26920
|
100
|
|
|
|
50374
|
$chk ^= (($b >> $i) & 1) ? $GEN[$i] : 0; |
|
91
|
|
|
|
|
|
|
} |
|
92
|
|
|
|
|
|
|
} |
|
93
|
72
|
|
|
|
|
413
|
return $chk; |
|
94
|
|
|
|
|
|
|
} |
|
95
|
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
sub bech32_hrp_expand { |
|
97
|
72
|
|
|
72
|
0
|
199
|
my ($hrp) = @_; |
|
98
|
72
|
|
|
|
|
102
|
my @result; |
|
99
|
72
|
|
|
|
|
360
|
for my $c (split //, $hrp) { |
|
100
|
620
|
|
|
|
|
1104
|
push @result, ord($c) >> 5; |
|
101
|
|
|
|
|
|
|
} |
|
102
|
72
|
|
|
|
|
187
|
push @result, 0; |
|
103
|
72
|
|
|
|
|
250
|
for my $c (split //, $hrp) { |
|
104
|
620
|
|
|
|
|
950
|
push @result, ord($c) & 31; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
72
|
|
|
|
|
1133
|
return \@result; |
|
107
|
|
|
|
|
|
|
} |
|
108
|
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
sub bech32_create_checksum { |
|
110
|
40
|
|
|
40
|
0
|
122
|
my ($hrp, $data) = @_; |
|
111
|
40
|
|
|
|
|
72
|
my @values = (@{bech32_hrp_expand($hrp)}, @$data, 0, 0, 0, 0, 0, 0); |
|
|
40
|
|
|
|
|
126
|
|
|
112
|
40
|
|
|
|
|
128
|
my $polymod = bech32_polymod(\@values) ^ 1; |
|
113
|
40
|
|
|
|
|
81
|
my @checksum; |
|
114
|
40
|
|
|
|
|
76
|
for my $i (0..5) { |
|
115
|
240
|
|
|
|
|
638
|
push @checksum, ($polymod >> (5 * (5 - $i))) & 31; |
|
116
|
|
|
|
|
|
|
} |
|
117
|
40
|
|
|
|
|
244
|
return \@checksum; |
|
118
|
|
|
|
|
|
|
} |
|
119
|
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
sub bech32_verify_checksum { |
|
121
|
32
|
|
|
32
|
0
|
113
|
my ($hrp, $data) = @_; |
|
122
|
32
|
|
|
|
|
53
|
return bech32_polymod([@{bech32_hrp_expand($hrp)}, @$data]) == 1; |
|
|
32
|
|
|
|
|
94
|
|
|
123
|
|
|
|
|
|
|
} |
|
124
|
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
sub bech32_encode { |
|
126
|
40
|
|
|
40
|
0
|
925
|
my ($class, $hrp, $bytes) = @_; |
|
127
|
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
# Convert 8-bit bytes to 5-bit groups |
|
129
|
40
|
|
|
|
|
324
|
my $data = $class->_convert_bits([unpack('C*', $bytes)], 8, 5, 1); |
|
130
|
|
|
|
|
|
|
|
|
131
|
40
|
|
|
|
|
242
|
my $checksum = bech32_create_checksum($hrp, $data); |
|
132
|
40
|
|
|
|
|
190
|
my @combined = (@$data, @$checksum); |
|
133
|
|
|
|
|
|
|
|
|
134
|
40
|
|
|
|
|
104
|
my $result = $hrp . '1'; |
|
135
|
40
|
|
|
|
|
80
|
for my $d (@combined) { |
|
136
|
2268
|
|
|
|
|
4091
|
$result .= substr($BECH32_CHARSET, $d, 1); |
|
137
|
|
|
|
|
|
|
} |
|
138
|
|
|
|
|
|
|
|
|
139
|
40
|
|
|
|
|
324
|
return $result; |
|
140
|
|
|
|
|
|
|
} |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
sub bech32_decode { |
|
143
|
33
|
|
|
33
|
0
|
960
|
my ($class, $str) = @_; |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
# Find separator |
|
146
|
33
|
|
|
|
|
96
|
my $sep_pos = rindex($str, '1'); |
|
147
|
33
|
100
|
|
|
|
275
|
croak "Invalid bech32: no separator" if $sep_pos < 1; |
|
148
|
32
|
50
|
|
|
|
97
|
croak "Invalid bech32: empty data" if $sep_pos + 1 >= length($str); |
|
149
|
|
|
|
|
|
|
|
|
150
|
32
|
|
|
|
|
94
|
my $hrp = substr($str, 0, $sep_pos); |
|
151
|
32
|
|
|
|
|
106
|
my $data_part = lc(substr($str, $sep_pos + 1)); |
|
152
|
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
# Decode data part |
|
154
|
32
|
|
|
|
|
58
|
my @data; |
|
155
|
32
|
|
|
|
|
409
|
for my $c (split //, $data_part) { |
|
156
|
1804
|
50
|
|
|
|
3139
|
croak "Invalid bech32 character: $c" unless exists $BECH32_CHAR_TO_VAL{$c}; |
|
157
|
1804
|
|
|
|
|
2670
|
push @data, $BECH32_CHAR_TO_VAL{$c}; |
|
158
|
|
|
|
|
|
|
} |
|
159
|
|
|
|
|
|
|
|
|
160
|
32
|
50
|
|
|
|
267
|
croak "Invalid bech32 checksum" |
|
161
|
|
|
|
|
|
|
unless bech32_verify_checksum(lc($hrp), \@data); |
|
162
|
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
# Remove checksum (last 6 values) |
|
164
|
32
|
|
|
|
|
160
|
splice(@data, -6); |
|
165
|
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
# Convert 5-bit groups back to 8-bit bytes |
|
167
|
32
|
|
|
|
|
153
|
my $bytes = $class->_convert_bits(\@data, 5, 8, 0); |
|
168
|
|
|
|
|
|
|
|
|
169
|
32
|
|
|
|
|
339
|
return ($hrp, pack('C*', @$bytes)); |
|
170
|
|
|
|
|
|
|
} |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
sub _convert_bits { |
|
173
|
72
|
|
|
72
|
|
244
|
my ($class, $data, $from_bits, $to_bits, $pad) = @_; |
|
174
|
|
|
|
|
|
|
|
|
175
|
72
|
|
|
|
|
151
|
my $acc = 0; |
|
176
|
72
|
|
|
|
|
147
|
my $bits = 0; |
|
177
|
72
|
|
|
|
|
113
|
my @result; |
|
178
|
72
|
|
|
|
|
154
|
my $maxv = (1 << $to_bits) - 1; |
|
179
|
|
|
|
|
|
|
|
|
180
|
72
|
|
|
|
|
166
|
for my $v (@$data) { |
|
181
|
2860
|
|
|
|
|
4044
|
$acc = ($acc << $from_bits) | $v; |
|
182
|
2860
|
|
|
|
|
3528
|
$bits += $from_bits; |
|
183
|
2860
|
|
|
|
|
7230
|
while ($bits >= $to_bits) { |
|
184
|
2981
|
|
|
|
|
3591
|
$bits -= $to_bits; |
|
185
|
2981
|
|
|
|
|
7374
|
push @result, ($acc >> $bits) & $maxv; |
|
186
|
|
|
|
|
|
|
} |
|
187
|
|
|
|
|
|
|
} |
|
188
|
|
|
|
|
|
|
|
|
189
|
72
|
100
|
|
|
|
174
|
if ($pad) { |
|
190
|
40
|
100
|
|
|
|
98
|
if ($bits > 0) { |
|
191
|
39
|
|
|
|
|
83
|
push @result, ($acc << ($to_bits - $bits)) & $maxv; |
|
192
|
|
|
|
|
|
|
} |
|
193
|
|
|
|
|
|
|
} else { |
|
194
|
32
|
50
|
|
|
|
104
|
croak "Invalid padding" if $bits >= $from_bits; |
|
195
|
32
|
50
|
|
|
|
112
|
croak "Non-zero padding" if (($acc << ($to_bits - $bits)) & $maxv); |
|
196
|
|
|
|
|
|
|
} |
|
197
|
|
|
|
|
|
|
|
|
198
|
72
|
|
|
|
|
247
|
return \@result; |
|
199
|
|
|
|
|
|
|
} |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
1; |
|
203
|
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
__END__ |