66//! `PHSF` bytes; each segment's 96-bit nonce is derived from the stream's nonce
77//! prefix, a segment counter, and a final-segment marker.
88
9+ use crate :: error:: AeadError ;
910use hkdf:: Hkdf ;
1011use sha2:: { Digest , Sha256 } ;
1112
@@ -15,6 +16,36 @@ pub(crate) const MAX_SEGMENT_SIZE: u32 = 67_108_864; // 64 MiB
1516pub ( crate ) const DEFAULT_SEGMENT_SIZE : u32 = 1_048_576 ; // 1 MiB
1617const DOMAIN_TAG : & [ u8 ; 13 ] = b"PNA-STREAM-v1" ;
1718
19+ /// On-wire GCM stream header: `salt(32) || nonce_prefix(7) || segment_size(u32 BE)`.
20+ #[ derive( Debug ) ]
21+ pub ( crate ) struct StreamHeader {
22+ pub ( crate ) salt : [ u8 ; 32 ] ,
23+ pub ( crate ) nonce_prefix : [ u8 ; 7 ] ,
24+ pub ( crate ) segment_size : u32 ,
25+ }
26+
27+ impl StreamHeader {
28+ pub ( crate ) fn to_bytes ( & self ) -> [ u8 ; STREAM_HEADER_LEN ] {
29+ let mut bytes = [ 0u8 ; STREAM_HEADER_LEN ] ;
30+ bytes[ ..32 ] . copy_from_slice ( & self . salt ) ;
31+ bytes[ 32 ..39 ] . copy_from_slice ( & self . nonce_prefix ) ;
32+ bytes[ 39 ..43 ] . copy_from_slice ( & self . segment_size . to_be_bytes ( ) ) ;
33+ bytes
34+ }
35+
36+ pub ( crate ) fn try_from_bytes ( bytes : & [ u8 ; STREAM_HEADER_LEN ] ) -> Result < Self , AeadError > {
37+ let segment_size = u32:: from_be_bytes ( bytes[ 39 ..43 ] . try_into ( ) . unwrap ( ) ) ;
38+ if segment_size == 0 || segment_size > MAX_SEGMENT_SIZE {
39+ return Err ( AeadError :: Malformed ( "segment size out of range" ) ) ;
40+ }
41+ Ok ( Self {
42+ salt : bytes[ ..32 ] . try_into ( ) . unwrap ( ) ,
43+ nonce_prefix : bytes[ 32 ..39 ] . try_into ( ) . unwrap ( ) ,
44+ segment_size,
45+ } )
46+ }
47+ }
48+
1849#[ derive( Copy , Clone , Debug ) ]
1950pub ( crate ) enum StreamKind {
2051 PerEntry ,
@@ -94,6 +125,67 @@ mod tests {
94125 0xc5 , 0xbf , 0x34 , 0x00 , 0x72 , 0x08 , 0xd5 , 0xb8 , 0x87 , 0x18 , 0x58 , 0x65 ,
95126 ] ;
96127
128+ #[ test]
129+ fn stream_header_roundtrips_through_bytes ( ) {
130+ let header = StreamHeader {
131+ salt : [ 0xA5 ; 32 ] ,
132+ nonce_prefix : [ 0x5A ; 7 ] ,
133+ segment_size : 0x01020304 ,
134+ } ;
135+ let bytes = header. to_bytes ( ) ;
136+ assert_eq ! ( bytes[ ..32 ] , [ 0xA5 ; 32 ] ) ;
137+ assert_eq ! ( bytes[ 32 ..39 ] , [ 0x5A ; 7 ] ) ;
138+ assert_eq ! ( bytes[ 39 ..43 ] , [ 0x01 , 0x02 , 0x03 , 0x04 ] ) ;
139+ let parsed = StreamHeader :: try_from_bytes ( & bytes) . unwrap ( ) ;
140+ assert_eq ! ( parsed. salt, header. salt) ;
141+ assert_eq ! ( parsed. nonce_prefix, header. nonce_prefix) ;
142+ assert_eq ! ( parsed. segment_size, header. segment_size) ;
143+ }
144+
145+ #[ test]
146+ fn stream_header_rejects_zero_segment_size ( ) {
147+ let bytes = StreamHeader {
148+ salt : [ 0 ; 32 ] ,
149+ nonce_prefix : [ 0 ; 7 ] ,
150+ segment_size : 0 ,
151+ }
152+ . to_bytes ( ) ;
153+ assert ! ( matches!(
154+ StreamHeader :: try_from_bytes( & bytes) ,
155+ Err ( AeadError :: Malformed ( _) )
156+ ) ) ;
157+ }
158+
159+ #[ test]
160+ fn stream_header_rejects_oversized_segment_size ( ) {
161+ let bytes = StreamHeader {
162+ salt : [ 0 ; 32 ] ,
163+ nonce_prefix : [ 0 ; 7 ] ,
164+ segment_size : MAX_SEGMENT_SIZE + 1 ,
165+ }
166+ . to_bytes ( ) ;
167+ assert ! ( matches!(
168+ StreamHeader :: try_from_bytes( & bytes) ,
169+ Err ( AeadError :: Malformed ( _) )
170+ ) ) ;
171+ }
172+
173+ #[ test]
174+ fn stream_header_accepts_boundary_segment_sizes ( ) {
175+ for segment_size in [ 1 , MAX_SEGMENT_SIZE ] {
176+ let bytes = StreamHeader {
177+ salt : [ 0 ; 32 ] ,
178+ nonce_prefix : [ 0 ; 7 ] ,
179+ segment_size,
180+ }
181+ . to_bytes ( ) ;
182+ assert_eq ! (
183+ StreamHeader :: try_from_bytes( & bytes) . unwrap( ) . segment_size,
184+ segment_size
185+ ) ;
186+ }
187+ }
188+
97189 #[ test]
98190 fn hkdf_sha256_rfc5869_test_case_1 ( ) {
99191 let ikm = [ 0x0bu8 ; 22 ] ;
0 commit comments