chacha20blake3 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 884132d1f327e45e947d008367009753b1c014a8809a1330fb1cbc8cf014def6
4
- data.tar.gz: c1ff6117acd18ead79c48d08a9bcb2f223dd4524e65b93268f06c9ca1ac16b37
3
+ metadata.gz: 6bc223f7d13a7c25813924f8de95fd9b98e335a76ed9781fa434a0884d0bfd3a
4
+ data.tar.gz: 43789cf6dbed44f5347a51812d1e181c27d45e91124e59c1c14f24f9c778dfec
5
5
  SHA512:
6
- metadata.gz: 71187d3c2e6d86858938c3fe26476c60bdc1af777d4eee842794575d1823fb95514d73fe14282c1e38828b6b691e2d71ffbf5c570e6abb2cd0efc438a3e9a547
7
- data.tar.gz: affa5cb7f8123f050a9f8cb38b83f1c11110e7730ee360b53287e81c12c8017a36ffcb68dc88113be539aeab23247626864023b27b39463b6ec48f8a20b8ad84
6
+ metadata.gz: 6b94fd99f4c90238517f12e816d8a34fb620746674aab9f5d942258ac7e3c3769b3fe5b44f3282c6d29bafb24cab7ba10e63525c2769ddd6cd3f658fd058671d
7
+ data.tar.gz: bbeeee226c47688e6398fab4bfc3b17025d2629d3e8e54a9e52f013b751be2be377b8ddd996b730cab208249cf0c94770cf067c92a36e35ecbbf90e54ce8869e
data/Cargo.lock CHANGED
@@ -60,7 +60,7 @@ dependencies = [
60
60
  "arrayvec",
61
61
  "cc",
62
62
  "cfg-if",
63
- "constant_time_eq",
63
+ "constant_time_eq 0.4.2",
64
64
  "cpufeatures",
65
65
  "zeroize",
66
66
  ]
@@ -90,22 +90,14 @@ version = "1.0.4"
90
90
  source = "registry+https://github.com/rust-lang/crates.io-index"
91
91
  checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
92
92
 
93
- [[package]]
94
- name = "chacha"
95
- version = "0.1.0"
96
- source = "git+https://github.com/skerkour/chacha20-blake3?rev=9942bf34c77b03896bb1733079256076ca823e58#9942bf34c77b03896bb1733079256076ca823e58"
97
- dependencies = [
98
- "zeroize",
99
- ]
100
-
101
93
  [[package]]
102
94
  name = "chacha20-blake3"
103
- version = "0.9.11"
104
- source = "git+https://github.com/skerkour/chacha20-blake3?rev=9942bf34c77b03896bb1733079256076ca823e58#9942bf34c77b03896bb1733079256076ca823e58"
95
+ version = "0.10.0"
96
+ source = "registry+https://github.com/rust-lang/crates.io-index"
97
+ checksum = "55d7729707a8889ea911e9c775c5af9eba82e98ed1588e18b0ec7ae3a51c71bd"
105
98
  dependencies = [
106
99
  "blake3",
107
- "chacha",
108
- "constant_time_eq",
100
+ "constant_time_eq 0.5.0",
109
101
  "zeroize",
110
102
  ]
111
103
 
@@ -137,6 +129,12 @@ version = "0.4.2"
137
129
  source = "registry+https://github.com/rust-lang/crates.io-index"
138
130
  checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
139
131
 
132
+ [[package]]
133
+ name = "constant_time_eq"
134
+ version = "0.5.0"
135
+ source = "registry+https://github.com/rust-lang/crates.io-index"
136
+ checksum = "1a1ec0cfec728a79a5109075543131387f911cb4d07716436d7ae20533657a96"
137
+
140
138
  [[package]]
141
139
  name = "cpufeatures"
142
140
  version = "0.3.0"
@@ -8,7 +8,7 @@ name = "chacha20blake3"
8
8
  crate-type = ["cdylib", "rlib"]
9
9
 
10
10
  [dependencies]
