feat!: remove output index from the ciphertext encryption/decryption

BREAKING:

Before: KDF for ChaCha ciphertext had the position index as an argument

After: No such argument provided

Mitigation: Remove the index when generating the decoding key
This commit is contained in:
agureev 2026-06-22 22:25:23 +04:00
parent f1f76247da
commit ed4329103e
4 changed files with 10 additions and 48 deletions

View File

@ -87,13 +87,12 @@ impl EncryptionScheme {
kind: &PrivateAccountKind,
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Ciphertext {
// Plaintext: PrivateAccountKind::HEADER_LEN bytes header || account bytes.
// Both variants produce the same header length — see PrivateAccountKind::to_header_bytes.
let mut buffer = kind.to_header_bytes().to_vec();
buffer.extend_from_slice(&account.to_bytes());
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
Self::symmetric_transform(&mut buffer, shared_secret, commitment);
Ciphertext(buffer)
}
@ -101,43 +100,32 @@ impl EncryptionScheme {
buffer: &mut [u8],
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) {
let key = Self::kdf(shared_secret, commitment, output_index);
let key = Self::kdf(shared_secret, commitment);
let mut cipher = ChaCha20::new(&key.into(), &[0; 12].into());
cipher.apply_keystream(buffer);
}
fn kdf(
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> [u8; 32] {
fn kdf(shared_secret: &SharedSecretKey, commitment: &Commitment) -> [u8; 32] {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"LEE/v0.2/KDF-SHA256/");
bytes.extend_from_slice(&shared_secret.0);
bytes.extend_from_slice(&commitment.to_byte_array());
bytes.extend_from_slice(&output_index.to_le_bytes());
Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()
}
#[cfg(feature = "host")]
#[expect(
clippy::print_stdout,
reason = "This is the current way to debug things. TODO: fix later"
)]
#[must_use]
pub fn decrypt(
ciphertext: &Ciphertext,
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Option<(PrivateAccountKind, Account)> {
use std::io::Cursor;
let mut buffer = ciphertext.0.clone();
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
Self::symmetric_transform(&mut buffer, shared_secret, commitment);
if buffer.len() < PrivateAccountKind::HEADER_LEN {
return None;
@ -148,16 +136,6 @@ impl EncryptionScheme {
let mut cursor = Cursor::new(&buffer[PrivateAccountKind::HEADER_LEN..]);
Account::from_cursor(&mut cursor)
.inspect_err(|err| {
println!(
"Failed to decode {ciphertext:?} \n
with secret {:?} ,\n
commitment {commitment:?} ,\n
and output_index {output_index} ,\n
with error {err:?}",
shared_secret.0
);
})
.ok()
.map(|account| (kind, account))
}
@ -182,7 +160,6 @@ mod tests {
&PrivateAccountKind::Regular(42),
&secret,
&commitment,
0,
);
let pda_ct = EncryptionScheme::encrypt(
&account,
@ -193,7 +170,6 @@ mod tests {
},
&secret,
&commitment,
0,
);
assert_eq!(account_ct.0.len(), pda_ct.0.len());
@ -219,9 +195,9 @@ mod tests {
let kind = PrivateAccountKind::Regular(0);
let commitment = crate::Commitment::new(&AccountId::new([7_u8; 32]), &account);
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment, 0);
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment);
let (decoded_kind, decoded_account) =
EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0)
EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment)
.expect("decryption must succeed with correct shared secret");
assert_eq!(decoded_account, account);
@ -229,7 +205,7 @@ mod tests {
// Wrong shared secret must not decrypt correctly.
let wrong_ss = SharedSecretKey([0_u8; 32]);
let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0);
let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment);
assert!(
bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999),
"wrong shared secret must not produce the correct plaintext"

View File

@ -205,7 +205,6 @@ pub mod tests {
&PrivateAccountKind::Regular(0),
&shared_secret,
&commitment,
2,
);
let encrypted_account_data =
EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone());

View File

@ -30,7 +30,6 @@ pub fn compute_circuit_output(
"Invalid account_identities length"
);
let mut output_index = 0;
for (pos, (account_identity, (pre_state, post_state))) in
account_identities.iter().zip(states_iter).enumerate()
{
@ -68,7 +67,6 @@ pub fn compute_circuit_output(
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
@ -106,7 +104,6 @@ pub fn compute_circuit_output(
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
@ -145,7 +142,6 @@ pub fn compute_circuit_output(
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Regular(*identifier),
@ -192,7 +188,6 @@ pub fn compute_circuit_output(
.expect("PrivatePdaInit position must be in pda_seed_by_position");
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Pda {
@ -240,7 +235,6 @@ pub fn compute_circuit_output(
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
emit_private_output(
&mut output,
&mut output_index,
post_state,
&account_id,
&PrivateAccountKind::Pda {
@ -267,7 +261,6 @@ pub fn compute_circuit_output(
)]
fn emit_private_output(
output: &mut PrivacyPreservingCircuitOutput,
output_index: &mut u32,
post_state: Account,
account_id: &AccountId,
kind: &PrivateAccountKind,
@ -288,7 +281,6 @@ fn emit_private_output(
kind,
shared_secret,
&commitment_post,
*output_index,
);
output.new_commitments.push(commitment_post);
@ -299,9 +291,6 @@ fn emit_private_output(
epk: epk.clone(),
view_tag,
});
*output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
fn compute_update_nullifier_and_set_digest(

View File

@ -52,16 +52,14 @@ fn bench_encryption(c: &mut Criterion) {
let commitment = Commitment::new(&account_id, &account);
let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key);
let kind = PrivateAccountKind::Regular(0_u128);
let output_index: u32 = 0;
let mut g = c.benchmark_group("encryption");
g.sample_size(50).noise_threshold(0.05);
g.bench_function("encrypt", |b| {
b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index));
b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment));
});
let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index);
let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment);
g.bench_function("decrypt", |b| {
b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index));
b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment));
});
g.finish();
}