11use chacha20:: cipher:: { KeyIvInit , StreamCipher } ;
22use chacha20:: ChaCha20 ;
33
4+ use crate :: config:: CipherMode ;
5+
46pub const KS_LEN : usize = 9000 ;
57const BUF_LEN : usize = KS_LEN + 1 ;
68
79pub struct Keystream {
810 bytes : [ u8 ; BUF_LEN ] ,
11+ mode : CipherMode ,
912}
1013
1114impl Keystream {
12- pub fn from_passphrase ( passphrase : & str ) -> Self {
15+ pub fn from_passphrase ( passphrase : & str , mode : CipherMode ) -> Self {
1316 let key_hash = blake3:: hash ( passphrase. as_bytes ( ) ) ;
1417 let key: [ u8 ; 32 ] = * key_hash. as_bytes ( ) ;
1518 let nonce = [ 0u8 ; 12 ] ;
1619 let mut cipher = ChaCha20 :: new ( & key. into ( ) , & nonce. into ( ) ) ;
1720 let mut bytes = [ 0u8 ; BUF_LEN ] ;
1821 cipher. apply_keystream ( & mut bytes) ;
19- Self { bytes }
22+ Self { bytes, mode }
2023 }
2124
2225 pub fn max_payload ( & self ) -> usize {
2326 KS_LEN
2427 }
2528
26- /// Chained XOR encode, processed tail → head, in place.
27- /// `C[i] = P[i] ^ K[i] ^ C[i+1]`, with `C[N] = K[N]`.
2829 pub fn encode ( & self , buf : & mut [ u8 ] ) -> bool {
30+ match self . mode {
31+ CipherMode :: Chained => self . encode_chained ( buf) ,
32+ CipherMode :: Plain => self . xor_plain ( buf) ,
33+ }
34+ }
35+
36+ pub fn decode ( & self , buf : & mut [ u8 ] ) -> bool {
37+ match self . mode {
38+ CipherMode :: Chained => self . decode_chained ( buf) ,
39+ CipherMode :: Plain => self . xor_plain ( buf) ,
40+ }
41+ }
42+
43+ /// Chained XOR encode, tail → head, in place.
44+ /// `C[i] = P[i] ^ K[i] ^ C[i+1]`, with `C[N] = K[N]`.
45+ fn encode_chained ( & self , buf : & mut [ u8 ] ) -> bool {
2946 let n = buf. len ( ) ;
3047 if n == 0 || n > KS_LEN {
3148 return false ;
@@ -40,8 +57,8 @@ impl Keystream {
4057 true
4158 }
4259
43- /// Reverse of `encode `. Processed head → tail, in place.
44- pub fn decode ( & self , buf : & mut [ u8 ] ) -> bool {
60+ /// Reverse of `encode_chained `. Head → tail, in place.
61+ fn decode_chained ( & self , buf : & mut [ u8 ] ) -> bool {
4562 let n = buf. len ( ) ;
4663 if n == 0 || n > KS_LEN {
4764 return false ;
@@ -53,6 +70,20 @@ impl Keystream {
5370 }
5471 true
5572 }
73+
74+ /// Pure stateless XOR. Encode == decode. Auto-vectorizable (the compiler
75+ /// turns this into AVX2/NEON chunks).
76+ fn xor_plain ( & self , buf : & mut [ u8 ] ) -> bool {
77+ let n = buf. len ( ) ;
78+ if n == 0 || n > KS_LEN {
79+ return false ;
80+ }
81+ let k = & self . bytes [ ..n] ;
82+ for ( b, kb) in buf. iter_mut ( ) . zip ( k. iter ( ) ) {
83+ * b ^= * kb;
84+ }
85+ true
86+ }
5687}
5788
5889#[ cfg( test) ]
@@ -61,20 +92,22 @@ mod tests {
6192
6293 #[ test]
6394 fn roundtrip ( ) {
64- let ks = Keystream :: from_passphrase ( "hunter2" ) ;
65- for n in [ 1 , 2 , 7 , 16 , 100 , 1500 , 8999 , 9000 ] {
66- let pt: Vec < u8 > = ( 0 ..n) . map ( |i| ( i * 31 ) as u8 ) . collect ( ) ;
67- let mut buf = pt. clone ( ) ;
68- assert ! ( ks. encode( & mut buf) ) ;
69- assert_ne ! ( buf, pt, "ciphertext equals plaintext at n={n}" ) ;
70- assert ! ( ks. decode( & mut buf) ) ;
71- assert_eq ! ( buf, pt, "roundtrip mismatch at n={n}" ) ;
95+ for mode in [ CipherMode :: Chained , CipherMode :: Plain ] {
96+ let ks = Keystream :: from_passphrase ( "hunter2" , mode) ;
97+ for n in [ 1 , 2 , 7 , 16 , 100 , 1500 , 8999 , 9000 ] {
98+ let pt: Vec < u8 > = ( 0 ..n) . map ( |i| ( i * 31 ) as u8 ) . collect ( ) ;
99+ let mut buf = pt. clone ( ) ;
100+ assert ! ( ks. encode( & mut buf) ) ;
101+ assert_ne ! ( buf, pt, "ciphertext equals plaintext at n={n} mode={mode:?}" ) ;
102+ assert ! ( ks. decode( & mut buf) ) ;
103+ assert_eq ! ( buf, pt, "roundtrip mismatch at n={n} mode={mode:?}" ) ;
104+ }
72105 }
73106 }
74107
75108 #[ test]
76- fn tail_change_affects_head ( ) {
77- let ks = Keystream :: from_passphrase ( "hunter2" ) ;
109+ fn chained_tail_change_affects_head ( ) {
110+ let ks = Keystream :: from_passphrase ( "hunter2" , CipherMode :: Chained ) ;
78111 let mut a = vec ! [ 0u8 ; 64 ] ;
79112 let mut b = vec ! [ 0u8 ; 64 ] ;
80113 b[ 63 ] = 1 ;
@@ -83,9 +116,20 @@ mod tests {
83116 assert_ne ! ( a[ 0 ] , b[ 0 ] , "tail change must propagate to head" ) ;
84117 }
85118
119+ #[ test]
120+ fn plain_is_stateless ( ) {
121+ let ks = Keystream :: from_passphrase ( "hunter2" , CipherMode :: Plain ) ;
122+ let mut a = vec ! [ 0u8 ; 64 ] ;
123+ let mut b = vec ! [ 0u8 ; 64 ] ;
124+ b[ 63 ] = 1 ;
125+ ks. encode ( & mut a) ;
126+ ks. encode ( & mut b) ;
127+ assert_eq ! ( a[ 0 ] , b[ 0 ] , "plain XOR: head must be independent of tail" ) ;
128+ }
129+
86130 #[ test]
87131 fn rejects_oversize ( ) {
88- let ks = Keystream :: from_passphrase ( "x" ) ;
132+ let ks = Keystream :: from_passphrase ( "x" , CipherMode :: Chained ) ;
89133 let mut buf = vec ! [ 0u8 ; KS_LEN + 1 ] ;
90134 assert ! ( !ks. encode( & mut buf) ) ;
91135 }
0 commit comments