11
- chacha20-blake3 = { git = "https://github.com/skerkour/chacha20-blake3", rev = "9942bf34c77b03896bb1733079256076ca823e58" }
11
+ chacha20-blake3 = "0.10.0"
12
12
  blake3 = { version = "1", features = ["std"] }
13
13
  magnus = "0.8"
14
14
  getrandom = "0.2"
@@ -13,9 +13,9 @@ use std::sync::{Mutex, OnceLock};
13
13
 
14
14
  const KEY_SIZE: usize = 32;
15
15
  const NONCE_SIZE: usize = 24;
16
+ const SESSION_NONCE_SIZE: usize = 8;
16
17
  const TAG_SIZE: usize = 32;
17
18
 
18
- // Opaque<T> is Send+Sync and is designed for storing Ruby values in statics.
19
19
  static DECRYPTION_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
20
20
 
21
21
  fn decryption_error(ruby: &Ruby) -> ExceptionClass {
@@ -29,27 +29,14 @@ struct Cipher(chacha20_blake3::ChaCha20Blake3);
29
29
  unsafe impl Send for Cipher {}
30
30
  unsafe impl Sync for Cipher {}
31
31
 
32
- // No #reset or #rewind method by design: allowing the counter to go backwards
33
- // would silently reuse (key, nonce) pairs, which is catastrophic for a stream cipher.
34
- #[magnus::wrap(class = "ChaCha20Blake3::Stream", free_immediately, size)]
35
- struct Stream {
36
- cipher: chacha20_blake3::ChaCha20Blake3,
37
- nonce_prefix: [u8; 16],
38
- counter_base: u64,
39
- counter: Mutex<u64>,
32
+ #[magnus::wrap(class = "ChaCha20Blake3::Session", free_immediately, size)]
33
+ struct Session {
34
+ inner: Mutex<chacha20_blake3::Session20>,
40
35
  }
41
36
 
42
- // Safety: all fields are Send. The Mutex<u64> provides interior mutability with
43
- // synchronization, making Stream both Send and Sync (safe for Ractors).
44
- unsafe impl Send for Stream {}
45
- unsafe impl Sync for Stream {}
46
-
47
- fn nonce_for_counter(s: &Stream, counter: u64) -> [u8; 24] {
48
- let mut nonce = [0u8; 24];
49
- nonce[..16].copy_from_slice(&s.nonce_prefix);
50
- nonce[16..].copy_from_slice(&s.counter_base.wrapping_add(counter).to_le_bytes());
51
- nonce
52
- }
37
+ // Safety: Mutex<Session20> provides Send+Sync via interior synchronization.
38
+ unsafe impl Send for Session {}
39
+ unsafe impl Sync for Session {}
53
40
 
54
41
  fn validate_key(ruby: &Ruby, key: &[u8]) -> Result<[u8; KEY_SIZE], Error> {
55
42
  if key.len() != KEY_SIZE {
@@ -71,6 +58,19 @@ fn validate_nonce(ruby: &Ruby, nonce: &[u8]) -> Result<[u8; NONCE_SIZE], Error>
71
58
  Ok(nonce.try_into().unwrap())
72
59
  }
73
60
 
61
+ fn validate_session_nonce(ruby: &Ruby, nonce: &[u8]) -> Result<[u8; SESSION_NONCE_SIZE], Error> {
62
+ if nonce.len() != SESSION_NONCE_SIZE {
63
+ return Err(Error::new(
64
+ ruby.exception_arg_error(),
65
+ format!(
66
+ "session nonce must be exactly {SESSION_NONCE_SIZE} bytes, got {}",
67
+ nonce.len()
68
+ ),
69
+ ));
70
+ }
71
+ Ok(nonce.try_into().unwrap())
72
+ }
73
+
74
74
  fn validate_tag(ruby: &Ruby, tag: &[u8]) -> Result<[u8; TAG_SIZE], Error> {
75
75
  if tag.len() != TAG_SIZE {
76
76
  return Err(Error::new(
@@ -222,46 +222,41 @@ fn cipher_decrypt_detached(
222
222
  Ok(output)
223
223
  }
224
224
 
225
- fn stream_initialize(ruby: &Ruby, rb_key: RString, rb_nonce: RString) -> Result<Stream, Error> {
226
- let (key_arr, nonce_arr) = unsafe {
227
- (validate_key(ruby, rb_key.as_slice())?,
228
- validate_nonce(ruby, rb_nonce.as_slice())?)
225
+ fn session_initialize(
226
+ ruby: &Ruby,
227
+ rb_enc_key: RString,
228
+ rb_auth_key: RString,
229
+ rb_nonce: RString,
230
+ ) -> Result<Session, Error> {
231
+ let (enc_key, auth_key, nonce) = unsafe {
232
+ (
233
+ validate_key(ruby, rb_enc_key.as_slice())?,
234
+ validate_key(ruby, rb_auth_key.as_slice())?,
235
+ validate_session_nonce(ruby, rb_nonce.as_slice())?,
236
+ )
229
237
  };
230
- rb_key.freeze();
238
+ rb_enc_key.freeze();
239
+ rb_auth_key.freeze();
231
240
  rb_nonce.freeze();
232
- Ok(Stream {
233
- cipher: chacha20_blake3::ChaCha20Blake3::new(key_arr),
234
- nonce_prefix: nonce_arr[..16].try_into().unwrap(),
235
- counter_base: u64::from_le_bytes(nonce_arr[16..].try_into().unwrap()),
236
- counter: Mutex::new(0),
241
+ Ok(Session {
242
+ inner: Mutex::new(chacha20_blake3::Session20::new(enc_key, auth_key, nonce)),
237
243
  })
238
244
  }
239
245
 
240
- fn stream_encrypt(ruby: &Ruby, rb_self: &Stream, args: &[Value]) -> Result<RString, Error> {
246
+ fn session_encrypt(ruby: &Ruby, rb_self: &Session, args: &[Value]) -> Result<RString, Error> {
241
247
  let parsed = scan_args::<(RString,), (), (), (), RHash, ()>(args)?;
242
248
  let (rb_plaintext,) = parsed.required;
243
249
  let kw = get_kwargs::<_, (), (Option<RString>,), ()>(parsed.keywords, &[], &["aad"])?;
244
250
  let (opt_aad,) = kw.optional;
245
251
 
246
- // Hold the lock for the entire operation so no two threads can encrypt
247
- // with the same nonce.
248
- let mut counter = rb_self.counter.lock().unwrap();
252
+ let mut session = rb_self.inner.lock().unwrap();
249
253
 
250
254
  let (buf, tag) = unsafe {
251
- let nonce = nonce_for_counter(rb_self, *counter);
252
255
  let mut buf = rb_plaintext.as_slice().to_vec();
253
256
  let aad = opt_aad.as_ref().map_or_else(Vec::new, |s| s.as_slice().to_vec());
254
- let tag = rb_self.cipher.encrypt_in_place_detached(&nonce, &mut buf, &aad);
257
+ let tag = session.encrypt_in_place_detached(&mut buf, &aad);
255
258
  (buf, tag)
256
259
  };
257
- // Advance counter. Overflow would reuse the initial nonce, which is
258
- // catastrophic for a stream cipher.
259
- *counter = counter.checked_add(1).ok_or_else(|| {
260
- Error::new(
261
- ruby.exception_runtime_error(),
262
- "stream counter exhausted (2^64 messages); create a new Stream to continue",
263
- )
264
- })?;
265
260
 
266
261
  let output = ruby.str_buf_new(buf.len() + TAG_SIZE);
267
262
  output.cat(&buf);
@@ -269,18 +264,15 @@ fn stream_encrypt(ruby: &Ruby, rb_self: &Stream, args: &[Value]) -> Result<RStri
269
264
  Ok(output)
270
265
  }
271
266
 
272
- fn stream_decrypt(ruby: &Ruby, rb_self: &Stream, args: &[Value]) -> Result<RString, Error> {
267
+ fn session_decrypt(ruby: &Ruby, rb_self: &Session, args: &[Value]) -> Result<RString, Error> {
273
268
  let parsed = scan_args::<(RString,), (), (), (), RHash, ()>(args)?;
274
269
  let (rb_ciphertext,) = parsed.required;
275
270
  let kw = get_kwargs::<_, (), (Option<RString>,), ()>(parsed.keywords, &[], &["aad"])?;
276
271
  let (opt_aad,) = kw.optional;
277
272
 
278
- // Hold the lock for the entire operation so the counter only advances
279
- // after successful authentication.
280
- let mut counter = rb_self.counter.lock().unwrap();
273
+ let mut session = rb_self.inner.lock().unwrap();
281
274
 
282
275
  let buf = unsafe {
283
- let nonce = nonce_for_counter(rb_self, *counter);
284
276
  let mut buf = rb_ciphertext.as_slice().to_vec();
285
277
  let aad = opt_aad.as_ref().map_or_else(Vec::new, |s| s.as_slice().to_vec());
286
278
  if buf.len() < TAG_SIZE {
@@ -289,27 +281,54 @@ fn stream_decrypt(ruby: &Ruby, rb_self: &Stream, args: &[Value]) -> Result<RStri
289
281
  let tag_start = buf.len() - TAG_SIZE;
290
282
  let tag: [u8; TAG_SIZE] = buf[tag_start..].try_into().unwrap();
291
283
  buf.truncate(tag_start);
292
- rb_self.cipher.decrypt_in_place_detached(&nonce, &mut buf, &tag, &aad)
284
+ session
285
+ .decrypt_in_place_detached(&mut buf, &tag, &aad)
293
286
  .map_err(|_| Error::new(decryption_error(ruby), "decryption failed"))?;
294
287
  buf
295
288
  };
296
- // Advance counter only after successful MAC verification.
297
- *counter = counter.checked_add(1).ok_or_else(|| {
298
- Error::new(
299
- ruby.exception_runtime_error(),
300
- "stream counter exhausted (2^64 messages); create a new Stream to continue",
301
- )
302
- })?;
303
289
 
304
290
  let output = ruby.str_buf_new(buf.len());
305
291
  output.cat(&buf);
306
292
  Ok(output)
307
293
  }
308
294
 
309
- fn stream_message_index(rb_self: &Stream) -> u64 {
310
- *rb_self.counter.lock().unwrap()
295
+ fn session_block_counter(rb_self: &Session) -> u64 {
296
+ rb_self.inner.lock().unwrap().block_counter()
311
297
  }
312
298
 
299
+ fn blake3_derive_key(ruby: &Ruby, args: &[Value]) -> Result<RString, Error> {
300
+ let parsed = scan_args::<(RString, RString), (), (), (), RHash, ()>(args)?;
301
+ let (rb_context, rb_material) = parsed.required;
302
+ let kw = get_kwargs::<_, (), (Option<usize>,), ()>(parsed.keywords, &[], &["length"])?;
303
+ let (opt_length,) = kw.optional;
304
+ let length = opt_length.unwrap_or(32);
305
+
306
+ if length == 0 || length > 65535 {
307
+ return Err(Error::new(
308
+ ruby.exception_arg_error(),
309
+ format!("length must be 1..65535, got {length}"),
310
+ ));
311
+ }
312
+
313
+ // SAFETY: copy context string before any allocation
314
+ let context = unsafe { std::str::from_utf8(rb_context.as_slice()) }
315
+ .map_err(|_| Error::new(ruby.exception_arg_error(), "context must be valid UTF-8"))?
316
+ .to_owned();
317
+
318
+ let mut output_buf = vec![0u8; length];
319
+ unsafe {
320
+ let mut deriver = blake3::Hasher::new_derive_key(&context);
321
+ deriver.update(rb_material.as_slice());
322
+ let mut reader = deriver.finalize_xof();
323
+ reader.fill(&mut output_buf);
324
+ }
325
+
326
+ let output = ruby.str_from_slice(&output_buf);
327
+ output.freeze();
328
+ Ok(output)
329
+ }
330
+
331
+
313
332
  fn generate_key(ruby: &Ruby) -> Result<RString, Error> {
314
333
  let mut key = [0u8; KEY_SIZE];
315
334
  getrandom::getrandom(&mut key).map_err(|e| {
@@ -342,6 +361,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
342
361
 
343
362
  module.const_set("KEY_SIZE", KEY_SIZE as u64)?;
344
363
  module.const_set("NONCE_SIZE", NONCE_SIZE as u64)?;
364
+ module.const_set("SESSION_NONCE_SIZE", SESSION_NONCE_SIZE as u64)?;
345
365
  module.const_set("TAG_SIZE", TAG_SIZE as u64)?;
346
366
 
347
367
  let decryption_error_class =
@@ -357,122 +377,125 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
357
377
  cipher_class.define_method("encrypt_detached", method!(cipher_encrypt_detached, -1))?;
358
378
  cipher_class.define_method("decrypt_detached", method!(cipher_decrypt_detached, -1))?;
359
379
 
360
- let stream_class = module.define_class("Stream", ruby.class_object())?;
361
- stream_class.define_singleton_method("new", function!(stream_initialize, 2))?;
362
- stream_class.define_method("encrypt", method!(stream_encrypt, -1))?;
363
- stream_class.define_method("decrypt", method!(stream_decrypt, -1))?;
364
- stream_class.define_method("message_index", method!(stream_message_index, 0))?;
380
+ let session_class = module.define_class("Session", ruby.class_object())?;
381
+ session_class.define_singleton_method("new", function!(session_initialize, 3))?;
382
+ session_class.define_method("encrypt", method!(session_encrypt, -1))?;
383
+ session_class.define_method("decrypt", method!(session_decrypt, -1))?;
384
+ session_class.define_method("block_counter", method!(session_block_counter, 0))?;
365
385
 
366
386
  module.define_module_function("generate_key", function!(generate_key, 0))?;
367
387
  module.define_module_function("generate_nonce", function!(generate_nonce, 0))?;
388
+ module.define_module_function("derive_key", function!(blake3_derive_key, -1))?;
368
389
 
369
390
  Ok(())
370
391
  }
371
392
 
372
393
  #[cfg(test)]
373
394
  mod tests {
374
- use super::{nonce_for_counter, Mutex, Stream};
375
- use chacha20_blake3::ChaCha20Blake3;
376
-
377
- fn make_stream(key: [u8; 32], nonce: [u8; 24]) -> Stream {
378
- Stream {
379
- cipher: ChaCha20Blake3::new(key),
380
- nonce_prefix: nonce[..16].try_into().unwrap(),
381
- counter_base: u64::from_le_bytes(nonce[16..].try_into().unwrap()),
382
- counter: Mutex::new(0),
383
- }
384
- }
395
+ use chacha20_blake3::{ChaCha20Blake3, Session20};
385
396
 
386
397
  #[test]
387
- fn stream_multi_message_roundtrip() {
388
- let key = [0x11u8; 32];
389
- let nonce = [0x22u8; 24];
390
- let enc = make_stream(key, nonce);
391
- let dec = make_stream(key, nonce);
398
+ fn session_multi_message_roundtrip() {
399
+ let enc_key = [0x11u8; 32];
400
+ let auth_key = [0x22u8; 32];
401
+ let nonce = [0x33u8; 8];
402
+ let mut enc = Session20::new(enc_key, auth_key, nonce);
403
+ let mut dec = Session20::new(enc_key, auth_key, nonce);
392
404
 
393
405
  let messages: &[&[u8]] = &[b"alpha", b"beta", b"gamma"];
394
- let ciphertexts: Vec<Vec<u8>> = messages.iter().map(|m| {
395
- let mut counter = enc.counter.lock().unwrap();
396
- let n = nonce_for_counter(&enc, *counter);
397
- let ct = enc.cipher.encrypt(&n, m, b"");
398
- *counter += 1;
399
- ct
400
- }).collect();
406
+ let ciphertexts: Vec<Vec<u8>> = messages
407
+ .iter()
408
+ .map(|m| enc.encrypt(m, b""))
409
+ .collect();
401
410
 
402
411
  for (ct, expected) in ciphertexts.iter().zip(messages.iter()) {
403
- let mut counter = dec.counter.lock().unwrap();
404
- let n = nonce_for_counter(&dec, *counter);
405
- let pt = dec.cipher.decrypt(&n, ct, b"").expect("decrypt failed");
406
- *counter += 1;
412
+ let pt = dec.decrypt(ct, b"").expect("decrypt failed");
407
413
  assert_eq!(pt.as_slice(), *expected);
408
414
  }
409
415
  }
410
416
 
411
417
  #[test]
412
- fn stream_same_message_different_ciphertext() {
413
- let key = [0x33u8; 32];
414
- let nonce = [0x44u8; 24];
415
- let s = make_stream(key, nonce);
416
-
417
- let mut counter = s.counter.lock().unwrap();
418
- let n1 = nonce_for_counter(&s, *counter);
419
- let ct1 = s.cipher.encrypt(&n1, b"repeat", b"");
420
- *counter += 1;
418
+ fn session_same_message_different_ciphertext() {
419
+ let enc_key = [0x44u8; 32];
420
+ let auth_key = [0x55u8; 32];
421
+ let nonce = [0x66u8; 8];
422
+ let mut session = Session20::new(enc_key, auth_key, nonce);
421
423
 
422
- let n2 = nonce_for_counter(&s, *counter);
423
- let ct2 = s.cipher.encrypt(&n2, b"repeat", b"");
424
+ let ct1 = session.encrypt(b"repeat", b"");
425
+ let ct2 = session.encrypt(b"repeat", b"");
424
426
 
425
427
  assert_ne!(ct1, ct2);
426
428
  }
427
429
 
428
430
  #[test]
429
- fn stream_nonce_suffix_wraps() {
430
- // When counter_base is near u64::MAX, the nonce suffix (counter_base + counter)
431
- // wraps around. This is fine - it's the counter *index* that must never wrap.
432
- let key = [0x55u8; 32];
433
- let mut nonce = [0u8; 24];
434
- nonce[16..].copy_from_slice(&u64::MAX.to_le_bytes());
435
-
436
- let s = make_stream(key, nonce);
437
- *s.counter.lock().unwrap() = 1; // counter_base(MAX) + 1 wraps nonce suffix to 0
438
-
439
- let n = nonce_for_counter(&s, *s.counter.lock().unwrap());
440
- assert_eq!(&n[16..], &0u64.to_le_bytes());
431
+ fn session_block_counter_advances_by_blocks() {
432
+ let enc_key = [0x77u8; 32];
433
+ let auth_key = [0x88u8; 32];
434
+ let nonce = [0x99u8; 8];
435
+ let mut session = Session20::new(enc_key, auth_key, nonce);
436
+
437
+ assert_eq!(session.block_counter(), 0);
438
+
439
+ // 100 bytes = ceil(100/64) = 2 blocks
440
+ session.encrypt(&[0u8; 100], b"");
441
+ assert_eq!(session.block_counter(), 2);
442
+
443
+ // 64 bytes = exactly 1 block
444
+ session.encrypt(&[0u8; 64], b"");
445
+ assert_eq!(session.block_counter(), 3);
446
+
447
+ // 0 bytes = 0 blocks
448
+ session.encrypt(b"", b"");
449
+ assert_eq!(session.block_counter(), 3);
441
450
  }
442
451
 
443
452
  #[test]
444
- fn stream_failed_decrypt_does_not_advance_counter() {
445
- let key = [0x66u8; 32];
446
- let nonce = [0x77u8; 24];
447
- let enc = make_stream(key, nonce);
448
- let dec = make_stream(key, nonce);
449
-
450
- // Encrypt a message
451
- let mut enc_counter = enc.counter.lock().unwrap();
452
- let n = nonce_for_counter(&enc, *enc_counter);
453
- let ct = enc.cipher.encrypt(&n, b"hello", b"");
454
- *enc_counter = enc_counter.wrapping_add(1);
455
- drop(enc_counter);
453
+ fn session_failed_decrypt_does_not_advance_counter() {
454
+ let enc_key = [0xAAu8; 32];
455
+ let auth_key = [0xBBu8; 32];
456
+ let nonce = [0xCCu8; 8];
457
+ let mut enc = Session20::new(enc_key, auth_key, nonce);
458
+ let mut dec = Session20::new(enc_key, auth_key, nonce);
459
+
460
+ let ct = enc.encrypt(b"hello", b"");
456
461
 
457
462
  // Tamper with ciphertext
458
463
  let mut tampered = ct.clone();
459
464
  tampered[0] ^= 0xFF;
460
465
 
461
- // Decrypt should fail
462
- let mut dec_counter = dec.counter.lock().unwrap();
463
- let n = nonce_for_counter(&dec, *dec_counter);
464
- let result = dec.cipher.decrypt(&n, &tampered, b"");
465
- assert!(result.is_err());
466
- // Counter must NOT have advanced
467
- assert_eq!(*dec_counter, 0);
468
-
469
- // Original ciphertext should still decrypt at counter 0
470
- let n = nonce_for_counter(&dec, *dec_counter);
471
- let pt = dec.cipher.decrypt(&n, &ct, b"").expect("decrypt failed");
472
- *dec_counter = dec_counter.wrapping_add(1);
466
+ assert!(dec.decrypt(&tampered, b"").is_err());
467
+ assert_eq!(dec.block_counter(), 0);
468
+
469
+ // Original still decrypts
470
+ let pt = dec.decrypt(&ct, b"").expect("decrypt failed");
473
471
  assert_eq!(pt.as_slice(), b"hello");
474
472
  }
475
473
 
474
+ #[test]
475
+ fn session_with_aad() {
476
+ let enc_key = [0xDDu8; 32];
477
+ let auth_key = [0xEEu8; 32];
478
+ let nonce = [0xFFu8; 8];
479
+ let mut enc = Session20::new(enc_key, auth_key, nonce);
480
+ let mut dec = Session20::new(enc_key, auth_key, nonce);
481
+
482
+ let ct = enc.encrypt(b"payload", b"header");
483
+ let pt = dec.decrypt(&ct, b"header").expect("decrypt with aad failed");
484
+ assert_eq!(pt.as_slice(), b"payload");
485
+ }
486
+
487
+ #[test]
488
+ fn session_wrong_aad_fails() {
489
+ let enc_key = [0x01u8; 32];
490
+ let auth_key = [0x02u8; 32];
491
+ let nonce = [0x03u8; 8];
492
+ let mut enc = Session20::new(enc_key, auth_key, nonce);
493
+ let mut dec = Session20::new(enc_key, auth_key, nonce);
494
+
495
+ let ct = enc.encrypt(b"payload", b"correct");
496
+ assert!(dec.decrypt(&ct, b"wrong").is_err());
497
+ }
498
+
476
499
  #[test]
477
500
  fn round_trip_encrypt_decrypt() {
478
501
  let key = [0x42u8; 32];
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChaCha20Blake3
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -8,7 +8,7 @@ name = "chacha20blake3"
8
8
  crate-type = ["cdylib", "rlib"]
9
9
 
10
10
  [dependencies]
11
- chacha20-blake3 = { git = "https://github.com/skerkour/chacha20-blake3", rev = "9942bf34c77b03896bb1733079256076ca823e58" }
11
+ chacha20-blake3 = "0.10.0"
12
12
  blake3 = { version = "1", features = ["std"] }
13
13
  magnus = "0.8"
14
14
  getrandom = "0.2"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chacha20blake3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
109
  requirements: []
110
- rubygems_version: 4.0.6
110
+ rubygems_version: 4.0.10
111
111
  specification_version: 4
112
112
  summary: 'Fast DJB-family authenticated encryption: ChaCha20 + BLAKE3 MAC'
113
113
  test_files: []