11package zkencryption
22
33import (
4- "crypto/sha3"
4+ "crypto/hkdf"
5+ "crypto/sha512"
56 "fmt"
67
78 "filippo.io/edwards25519"
@@ -13,39 +14,62 @@ import (
1314// encoded in little-endian form (matches curve25519-dalek Scalar::as_bytes).
1415const ElGamalSecretKeyLen = 32
1516
16- // elGamalSigningDomain is the domain-separation prefix prepended to the
17- // public seed before signing. It must match b"ElGamalSecretKey" in
18- // solana-zk-sdk.
19- const elGamalSigningDomain = "ElGamalSecretKey"
17+ // SigningDomain is the domain-separation prefix for confidential-balances key
18+ // derivation. It must match HKDF_SALT (b"solana-conf-bal/v1") in solana-zk-sdk
19+ // (derive_confidential_keys_from_ikm): it is both the message prefix the
20+ // signer signs and the HKDF salt.
21+ const SigningDomain = "solana-conf-bal/v1"
2022
21- // minElGamalSeedLen / maxElGamalSeedLen mirror the bounds enforced in
22- // solana-zk-sdk's ElGamalSecretKey::from_seed implementation.
23- const (
24- minElGamalSeedLen = ElGamalSecretKeyLen
25- maxElGamalSeedLen = 65535
26- )
23+ // elgamalInfo is the HKDF info string that scopes the ElGamal expansion,
24+ // matching ELGAMAL_HKDF_INFO in solana-zk-sdk.
25+ const elgamalInfo = "elgamal"
26+
27+ // elgamalMinSeedLen is the minimum seed length accepted by the ElGamal
28+ // derivation. It mirrors MINIMUM_SEED_LEN in solana-zk-sdk's
29+ // derive_confidential_keys_from_ikm (32), which is independent from the AE
30+ // minimum of 16.
31+ const elgamalMinSeedLen = ElGamalSecretKeyLen
32+
33+ // maxSeedLen mirrors the maximum bound enforced by solana-zk-sdk's
34+ // derive_confidential_keys_from_ikm (MAXIMUM_IKM_LEN). It applies to both the
35+ // AE and ElGamal derivations.
36+ const maxSeedLen = 65535
2737
2838// ElGamalSecretKey is a canonical little-endian encoding of a Ristretto/Ed25519
2939// scalar mod ell. It is the Token-2022 confidential-transfer ElGamal private
3040// key; byte-for-byte equivalent to ElGamalSecretKey::as_bytes in solana-zk-sdk.
3141type ElGamalSecretKey [ElGamalSecretKeyLen ]byte
3242
33- // ElGamalSecretKeyFromSeed derives an ElGamal secret key from an entropy seed
34- // by computing Scalar::from_bytes_mod_order_wide(SHA3-512(seed)), matching
35- // curve25519-dalek's Scalar::hash_from_bytes::<Sha3_512>.
36- func ElGamalSecretKeyFromSeed (seed []byte ) (ElGamalSecretKey , error ) {
37- if len (seed ) < minElGamalSeedLen {
43+ // ConfidentialDerivationMessage returns the canonical confidential-balances
44+ // derivation message, b"solana-conf-bal/v1" || publicSeed. Mirrors
45+ // confidential_derivation_message in solana-zk-sdk; this is the exact message
46+ // a Signer must sign to derive confidential-balances keys.
47+ func ConfidentialDerivationMessage (publicSeed []byte ) []byte {
48+ msg := make ([]byte , 0 , len (SigningDomain )+ len (publicSeed ))
49+ msg = append (msg , SigningDomain ... )
50+ msg = append (msg , publicSeed ... )
51+ return msg
52+ }
53+
54+ // deriveElGamalSecretKey implements the solana-conf-bal/v1 derivation:
55+ // HKDF-SHA512(salt=SigningDomain, ikm).expand(info="elgamal", 64) reduced via
56+ // Scalar::from_bytes_mod_order_wide, matching derive_confidential_keys_from_ikm.
57+ func deriveElGamalSecretKey (ikm []byte ) (ElGamalSecretKey , error ) {
58+ if len (ikm ) < elgamalMinSeedLen {
3859 return ElGamalSecretKey {}, ErrSeedTooShort
3960 }
40- if len (seed ) > maxElGamalSeedLen {
61+ if len (ikm ) > maxSeedLen {
4162 return ElGamalSecretKey {}, ErrSeedTooLong
4263 }
4364
44- h := sha3 .Sum512 (seed )
45- // SetUniformBytes only errors on wrong input length; Sum512 always
46- // returns 64 bytes, so this branch is unreachable in practice but kept
47- // to avoid an implicit panic if the upstream contract ever changes.
48- s , err := edwards25519 .NewScalar ().SetUniformBytes (h [:])
65+ wide , err := hkdf .Key (sha512 .New , ikm , []byte (SigningDomain ), elgamalInfo , 64 )
66+ if err != nil {
67+ return ElGamalSecretKey {}, fmt .Errorf ("zkencryption: HKDF expand elgamal: %w" , err )
68+ }
69+ // SetUniformBytes performs Scalar::from_bytes_mod_order_wide on 64 bytes.
70+ s , err := edwards25519 .NewScalar ().SetUniformBytes (wide )
71+ // wide holds expanded secret material; scrub it before it leaves scope.
72+ clear (wide )
4973 if err != nil {
5074 return ElGamalSecretKey {}, ErrInvalidScalarEncoding
5175 }
@@ -55,38 +79,40 @@ func ElGamalSecretKeyFromSeed(seed []byte) (ElGamalSecretKey, error) {
5579 return out , nil
5680}
5781
82+ // ElGamalSecretKeyFromSeed derives an ElGamal secret key from raw input key
83+ // material, matching derive_confidential_keys_from_ikm in solana-zk-sdk.
84+ func ElGamalSecretKeyFromSeed (seed []byte ) (ElGamalSecretKey , error ) {
85+ return deriveElGamalSecretKey (seed )
86+ }
87+
5888// ElGamalSecretKeyFromSignature derives an ElGamal secret key from an ed25519
59- // signature by using SHA3-512(signature) as the seed. Mirrors
60- // ElGamalSecretKey::seed_from_signature + from_seed in solana-zk-sdk.
89+ // signature over ConfidentialDerivationMessage. Mirrors
90+ // derive_confidential_keys_from_signature in solana-zk-sdk. An all-zero
91+ // (default) signature is rejected, matching the Rust implementation.
6192func ElGamalSecretKeyFromSignature (sig solana.Signature ) (ElGamalSecretKey , error ) {
62- h := sha3 .Sum512 (sig [:])
63- return ElGamalSecretKeyFromSeed (h [:])
93+ if sig == (solana.Signature {}) {
94+ return ElGamalSecretKey {}, ErrDefaultSignature
95+ }
96+ return deriveElGamalSecretKey (sig [:])
6497}
6598
6699// ElGamalSecretKeyFromSigner deterministically derives an ElGamal secret key
67100// from a Solana signer and a public seed. The signer signs
68- // b"ElGamalSecretKey " || publicSeed; the signature is hashed with SHA3-512
69- // and fed into ElGamalSecretKeyFromSeed. An all-zero (default) signature is
70- // rejected to match the Rust implementation .
101+ // b"solana-conf-bal/v1 " || publicSeed (see ConfidentialDerivationMessage); the
102+ // signature is fed through the HKDF-SHA512 solana-conf-bal/v1 derivation. The
103+ // all-zero signature rejection lives in ElGamalSecretKeyFromSignature .
71104func ElGamalSecretKeyFromSigner (signer Signer , publicSeed []byte ) (ElGamalSecretKey , error ) {
72- msg := make ([]byte , 0 , len (elGamalSigningDomain )+ len (publicSeed ))
73- msg = append (msg , elGamalSigningDomain ... )
74- msg = append (msg , publicSeed ... )
75-
76- sig , err := signer .Sign (msg )
105+ sig , err := signer .Sign (ConfidentialDerivationMessage (publicSeed ))
77106 if err != nil {
78- return ElGamalSecretKey {}, fmt .Errorf ("zkencryption: sign ElGamalSecretKey public seed: %w" , err )
79- }
80- if sig == (solana.Signature {}) {
81- return ElGamalSecretKey {}, ErrDefaultSignature
107+ return ElGamalSecretKey {}, fmt .Errorf ("zkencryption: sign confidential-balances public seed: %w" , err )
82108 }
83109 return ElGamalSecretKeyFromSignature (sig )
84110}
85111
86112// ElGamalSecretKeyFromSeedPhraseAndPassphrase derives an ElGamal secret key
87- // from a BIP39 mnemonic and an optional passphrase, matching
88- // solana_seed_phrase's PBKDF2-HMAC-SHA512 derivation. Solana does not
89- // validate the mnemonic checksum at this layer, and neither do we .
113+ // from a BIP39 mnemonic and an optional passphrase using the standard BIP39
114+ // PBKDF2-HMAC-SHA512 seed derivation (2048 iterations, 64-byte output). The
115+ // seed is used directly as the HKDF input key material .
90116func ElGamalSecretKeyFromSeedPhraseAndPassphrase (mnemonic , passphrase string ) (ElGamalSecretKey , error ) {
91117 return ElGamalSecretKeyFromSeed (bip39 .NewSeed (mnemonic , passphrase ))
92118}
0 commit comments