Skip to content

Commit 6305106

Browse files
committed
Prepare hashavatar 0.12.0
Expand avatar families and harden identity, encoding, and fuzzing controls.
1 parent bb9e236 commit 6305106

4 files changed

Lines changed: 47 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
and explicitly zeroized BLAKE3 hasher/XOF reader state after digest
4242
derivation.
4343
- Wrapped digest-derived renderer RNG seed copies in `zeroize::Zeroizing` so
44-
the temporary mixed seed is scrubbed after RNG initialization.
44+
the temporary mixed seed is scrubbed immediately after RNG initialization,
45+
and documented `StdRng`'s non-zeroized expanded internal state as a known
46+
residual.
4547
- Wrapped owned RGBA encode buffers and JPEG RGB flattening buffers in RAII
4648
zeroization guards so temporary pixel data is scrubbed during normal returns,
4749
encoder errors, and unwinding panics.

docs/SECURITY_CONTROLS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,15 @@
3737
digest, separate from the lower digest bytes commonly used for direct visual
3838
parameters.
3939
- The temporary 256-bit RNG seed copy is stored in `zeroize::Zeroizing`, so the
40-
digest-derived seed copy is scrubbed when it leaves scope after seeding the
41-
renderer RNG.
40+
digest-derived seed copy is scrubbed immediately after seeding the renderer
41+
RNG.
42+
- The procedural RNG itself is `rand::rngs::StdRng`. Its expanded internal
43+
state is not zeroized on drop because `StdRng` does not currently implement
44+
`ZeroizeOnDrop`. In the default SHA-512 mode, recovering the original
45+
identity from that expanded state would require reversing SHA-512 output,
46+
which is computationally infeasible. High-assurance callers should still
47+
treat this as a known residual and prefer SHA-512 or BLAKE3 over XXH3 for
48+
sensitive identifiers.
4249
- `AvatarIdentity` equality uses constant-time digest comparison.
4350
- `AvatarIdentity` has a redacted `Debug` implementation so accidental
4451
`{:?}` logging does not print the raw digest.

docs/release-notes/RELEASE_NOTES_0.12.0.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ single, asset-free image-generation crate.
6161
BLAKE3 hasher plus XOF reader are explicitly zeroized after digest
6262
derivation.
6363
- Digest-derived renderer RNG seed copies are now wrapped in
64-
`zeroize::Zeroizing`, so the temporary mixed seed is scrubbed after RNG
65-
initialization.
64+
`zeroize::Zeroizing`, so the temporary mixed seed is scrubbed immediately
65+
after RNG initialization. The security controls also document `StdRng`'s
66+
non-zeroized expanded internal state as a known residual.
6667
- Owned RGBA encode buffers and JPEG RGB flattening buffers now use RAII
6768
zeroization guards, so temporary pixel data is scrubbed during normal
6869
returns, encoder errors, and unwinding panics.

src/lib.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4156,16 +4156,24 @@ pub fn render_cat_avatar_for_identity_with_background(
41564156
Ok(render_cat_avatar_with_identity(spec, identity, background))
41574157
}
41584158

4159+
fn seeded_renderer_rng(spec: AvatarSpec, identity: &AvatarIdentity) -> StdRng {
4160+
let mut rng_seed = identity.rng_seed();
4161+
for (index, byte) in spec.seed.to_le_bytes().iter().enumerate() {
4162+
rng_seed[index] ^= *byte;
4163+
}
4164+
let mut rng_seed_value = *rng_seed;
4165+
drop(rng_seed);
4166+
let rng = StdRng::from_seed(rng_seed_value);
4167+
rng_seed_value.zeroize();
4168+
rng
4169+
}
4170+
41594171
fn render_cat_avatar_with_identity(
41604172
spec: AvatarSpec,
41614173
identity: &AvatarIdentity,
41624174
background: AvatarBackground,
41634175
) -> RgbaImage {
4164-
let mut rng_seed = identity.rng_seed();
4165-
for (index, byte) in spec.seed.to_le_bytes().iter().enumerate() {
4166-
rng_seed[index] ^= *byte;
4167-
}
4168-
let mut rng = StdRng::from_seed(*rng_seed);
4176+
let mut rng = seeded_renderer_rng(spec, identity);
41694177
let genome = CatGenome::from_identity(identity, &mut rng);
41704178
let palette = CatPalette::from_genome(&genome);
41714179
let mut image = ImageBuffer::from_pixel(
@@ -9895,6 +9903,25 @@ mod tests {
98959903
assert_eq!(rng_seed.len(), 32);
98969904
}
98979905

9906+
#[test]
9907+
fn renderer_rng_seed_copy_is_zeroized_before_rng_use() {
9908+
let source = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
9909+
let helper = source
9910+
.split("fn seeded_renderer_rng")
9911+
.nth(1)
9912+
.and_then(|after_name| {
9913+
after_name
9914+
.split("fn render_cat_avatar_with_identity")
9915+
.next()
9916+
})
9917+
.expect("seeded renderer rng helper should exist");
9918+
9919+
assert!(helper.contains("let mut rng_seed_value = *rng_seed;"));
9920+
assert!(helper.contains("drop(rng_seed);"));
9921+
assert!(helper.contains("let rng = StdRng::from_seed(rng_seed_value);"));
9922+
assert!(helper.contains("rng_seed_value.zeroize();"));
9923+
}
9924+
98989925
#[test]
98999926
fn avatar_identity_equality_compares_digest_values() {
99009927
let left = valid_identity("alice@example.com");

0 commit comments

Comments
 (0)