File Coverage

lib/Noise/HandshakeState.pm
Criterion Covered Total %
statement 20 20 100.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 27 27 100.0


line stmt bran cond sub pod time code
1 3     3   65 use v5.42.0;
  3         14  
2 3     3   16 use feature 'class';
  3         3  
  3         367  
3 3     3   16 no warnings 'experimental::class';
  3         4  
  3         248  
4             #
5             class Noise::HandshakeState v0.0.2 {
6 3     3   1597 use Noise::SymmetricState;
  3         9  
  3         121  
7 3     3   1607 use Noise::Pattern;
  3         9  
  3         120  
8 3     3   1714 use Crypt::PK::X25519;
  3         33798  
  3         161  
9 3     3   2061 use Crypt::PK::ECC;
  3         10383  
  3         8176  
10             #
11             my %DH_CONFIG = (
12             25519 => { class => 'Crypt::PK::X25519', pub_len => 32 },
13             P256 => { class => 'Crypt::PK::ECC', pub_len => 65, params => 'secp256r1' },
14             P384 => { class => 'Crypt::PK::ECC', pub_len => 97, params => 'secp384r1' },
15             P521 => { class => 'Crypt::PK::ECC', pub_len => 133, params => 'secp521r1' }
16             );
17             #
18             field $s : reader : param //= undef; # Local static key
19             field $e : reader : param //= undef; # Local ephemeral key
20             field $rs : reader : param //= undef; # Remote static public key
21             field $re : reader : param //= undef; # Remote ephemeral public key
22             field $psks : reader : param //= []; # Array of pre-shared keys
23             field $psk_idx = 0;
24             field $initiator : param; # Boolean
25             field $pattern : param; # Pattern name or object
26             field $prologue : param //= ''; # Optional prologue
27             field $symmetric_state : reader;
28             field $msg_idx = 0;
29             field $dh_name;
30             field $dh_len;
31             #
32             ADJUST {
33             my $full_name = $pattern;
34             if ( $full_name !~ /^Noise(?:PSK)?_/ ) {
35             $full_name = 'Noise_' . $pattern . '_25519_ChaChaPoly_SHA256';
36             }
37             my $is_psk_prefix = ( $full_name =~ /^NoisePSK_/ );
38             my ( $p_name, $dh, $cipher_name, $hash_name ) = $full_name =~ /^Noise(?:PSK)?_([^_]+)_([^_]+)_([^_]+)_([^_]+)$/;
39             die 'Invalid protocol name: ' . $full_name unless $p_name;
40             if ( $is_psk_prefix && $p_name !~ /psk/ ) {
41             $p_name .= 'psk0';
42             }
43              
44             # The protocol name used for h_init must be the standard Noise_... format
45             my $noise_name = 'Noise_' . $p_name . '_' . $dh . '_' . $cipher_name . '_' . $hash_name;
46             $dh_name = $dh;
47             $dh_len = $DH_CONFIG{$dh_name}->{pub_len} or die "Unsupported DH: $dh_name";
48             if ( ref $pattern ne 'Noise::Pattern' ) {
49             $pattern = Noise::Pattern->new( name => $p_name );
50             }
51             $symmetric_state = Noise::SymmetricState->new( cipher => $cipher_name, hash => $hash_name );
52             $symmetric_state->initialize_symmetric($full_name); # using full_name matched noise-c Vector 16 better
53              
54             # Mix prologue (always mixed, even if empty)
55             $symmetric_state->mix_hash($prologue);
56              
57             # Process pre-messages
58             my $pre = $pattern->pre_msg;
59              
60             # Process initiator pre-messages (index 0)
61             for my $token ( $pre->[0]->@* ) {
62             if ( $token eq 's' ) {
63             my $pk = $initiator ? $s : $rs;
64             $symmetric_state->mix_hash( $pk->export_key_raw('public') // '' );
65             }
66             elsif ( $token eq 'e' ) {
67             my $pk = $initiator ? $e : $re;
68             $symmetric_state->mix_hash( $pk->export_key_raw('public') // '' );
69             }
70             }
71              
72             # Process responder pre-messages (index 1)
73             for my $token ( $pre->[1]->@* ) {
74             if ( $token eq 's' ) {
75             my $pk = $initiator ? $rs : $s;
76             $symmetric_state->mix_hash( $pk->export_key_raw('public') // '' );
77             }
78             elsif ( $token eq 'e' ) {
79             my $pk = $initiator ? $re : $e;
80             $symmetric_state->mix_hash( $pk->export_key_raw('public') // '' );
81             }
82             }
83             }
84             method _new_dh_obj { $DH_CONFIG{$dh_name}->{class}->new() }
85              
86             method _dh_generate_key($obj) {
87             if ( $dh_name =~ /^P/ ) {
88             $obj->generate_key( $DH_CONFIG{$dh_name}->{params} );
89             }
90             else {
91             $obj->generate_key();
92             }
93             }
94              
95             method _dh_import_key( $obj, $raw, $type ) {
96             if ( $dh_name =~ /^P/ ) {
97             $obj->import_key_raw( $raw, $DH_CONFIG{$dh_name}->{params} );
98             }
99             else {
100             $obj->import_key_raw( $raw, $type );
101             }
102             }
103              
104             method write_message ($payload) {
105             my $tokens = $pattern->msg_seq->[ $msg_idx++ ] or die 'No more messages in pattern';
106             my $message = '';
107             for my $token (@$tokens) {
108             if ( $token eq 'e' ) {
109             $e //= $self->_new_dh_obj();
110             $self->_dh_generate_key($e) unless $e->is_private;
111             my $pub = $e->export_key_raw('public');
112             $message .= $pub;
113             $symmetric_state->mix_hash($pub);
114             $symmetric_state->mix_key($pub) if $pattern->has_psk;
115             }
116             elsif ( $token eq 's' ) {
117             my $pub = $s->export_key_raw('public');
118             $message .= $symmetric_state->encrypt_and_hash($pub);
119             $symmetric_state->mix_key($pub) if $pattern->has_psk;
120             }
121             elsif ( $token eq 'ee' ) { # ee: DH(initiator_e, responder_e)
122             die 'ee failed: e or re undefined' unless $e && $re;
123             $symmetric_state->mix_key( $e->shared_secret($re) );
124             }
125             elsif ( $token eq 'es' ) { # es: DH(initiator_e, responder_s)
126             if ($initiator) {
127             die 'es failed: e or rs undefined' unless $e && $rs;
128             $symmetric_state->mix_key( $e->shared_secret($rs) );
129             }
130             else {
131             die 'es failed: s or re undefined' unless $s && $re;
132             $symmetric_state->mix_key( $s->shared_secret($re) );
133             }
134             }
135             elsif ( $token eq 'se' ) { # se: DH(initiator_s, responder_e)
136             if ($initiator) {
137             die 'se failed: s or re undefined' unless $s && $re;
138             $symmetric_state->mix_key( $s->shared_secret($re) );
139             }
140             else {
141             die 'se failed: e or rs undefined' unless $e && $rs;
142             $symmetric_state->mix_key( $e->shared_secret($rs) );
143             }
144             }
145             elsif ( $token eq 'ss' ) { # ss: DH(initiator_s, responder_s)
146             die 'ss failed: s or rs undefined' unless $s && $rs;
147             $symmetric_state->mix_key( $s->shared_secret($rs) );
148             }
149             elsif ( $token eq 'psk' ) {
150             $symmetric_state->mix_key_and_hash( $psks->[ $psk_idx++ ] // die 'Missing PSK at index ' . $psk_idx );
151             }
152             }
153             return $message . $symmetric_state->encrypt_and_hash($payload);
154             }
155              
156             method read_message ($message) {
157             my $tokens = $pattern->msg_seq->[ $msg_idx++ ] or die 'No more messages in pattern';
158             my $pos = 0;
159             for my $token (@$tokens) {
160             if ( $token eq 'e' ) {
161             my $pub_raw = substr( $message, $pos, $dh_len );
162             $pos += $dh_len;
163             $re = $self->_new_dh_obj();
164             $self->_dh_import_key( $re, $pub_raw, 'public' );
165             $symmetric_state->mix_hash($pub_raw);
166             if ( $pattern->has_psk ) { $symmetric_state->mix_key($pub_raw); }
167             }
168             elsif ( $token eq 's' ) {
169             my $len = $symmetric_state->cipher_state->has_key ? $dh_len + 16 : $dh_len;
170             my $ct = substr( $message, $pos, $len );
171             $pos += $len;
172             my $pub_raw = $symmetric_state->decrypt_and_hash($ct);
173             $rs = $self->_new_dh_obj();
174             $self->_dh_import_key( $rs, $pub_raw, 'public' );
175             $symmetric_state->mix_key($pub_raw) if $pattern->has_psk;
176             }
177             elsif ( $token eq 'ee' ) { # ee: DH(initiator_e, responder_e)
178             die 'ee failed: e or re undefined' unless $e && $re;
179             $symmetric_state->mix_key( $e->shared_secret($re) );
180             }
181             elsif ( $token eq 'es' ) { # es: DH(initiator_e, responder_s)
182             if ($initiator) {
183             die 'es failed: e or rs undefined' unless $e && $rs;
184             $symmetric_state->mix_key( $e->shared_secret($rs) );
185             }
186             else {
187             die 'es failed: s or re undefined' unless $s && $re;
188             $symmetric_state->mix_key( $s->shared_secret($re) );
189             }
190             }
191             elsif ( $token eq 'se' ) { # se: DH(initiator_s, responder_e)
192             if ($initiator) {
193             die 'se failed: s or re undefined' unless $s && $re;
194             $symmetric_state->mix_key( $s->shared_secret($re) );
195             }
196             else {
197             die 'se failed: e or rs undefined' unless $e && $rs;
198             $symmetric_state->mix_key( $e->shared_secret($rs) );
199             }
200             }
201             elsif ( $token eq 'ss' ) { # ss: DH(initiator_s, responder_s)
202             die 'ss failed: s or rs undefined' unless $s && $rs;
203             $symmetric_state->mix_key( $s->shared_secret($rs) );
204             }
205             elsif ( $token eq 'psk' ) {
206             $symmetric_state->mix_key_and_hash( $psks->[ $psk_idx++ ] // die 'Missing PSK at index ' . $psk_idx );
207             }
208             }
209             my $payload = $symmetric_state->decrypt_and_hash( substr( $message, $pos ) );
210             return $payload;
211             }
212             method split () { $symmetric_state->split() }
213             };
214             #
215             1;