H-01 Account Identifiers And Encryption Secrets Share Undomain-Separated Hash
Severity: High
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
The account identifier derivation in Utils hashes a secret key using persistentHash over a single-element vector of Bytes<32>. The derivation of the ElGamal secret scalar applies the identical construction to its input and then truncates the result into the Jubjub scalar field. Neither includes a domain separation tag, so both compute the same function of the same input.
The first output is public. ConfidentialFungibleToken stores it as the map key for balances, encryption keys, memo lists and escrow entries, all of which are readable on chain. The second output is the private key protecting every ciphertext held under that identifier. For any 32-byte value used in both roles, the private scalar is a truncation of a value published on chain.
The two secrets arrive through separate witnesses, wit_ConfidentialTokenSK and wit_ConfidentialTokenEK, and no circuit constrains them to differ. The condition for exposure is precise: the 32-byte value used as the encryption secret must never also be a value hashed into an account identifier. A wallet returning the same value from both witnesses satisfies that condition directly, and because the account identifier helper is shared across the access control and token modules, so does a wallet that reuses one secret as an identity in any of those modules and as the encryption secret here. In either case an observer reads the identifier from the ledger, applies the same truncation the circuit applies, and holds the account's encryption secret, which opens the spendable balance, the pending balance, both ciphertexts of every allowance the account granted or received, and every entry in its memo list, requiring no privileged access and no computation beyond one hash truncation.
Consider adding distinct domain separation tags to both derivations, so that a shared input produces unrelated outputs and reuse of a single secret across the two roles becomes harmless. Consider also documenting whether integrating wallets are permitted to derive the account secret and the encryption secret from common key material, and if so, stating that domain separation is what makes the practice safe.
H-01 Account Identifiers And Encryption Secrets Share Undomain-Separated Hash
Severity: High
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
The account identifier derivation in
Utilshashes a secret key usingpersistentHashover a single-element vector ofBytes<32>. The derivation of the ElGamal secret scalar applies the identical construction to its input and then truncates the result into the Jubjub scalar field. Neither includes a domain separation tag, so both compute the same function of the same input.The first output is public.
ConfidentialFungibleTokenstores it as the map key for balances, encryption keys, memo lists and escrow entries, all of which are readable on chain. The second output is the private key protecting every ciphertext held under that identifier. For any 32-byte value used in both roles, the private scalar is a truncation of a value published on chain.The two secrets arrive through separate witnesses,
wit_ConfidentialTokenSKandwit_ConfidentialTokenEK, and no circuit constrains them to differ. The condition for exposure is precise: the 32-byte value used as the encryption secret must never also be a value hashed into an account identifier. A wallet returning the same value from both witnesses satisfies that condition directly, and because the account identifier helper is shared across the access control and token modules, so does a wallet that reuses one secret as an identity in any of those modules and as the encryption secret here. In either case an observer reads the identifier from the ledger, applies the same truncation the circuit applies, and holds the account's encryption secret, which opens the spendable balance, the pending balance, both ciphertexts of every allowance the account granted or received, and every entry in its memo list, requiring no privileged access and no computation beyond one hash truncation.Consider adding distinct domain separation tags to both derivations, so that a shared input produces unrelated outputs and reuse of a single secret across the two roles becomes harmless. Consider also documenting whether integrating wallets are permitted to derive the account secret and the encryption secret from common key material, and if so, stating that domain separation is what makes the practice safe.