diff --git a/Cargo.lock b/Cargo.lock index e31642ce..8df9808a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,7 +273,7 @@ dependencies = [ "ark-std 0.4.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "sha2", ] @@ -293,7 +293,7 @@ dependencies = [ "ark-std 0.5.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "fnv", "merlin", "sha2", @@ -359,7 +359,7 @@ dependencies = [ "ark-serialize 0.4.2", "ark-std 0.4.0", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint 0.4.6", "num-traits", @@ -379,7 +379,7 @@ dependencies = [ "ark-serialize 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint 0.4.6", @@ -541,7 +541,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive 0.4.2", "ark-std 0.4.0", - "digest", + "digest 0.10.7", "num-bigint 0.4.6", ] @@ -554,7 +554,7 @@ dependencies = [ "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint 0.4.6", ] @@ -1118,7 +1118,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1126,7 +1126,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -1164,9 +1164,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "bitvec" @@ -1186,7 +1186,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1221,7 +1221,7 @@ checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64", - "bitflags 2.11.1", + "bitflags 2.12.1", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -1447,14 +1447,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -1617,6 +1617,12 @@ dependencies = [ "lee_core", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -1751,6 +1757,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "0.4.3" @@ -2018,7 +2030,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -2040,6 +2054,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2049,7 +2072,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "serde", @@ -2189,11 +2212,21 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -2312,11 +2345,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", +] + [[package]] name = "directories" version = "6.0.0" @@ -2410,7 +2453,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34" dependencies = [ - "digest", + "digest 0.10.7", "futures", "rand 0.8.6", "reqwest", @@ -2442,13 +2485,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", - "digest", + "der 0.7.10", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -2457,7 +2500,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature", ] @@ -2519,12 +2562,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array 0.14.7", "group", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -3037,7 +3080,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "log", "managed", @@ -3068,9 +3111,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" dependencies = [ "rustversion", "serde_core", @@ -3429,7 +3472,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -3551,6 +3594,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ + "ctutils", "typenum", ] @@ -3572,9 +3616,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -4226,9 +4270,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", @@ -4239,9 +4283,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -4510,6 +4554,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "key_protocol" version = "0.1.0" @@ -4526,6 +4590,7 @@ dependencies = [ "k256", "lee", "lee_core", + "ml-kem", "rand 0.8.6", "serde", "sha2", @@ -4634,7 +4699,7 @@ dependencies = [ "bytemuck", "bytesize", "chacha20", - "k256", + "ml-kem", "risc0-zkvm", "serde", "serde_json", @@ -5335,9 +5400,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -5379,9 +5444,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "logos-blockchain-blend-crypto" @@ -5427,7 +5492,7 @@ version = "0.1.2" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" dependencies = [ "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-blend-crypto", "logos-blockchain-groth16", @@ -5605,7 +5670,7 @@ dependencies = [ "ark-ff 0.4.2", "ark-groth16 0.4.0", "ark-serialize 0.4.2", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "num-bigint 0.4.6", "serde", @@ -5641,7 +5706,7 @@ dependencies = [ "async-trait", "bytes", "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-groth16", "logos-blockchain-key-management-system-macros", @@ -6254,7 +6319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", - "keccak", + "keccak 0.1.6", "rand_core 0.6.4", "zeroize", ] @@ -6265,7 +6330,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -6317,6 +6382,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.15" @@ -6508,7 +6598,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "libc", "log", "netlink-packet-core 0.8.1", @@ -6568,10 +6658,10 @@ dependencies = [ "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-serialize 0.4.2", - "digest", + "digest 0.10.7", "generic-array 0.14.7", "hex", - "keccak", + "keccak 0.1.6", "log", "rand 0.8.6", "zeroize", @@ -6583,7 +6673,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", @@ -6840,7 +6930,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -7158,9 +7248,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -7169,8 +7259,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -7452,7 +7552,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -7912,7 +8012,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -8127,7 +8227,7 @@ dependencies = [ "anyhow", "bytemuck", "cfg-if", - "keccak", + "keccak 0.1.6", "liblzma", "paste", "rayon", @@ -8310,7 +8410,7 @@ dependencies = [ "borsh", "bytemuck", "cfg-if", - "digest", + "digest 0.10.7", "ff", "hex", "hex-literal 0.4.1", @@ -8349,7 +8449,7 @@ dependencies = [ "gdbstub_arch", "gimli", "hex", - "keccak", + "keccak 0.1.6", "lazy-regex", "num-bigint 0.4.6", "num-traits", @@ -8413,9 +8513,9 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "rpassword" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -8468,16 +8568,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -8583,7 +8683,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -8607,9 +8707,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -8793,9 +8893,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array 0.14.7", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -8807,7 +8907,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -9178,7 +9278,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -9189,7 +9289,17 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", ] [[package]] @@ -9207,6 +9317,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -9223,7 +9339,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -9322,7 +9438,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -9481,7 +9607,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -9492,7 +9618,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -10109,7 +10235,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", @@ -10372,9 +10498,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typetag" @@ -10441,9 +10567,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -10599,9 +10725,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -10898,7 +11024,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -11432,7 +11558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap 2.14.0", "log", "serde", @@ -11588,18 +11714,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7a6a37ad..2cfbe059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,6 +159,7 @@ k256 = { version = "0.13.3", features = [ "serde", "pem", ] } +ml-kem = { version = "0.3", features = ["hazmat"] } elliptic-curve = { version = "0.13.8", features = ["arithmetic"] } actix-web = { version = "4.13.0", default-features = false, features = [ "macros", diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index a3e841c8..046f21bb 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 750294f7..cae6ed4e 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 20360688..cebe1042 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/bridge.bin b/artifacts/program_methods/bridge.bin index bd30da09..4c224cb4 100644 Binary files a/artifacts/program_methods/bridge.bin and b/artifacts/program_methods/bridge.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 452784ba..1124913f 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/program_methods/faucet.bin index 4ad52fb9..ca45b686 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/program_methods/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index b15aa385..118f19d3 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index e28ff793..f3ecb0e9 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index f1317ebc..66f6d5b6 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 074dda6b..a36fbbc8 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin index 5cfa89d4..7628b459 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 3ead7c7b..3884195d 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index 26f4df47..6afda3a5 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 514a39f4..dd2db03f 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index dc0a4992..58291896 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index e2639c45..bb2feabb 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 99d1301c..b0f3a67c 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index fd0f52ef..5cda6717 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 31689af4..898cea24 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 121a7a29..a74d931d 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin index faa150dd..8effbf85 100644 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and b/artifacts/test_program_methods/faucet_chain_caller.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 1e4e74f8..e4aa09dc 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 2ae1e845..f0d031b8 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 20878083..589bc620 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 2a1358a0..8c0d1350 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_injector.bin b/artifacts/test_program_methods/malicious_injector.bin index 0f3cbfd7..8bcccb4e 100644 Binary files a/artifacts/test_program_methods/malicious_injector.bin and b/artifacts/test_program_methods/malicious_injector.bin differ diff --git a/artifacts/test_program_methods/malicious_launderer.bin b/artifacts/test_program_methods/malicious_launderer.bin index e8932cab..70392d9c 100644 Binary files a/artifacts/test_program_methods/malicious_launderer.bin and b/artifacts/test_program_methods/malicious_launderer.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index 91f88c58..62b8af74 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 5064f754..c4b115ab 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 11a34838..ae599f60 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 1f47a878..1435dea9 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 4d5ddb34..bc479a80 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index 4b59b8b7..e0d52639 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index 6ac0067d..f34373ff 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin index 03e85c2e..9a04a29c 100644 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin index 8f5641e6..33e38f00 100644 Binary files a/artifacts/test_program_methods/pda_spend_proxy.bin and b/artifacts/test_program_methods/pda_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 4f52bcdf..65879893 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index e3370b50..7652eaa7 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index b49681a3..81d9a7e1 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 4d5bb4d8..ef9b3006 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index 7606f56c..ba50eebd 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index ebc6e2ae..32a8b581 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index d2e203a5..85a38041 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index 1958ee4d..99f379c5 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/docs/LEZ testnet v0.1 tutorials/token-transfer.md b/docs/LEZ testnet v0.1 tutorials/token-transfer.md index 3a1ef43f..e3d04663 100644 --- a/docs/LEZ testnet v0.1 tutorials/token-transfer.md +++ b/docs/LEZ testnet v0.1 tutorials/token-transfer.md @@ -155,7 +155,7 @@ wallet account new private # Output: Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 -With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` > [!Tip] @@ -231,19 +231,29 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /1 With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e -With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` -> [!Tip] -> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account. +> [!Important] +> The VPK is now a 1184-byte ML-KEM-768 encapsulation key — too large to copy-paste into a command. +> The recommended workflow is: +> +> **Recipient:** export both keys to a single file and send the file to the sender (e.g. as an email attachment): +> ```bash +> wallet account show-keys --account-id Private/ > recipient.keys +> # Send recipient.keys to the sender out-of-band +> ``` +> The file contains two lines: the npk (hex) on line 1, the vpk (hex) on line 2. +> +> **Sender:** reference the received file with `--to-keys`: -### b. Send 3 tokens using the recipient’s npk and vpk +### b. Send 3 tokens using the recipient’s keys file ```bash +# The sender has received recipient.keys from the recipient out-of-band wallet auth-transfer send \ --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ - --to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \ - --to-vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \ + --to-keys recipient.keys \ --amount 3 ``` @@ -270,18 +280,19 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /2 With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 -With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` Alice shares the `npk` and `vpk` values with Bob and Charlie out of band. ### b. Bob sends 10 tokens to Alice using identifier 1 +Bob uses the received `alice.keys` file: + ```bash wallet auth-transfer send \ --from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 1 \ --amount 10 ``` @@ -291,8 +302,7 @@ wallet auth-transfer send \ ```bash wallet auth-transfer send \ --from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 2 \ --amount 5 ``` diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index b7a747f1..9f953001 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -132,6 +132,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -158,6 +159,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -530,6 +532,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_a_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; @@ -551,6 +554,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_b_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 5b4a6c0b..9e37061b 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -260,6 +260,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -487,6 +488,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -598,6 +600,7 @@ async fn burn_via_ata_private_owner() -> Result<()> { to: Some(public_mention(holder_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index ad3e5b18..9eea9b04 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,8 +11,9 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata, - encryption::shared_key_derivation::Secp256k1Point, + InputAccountIdentity, NullifierPublicKey, + account::AccountWithMetadata, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use log::info; use sequencer_service_rpc::RpcClient as _; @@ -38,6 +39,7 @@ async fn private_transfer_to_owned_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -71,13 +73,14 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: private_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -127,6 +130,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to: Some(public_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -189,7 +193,8 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -239,6 +244,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -274,14 +280,15 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let from: AccountId = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: public_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -351,7 +358,8 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -452,6 +460,7 @@ async fn private_transfer_using_from_label() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -545,7 +554,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { }; let npk_hex = hex::encode(npk.0); - let vpk_hex = hex::encode(vpk.0); + let vpk_hex = hex::encode(vpk.to_bytes()); let identifier_1 = 1_u128; let identifier_2 = 2_u128; @@ -560,6 +569,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex.clone()), to_vpk: Some(vpk_hex.clone()), + to_keys: None, to_identifier: Some(identifier_1), amount: 100, }), @@ -573,6 +583,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex), to_vpk: Some(vpk_hex), + to_keys: None, to_identifier: Some(identifier_2), amount: 200, }), @@ -654,8 +665,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let auth_transfer_program_id = Program::authenticated_transfer_program().id(); let nsk: lee_core::NullifierSecretKey = [3; 32]; let npk = NullifierPublicKey::from(&nsk); - let vpk = Secp256k1Point::from_scalar([4; 32]); - let ssk = SharedSecretKey::new([55; 32], &vpk); + let _vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let ssk = SharedSecretKey([55_u8; 32]); + let _epk = EphemeralPublicKey(vec![55_u8; 1088]); let attacker_vault_id = { let seed = vault_core::compute_vault_seed(attacker_id); AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337) diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index cd19e55d..d00b7964 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -25,6 +25,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -83,6 +84,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { to: Some(public_mention(new_persistent_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -120,6 +122,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000_000, }); @@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -192,6 +196,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -274,6 +279,7 @@ async fn successful_transfer_using_from_label() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -319,6 +325,7 @@ async fn successful_transfer_using_to_label() -> Result<()> { to: Some(CliAccountMention::Label(label)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/indexer_ffi_state_consistency.rs b/integration_tests/tests/indexer_ffi_state_consistency.rs index 165b332c..f84a3790 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency.rs @@ -30,6 +30,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { to: Some(public_mention(ctx.ctx().existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); @@ -67,6 +68,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); diff --git a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs index bce90dfb..34d5a4d7 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs @@ -46,6 +46,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> { to: Some(to_label.into()), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); diff --git a/integration_tests/tests/indexer_state_consistency.rs b/integration_tests/tests/indexer_state_consistency.rs index 7afbf5ae..e87927bc 100644 --- a/integration_tests/tests/indexer_state_consistency.rs +++ b/integration_tests/tests/indexer_state_consistency.rs @@ -25,6 +25,7 @@ async fn indexer_state_consistency() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -60,6 +61,7 @@ async fn indexer_state_consistency() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/indexer_state_consistency_with_labels.rs b/integration_tests/tests/indexer_state_consistency_with_labels.rs index a9817933..5f561d6f 100644 --- a/integration_tests/tests/indexer_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_state_consistency_with_labels.rs @@ -43,6 +43,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { to: Some(CliAccountMention::Label(to_label)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index 59628798..01af23cd 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -71,7 +71,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode( + to_account.key_chain.viewing_public_key.to_bytes(), + )), + to_keys: None, to_identifier: Some(to_account.kind.identifier()), amount: 100, }); @@ -147,6 +150,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -158,6 +162,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 101, }); @@ -197,6 +202,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id3)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 102, }); @@ -208,6 +214,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 103, }); @@ -268,6 +275,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 10, }); @@ -278,6 +286,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 11, }); diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index bc6de164..ea7cafab 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -64,9 +64,9 @@ async fn fund_private_pda( let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) .context("failed to serialize auth_transfer instruction")?; @@ -272,10 +272,10 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { // Fresh recipients — hardcoded npks not in any wallet. let recipient_npk_0 = NullifierPublicKey([0xAA; 32]); - let recipient_vpk_0 = ViewingPublicKey::from_scalar(recipient_npk_0.0); + let recipient_vpk_0 = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let recipient_npk_1 = NullifierPublicKey([0xBB; 32]); - let recipient_vpk_1 = ViewingPublicKey::from_scalar(recipient_npk_1.0); + let recipient_vpk_1 = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]); let amount_spend_0: u128 = 13; let amount_spend_1: u128 = 37; diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index aa1077ff..39bdd36c 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -107,8 +107,11 @@ async fn group_invite_join_key_agreement() -> Result<()> { .key_chain() .sealing_secret_key() .context("Sealing key not found")?; - let sealing_pk = - key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); + let sealing_pk = key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( + lee_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z) + .to_bytes() + .to_vec(), + ); let holder = ctx .wallet() @@ -204,6 +207,7 @@ async fn fund_shared_account_from_public() -> Result<()> { to: Some(private_mention(shared_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: None, amount: 100, }); diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index a5bd0ac9..b0c569e8 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -133,6 +133,7 @@ async fn create_and_transfer_public_token() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -223,6 +224,7 @@ async fn create_and_transfer_public_token() -> Result<()> { holder: Some(public_mention(recipient_account_id)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount, }; @@ -365,6 +367,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -554,6 +557,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(public_mention(recipient_account_id_public)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_public, }; @@ -601,6 +605,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(private_mention(recipient_account_id_private)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_private, }; @@ -740,6 +745,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -868,6 +874,7 @@ async fn shielded_token_transfer() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -991,6 +998,7 @@ async fn deshielded_token_transfer() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -1124,7 +1132,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { definition: private_mention(definition_account_id), holder: None, holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), - holder_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)), + holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.to_bytes())), + holder_keys: None, holder_identifier: Some(holder_identifier), amount: mint_amount, }; @@ -1323,6 +1332,7 @@ async fn transfer_token_using_from_label() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index ce15a63c..daf52609 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -256,8 +256,7 @@ pub async fn tps_test() -> Result<()> { fn build_privacy_transaction() -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); let sender_nsk = [1; 32]; - let sender_vsk = [99; 32]; - let sender_vpk = ViewingPublicKey::from_scalar(sender_vsk); + let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]); let sender_npk = NullifierPublicKey::from(&sender_nsk); let sender_pre = AccountWithMetadata::new( Account { @@ -270,8 +269,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&sender_npk, 0), ); let recipient_nsk = [2; 32]; - let recipient_vsk = [99; 32]; - let recipient_vpk = ViewingPublicKey::from_scalar(recipient_vsk); + let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]); let recipient_npk = NullifierPublicKey::from(&recipient_nsk); let recipient_pre = AccountWithMetadata::new( Account::default(), @@ -279,13 +277,13 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&recipient_npk, 0), ); - let eph_holder_from = EphemeralKeyHolder::new(&sender_npk); - let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_vpk); - let sender_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk); + let sender_ss = eph_holder_from.calculate_shared_secret_sender(); + let sender_epk = eph_holder_from.ephemeral_public_key().clone(); - let eph_holder_to = EphemeralKeyHolder::new(&recipient_npk); - let recipient_ss = eph_holder_to.calculate_shared_secret_sender(&recipient_vpk); - let recipient_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk); + let recipient_ss = eph_holder_to.calculate_shared_secret_sender(); + let recipient_epk = eph_holder_to.ephemeral_public_key().clone(); let balance_to_move: u128 = 1; let proof: MembershipProof = ( diff --git a/lee/key_protocol/Cargo.toml b/lee/key_protocol/Cargo.toml index bb8fe57d..06c244c7 100644 --- a/lee/key_protocol/Cargo.toml +++ b/lee/key_protocol/Cargo.toml @@ -19,6 +19,7 @@ common.workspace = true anyhow.workspace = true serde.workspace = true k256.workspace = true +ml-kem.workspace = true sha2.workspace = true rand.workspace = true hex.workspace = true diff --git a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs index f6e9a270..a53ae47c 100644 --- a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -1,53 +1,61 @@ use lee_core::{ - NullifierPublicKey, SharedSecretKey, - encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, + SharedSecretKey, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; -use rand::{RngCore as _, rngs::OsRng}; -use sha2::Digest as _; -#[derive(Debug)] -/// Ephemeral secret key holder. Non-clonable as intended for one-time use. Produces ephemeral -/// public keys. Can produce shared secret for sender. +/// Ephemeral key holder for the sender side of a KEM-based shared-secret exchange. +/// +/// Non-clonable as intended for one-time use: construction encapsulates once and +/// stores both the shared secret and the ciphertext (`EphemeralPublicKey`) that must +/// be sent to the receiver. pub struct EphemeralKeyHolder { - ephemeral_secret_key: EphemeralSecretKey, + shared_secret: SharedSecretKey, + ephemeral_public_key: EphemeralPublicKey, +} + +// SharedSecretKey does not implement Debug (intentional — leaking key material via +// debug output would be a security risk). We implement Debug manually here, redacting the +// shared secret while still allowing the ephemeral public key (KEM ciphertext) to be inspected. +impl std::fmt::Debug for EphemeralKeyHolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EphemeralKeyHolder") + .field("shared_secret", &"") + .field("ephemeral_public_key", &self.ephemeral_public_key) + .finish() + } } impl EphemeralKeyHolder { #[must_use] - pub fn new(receiver_nullifier_public_key: &NullifierPublicKey) -> Self { - let mut nonce_bytes = [0; 16]; - OsRng.fill_bytes(&mut nonce_bytes); - let mut hasher = sha2::Sha256::new(); - hasher.update(receiver_nullifier_public_key); - hasher.update(nonce_bytes); - + pub fn new(receiver_viewing_public_key: &ViewingPublicKey) -> Self { + let (shared_secret, ephemeral_public_key) = + SharedSecretKey::encapsulate(receiver_viewing_public_key); Self { - ephemeral_secret_key: hasher.finalize().into(), + shared_secret, + ephemeral_public_key, } } + /// Returns the KEM ciphertext to be transmitted to the receiver as the `EphemeralPublicKey`. #[must_use] - pub fn generate_ephemeral_public_key(&self) -> EphemeralPublicKey { - EphemeralPublicKey::from_scalar(self.ephemeral_secret_key) + pub const fn ephemeral_public_key(&self) -> &EphemeralPublicKey { + &self.ephemeral_public_key } + /// Returns the sender-side shared secret (established at construction time). #[must_use] - pub fn calculate_shared_secret_sender( - &self, - receiver_viewing_public_key: &ViewingPublicKey, - ) -> SharedSecretKey { - SharedSecretKey::new(self.ephemeral_secret_key, receiver_viewing_public_key) + pub const fn calculate_shared_secret_sender(&self) -> SharedSecretKey { + self.shared_secret } } +/// Encapsulates a fresh shared secret toward `vpk` and returns `(shared_secret, ciphertext)`. +/// +/// Used when the local side is acting as an "ephemeral receiver" — i.e. generating a +/// one-sided encryption that only the holder of the VSK can decrypt. #[must_use] pub fn produce_one_sided_shared_secret_receiver( vpk: &ViewingPublicKey, ) -> (SharedSecretKey, EphemeralPublicKey) { - let mut esk = [0; 32]; - OsRng.fill_bytes(&mut esk); - ( - SharedSecretKey::new(esk, vpk), - EphemeralPublicKey::from_scalar(esk), - ) + SharedSecretKey::encapsulate(vpk) } diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index a989f886..7fb24713 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -1,44 +1,39 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use lee_core::{ SharedSecretKey, - encryption::{Scalar, shared_key_derivation::Secp256k1Point}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{PdaSeed, ProgramId}, }; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; -use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey}; +use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}; /// Public key used to seal a `GroupKeyHolder` for distribution to a recipient. /// -/// Wraps a secp256k1 point but is a distinct type from `ViewingPublicKey` to enforce -/// key separation: viewing keys encrypt account state, sealing keys encrypt the GMS -/// for off-chain distribution. -pub struct SealingPublicKey(Secp256k1Point); +/// Wraps the ML-KEM-768 encapsulation key bytes (1184 bytes). Distinct from +/// `ViewingPublicKey` to enforce key separation: viewing keys encrypt account state, +/// sealing keys encrypt the GMS for off-chain distribution. +pub struct SealingPublicKey(Vec); impl SealingPublicKey { - /// Derive the sealing public key from a secret scalar. - #[must_use] - pub fn from_scalar(scalar: Scalar) -> Self { - Self(Secp256k1Point::from_scalar(scalar)) - } - - /// Construct from raw serialized bytes (e.g. received from another wallet). + /// Construct from raw serialized encapsulation-key bytes (e.g. received from another wallet). #[must_use] pub const fn from_bytes(bytes: Vec) -> Self { - Self(Secp256k1Point(bytes)) + Self(bytes) } /// Returns the raw bytes for display or transmission. #[must_use] pub fn to_bytes(&self) -> &[u8] { - &self.0.0 + &self.0 } } /// Secret key used to unseal a `GroupKeyHolder` received from another member. -pub type SealingSecretKey = Scalar; +/// Holds the two 32-byte FIPS 203 seed halves `d` and `z`. +pub type SealingSecretKey = ViewingSecretKey; /// Manages shared viewing keys for a group of controllers owning private PDAs. /// @@ -153,18 +148,17 @@ impl GroupKeyHolder { /// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`]. /// - /// Uses an ephemeral ECDH key exchange to derive a shared secret, then AES-256-GCM - /// to encrypt the payload. The returned bytes are - /// `ephemeral_pubkey (33) || nonce (12) || ciphertext+tag (48)` = 93 bytes. + /// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt + /// the payload. The returned bytes are + /// `kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48)` = 1148 bytes. /// - /// Each call generates a fresh ephemeral key, so two seals of the same holder produce + /// Each call generates a fresh KEM encapsulation, so two seals of the same holder produce /// different ciphertexts. #[must_use] pub fn seal_for(&self, recipient_key: &SealingPublicKey) -> Vec { - let mut ephemeral_scalar: Scalar = [0_u8; 32]; - OsRng.fill_bytes(&mut ephemeral_scalar); - let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar); - let shared = SharedSecretKey::new(ephemeral_scalar, &recipient_key.0); + let sealing_key = ViewingPublicKey::from_bytes(recipient_key.0.clone()) + .expect("key_protocol::group_key_holder::GroupKeyHolder::seal_for: SealingPublicKey must be a valid ML-KEM-768 encapsulation key"); + let (shared, kem_ct) = SharedSecretKey::encapsulate(&sealing_key); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -176,12 +170,12 @@ impl GroupKeyHolder { .encrypt(&nonce, self.gms.as_ref()) .expect("AES-GCM encryption should not fail with valid key/nonce"); - let capacity = 33_usize + let capacity = 1088_usize .checked_add(12) .and_then(|n| n.checked_add(ciphertext.len())) .expect("seal capacity overflow"); let mut out = Vec::with_capacity(capacity); - out.extend_from_slice(&ephemeral_pubkey.0); + out.extend_from_slice(&kem_ct.0); out.extend_from_slice(&nonce_bytes); out.extend_from_slice(&ciphertext); out @@ -189,20 +183,24 @@ impl GroupKeyHolder { /// Decrypts a sealed `GroupKeyHolder` using the recipient's [`SealingSecretKey`]. /// - /// Returns `Err` if the ciphertext is too short, the ECDH point is invalid, or the - /// AES-GCM authentication tag doesn't verify (wrong key or tampered data). - pub fn unseal(sealed: &[u8], own_key: SealingSecretKey) -> Result { - const HEADER_LEN: usize = 33 + 12; + /// Returns `Err` if the ciphertext is too short or the AES-GCM authentication tag + /// doesn't verify (wrong key or tampered data). + pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { + // kem_ciphertext (1088) + nonce (12) = header, then AES-GCM tag (16) minimum. + const KEM_CT_LEN: usize = 1088; + const HEADER_LEN: usize = KEM_CT_LEN + 12; const MIN_LEN: usize = HEADER_LEN + 16; + if sealed.len() < MIN_LEN { return Err(SealError::TooShort); } - // MIN_LEN (61) > HEADER_LEN (45), so all slicing below is in bounds. - let ephemeral_pubkey = Secp256k1Point(sealed[..33].to_vec()); - let nonce = aes_gcm::Nonce::from_slice(&sealed[33..HEADER_LEN]); + + let kem_ct = EphemeralPublicKey(sealed[..KEM_CT_LEN].to_vec()); + let nonce = aes_gcm::Nonce::from_slice(&sealed[KEM_CT_LEN..HEADER_LEN]); let ciphertext = &sealed[HEADER_LEN..]; - let shared = SharedSecretKey::new(own_key, &ephemeral_pubkey); + let shared = SharedSecretKey::decapsulate(&kem_ct, &own_key.d, &own_key.z) + .expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: KEM_CT_LEN guarantees exactly 1088 bytes"); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -219,7 +217,7 @@ impl GroupKeyHolder { Ok(Self::from_gms(gms)) } - /// Derives an AES-256 key from the ECDH shared secret via SHA-256 with a domain prefix. + /// Derives an AES-256 key from the ML-KEM shared secret via SHA-256 with a domain prefix. fn seal_kdf(shared: &SharedSecretKey) -> [u8; 32] { const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeySeal/AES\x00\x00\x00\x00\x00\x00"; let mut hasher = sha2::Sha256::new(); @@ -407,8 +405,10 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let restored = GroupKeyHolder::unseal(&sealed, recipient_vsk).expect("unseal"); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -433,13 +433,14 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let wrong_ssk = SecretSpendingKey([99_u8; 32]); - let wrong_vsk = wrong_ssk + let wrong_vsk = SecretSpendingKey([99_u8; 32]) .produce_private_key_holder(None) .viewing_secret_key; - let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let result = GroupKeyHolder::unseal(&sealed, wrong_vsk); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -453,16 +454,18 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - // Flip a byte in the ciphertext portion (after ephemeral_pubkey + nonce) + let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + // Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce). let last = sealed.len() - 1; sealed[last] ^= 0xFF; - let result = GroupKeyHolder::unseal(&sealed, recipient_vsk); + let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } - /// Two seals of the same holder produce different ciphertexts (ephemeral randomness). + /// Two seals of the same holder produce different ciphertexts (KEM randomness). #[test] fn two_seals_produce_different_ciphertexts() { let holder = GroupKeyHolder::from_gms([42_u8; 32]); @@ -472,7 +475,7 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0); + let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.to_bytes().to_vec()); let sealed_a = holder.seal_for(&sealing_key); let sealed_b = holder.seal_for(&sealing_key); assert_ne!(sealed_a, sealed_b); @@ -481,14 +484,15 @@ mod tests { /// Sealed payload is too short. #[test] fn unseal_too_short_fails() { - let vsk: SealingSecretKey = [7_u8; 32]; - let result = GroupKeyHolder::unseal(&[0_u8; 10], vsk); + let vsk = SealingSecretKey { + d: [7_u8; 32], + z: [0_u8; 32], + }; + let result = GroupKeyHolder::unseal(&[0_u8; 10], &vsk); assert!(matches!(result, Err(super::SealError::TooShort))); } - /// Degenerate GMS values (all-zeros, all-ones, single-bit) must still produce valid, - /// non-zero, pairwise-distinct npks. Rules out accidental "if gms == default { return - /// default }" style shortcuts in the derivation. + /// Degenerate GMS values must still produce valid, non-zero, pairwise-distinct npks. #[test] fn degenerate_gms_produces_distinct_non_zero_keys() { let seed = PdaSeed::new([1; 32]); @@ -526,21 +530,19 @@ mod tests { let pda_seed = PdaSeed::new([42_u8; 32]); let program_id: lee_core::program::ProgramId = [1; 8]; - // Derive Alice's keys let alice_keys = alice_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed); let alice_npk = alice_keys.generate_nullifier_public_key(); - // Seal GMS for Bob using Bob's viewing key, Bob unseals let bob_ssk = SecretSpendingKey([77_u8; 32]); let bob_keys = bob_ssk.produce_private_key_holder(None); let bob_vpk = bob_keys.generate_viewing_public_key(); let bob_vsk = bob_keys.viewing_secret_key; - let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0)); + let sealed = + alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.to_bytes().to_vec())); let bob_holder = - GroupKeyHolder::unseal(&sealed, bob_vsk).expect("Bob should unseal the GMS"); + GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); - // Key agreement: both derive identical NPK and AccountId let bob_npk = bob_holder .derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed) .generate_nullifier_public_key(); diff --git a/lee/key_protocol/src/key_management/key_tree/keys_private.rs b/lee/key_protocol/src/key_management/key_tree/keys_private.rs index cd86720b..5a27be79 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use k256::{Scalar, elliptic_curve::PrimeField as _}; use lee_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; +use sha2::Digest as _; use crate::key_management::{ KeyChain, @@ -34,10 +34,10 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(None); - let vsk = ssk.generate_viewing_secret_key(None); + let vsk = ssk.generate_viewing_secret_seed_key(None); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from_scalar(vsk); + let vpk = ViewingPublicKey::from(&vsk); Self { value: ( @@ -59,16 +59,20 @@ impl ChildKeysPrivate { #[must_use] pub fn nth_child(&self, cci: u32) -> Self { - #[expect(clippy::arithmetic_side_effects, reason = "TODO: fix later")] - let parent_pt = - Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into()) - .expect("Key generated as scalar, must be valid representation") - * Scalar::from_repr(self.value.0.private_key_holder.viewing_secret_key.into()) - .expect("Key generated as scalar, must be valid representation"); - let mut input = vec![]; + // `parent_hash`` is used to incorporate entropy based on the parent node's keys + // to generate the `ssk` and `ccc` values. + let mut parent_hash = sha2::Sha256::new(); + parent_hash.update(b"LEE/keys"); + parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.z); + let parent_pt = parent_hash.finalize(); + // Each child (of the same parent node) share the same `parent_pt`. + // To ensure that each child generates unique keys, we include the child index. + let mut input = vec![]; input.extend_from_slice(b"LEE_seed_priv"); - input.extend_from_slice(&parent_pt.to_bytes()); + input.extend_from_slice(&parent_pt); #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] input.extend_from_slice(&cci.to_be_bytes()); @@ -84,10 +88,10 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(Some(cci)); - let vsk = ssk.generate_viewing_secret_key(Some(cci)); + let vsk = ssk.generate_viewing_secret_seed_key(Some(cci)); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from_scalar(vsk); + let vpk = ViewingPublicKey::from(&vsk); Self { value: ( @@ -128,12 +132,11 @@ impl KeyTreeNode for ChildKeysPrivate { #[cfg(test)] mod tests { - use lee_core::{NullifierPublicKey, NullifierSecretKey}; + use lee_core::NullifierSecretKey; use super::*; use crate::key_management::{self, secret_holders::ViewingSecretKey}; - #[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")] #[test] fn master_key_generation() { let seed: [u8; 64] = [ @@ -145,7 +148,7 @@ mod tests { let keys = ChildKeysPrivate::root(seed); - let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([ + let expected_ssk = key_management::secret_holders::SecretSpendingKey([ 246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139, 222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191, ]); @@ -160,26 +163,92 @@ mod tests { 34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163, ]; - let expected_npk: NullifierPublicKey = lee_core::NullifierPublicKey([ + let expected_npk = lee_core::NullifierPublicKey([ 7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97, 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); - let expected_vsk = [ - 155, 90, 54, 75, 228, 130, 68, 201, 129, 251, 180, 195, 250, 64, 34, 230, 241, 204, - 216, 50, 149, 156, 10, 67, 208, 74, 9, 10, 47, 59, 50, 202, - ]; - let expected_vpk_as_bytes: [u8; 33] = [ - 2, 191, 99, 102, 114, 40, 131, 109, 166, 8, 222, 186, 107, 29, 156, 106, 206, 96, 127, - 80, 170, 66, 217, 79, 38, 80, 11, 74, 147, 123, 221, 159, 166, - ]; + let expected_vsk = ViewingSecretKey::new( + [ + 187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194, + 159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36, + ], + [ + 80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19, + 223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82, + ], + ); + let expected_vpk: [u8; 1184] = [ + 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, + 150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181, + 118, 237, 25, 129, 51, 160, 189, 51, 99, 184, 57, 28, 121, 240, 236, 2, 170, 198, 26, + 91, 172, 110, 52, 32, 186, 35, 179, 202, 234, 249, 15, 242, 100, 198, 168, 163, 120, + 205, 118, 85, 195, 210, 187, 95, 150, 154, 8, 68, 165, 237, 87, 166, 101, 57, 4, 18, + 11, 122, 235, 180, 199, 154, 165, 158, 55, 136, 30, 237, 43, 167, 215, 68, 80, 102, 0, + 71, 90, 130, 206, 240, 215, 69, 199, 83, 7, 60, 184, 128, 230, 184, 61, 93, 201, 204, + 165, 104, 9, 127, 220, 52, 246, 217, 131, 251, 2, 170, 133, 6, 51, 40, 224, 101, 61, + 16, 135, 32, 182, 201, 68, 58, 171, 54, 161, 184, 243, 38, 106, 200, 251, 17, 172, 8, + 24, 73, 230, 55, 85, 20, 147, 222, 165, 200, 116, 135, 47, 20, 227, 56, 220, 64, 120, + 215, 245, 58, 86, 102, 149, 252, 193, 163, 160, 59, 82, 138, 249, 171, 1, 54, 199, 193, + 171, 85, 38, 64, 56, 121, 106, 84, 57, 252, 94, 147, 16, 191, 196, 104, 47, 129, 84, + 21, 252, 160, 81, 207, 184, 199, 3, 177, 74, 117, 115, 175, 138, 108, 36, 198, 5, 32, + 15, 218, 3, 20, 19, 15, 251, 209, 86, 128, 139, 148, 78, 10, 34, 144, 149, 74, 102, 48, + 59, 70, 124, 47, 193, 100, 26, 9, 104, 178, 102, 156, 199, 242, 101, 147, 161, 87, 27, + 234, 192, 204, 41, 36, 43, 83, 219, 15, 211, 66, 91, 76, 73, 13, 113, 155, 203, 193, + 160, 130, 84, 103, 47, 70, 100, 147, 169, 65, 119, 84, 121, 122, 161, 76, 203, 144, + 248, 145, 22, 8, 46, 121, 44, 77, 20, 149, 66, 179, 56, 149, 231, 98, 184, 9, 64, 14, + 67, 196, 34, 8, 123, 21, 80, 169, 168, 223, 230, 133, 0, 66, 159, 230, 69, 201, 205, + 169, 105, 196, 21, 71, 84, 70, 58, 165, 165, 134, 186, 232, 60, 70, 51, 57, 239, 74, + 174, 116, 234, 36, 178, 49, 42, 168, 250, 104, 141, 106, 0, 109, 52, 86, 104, 243, 62, + 214, 137, 48, 107, 2, 152, 206, 227, 175, 147, 236, 19, 113, 27, 191, 231, 235, 167, + 114, 104, 23, 126, 203, 94, 242, 149, 171, 115, 170, 89, 244, 58, 29, 176, 73, 203, 44, + 8, 32, 9, 226, 32, 78, 246, 38, 235, 149, 133, 25, 243, 47, 124, 180, 200, 211, 165, + 137, 56, 169, 117, 31, 244, 65, 91, 135, 146, 158, 20, 75, 102, 32, 65, 250, 103, 199, + 36, 48, 31, 155, 164, 191, 222, 85, 37, 66, 243, 17, 120, 104, 0, 228, 83, 200, 116, 6, + 199, 106, 236, 139, 246, 216, 152, 241, 211, 85, 106, 200, 44, 231, 240, 66, 3, 193, + 147, 16, 145, 65, 49, 33, 53, 247, 69, 47, 44, 113, 86, 117, 6, 20, 193, 183, 128, 178, + 181, 21, 251, 99, 39, 149, 210, 146, 106, 181, 186, 7, 36, 63, 186, 234, 191, 164, 193, + 162, 127, 250, 122, 189, 219, 21, 92, 48, 86, 209, 184, 99, 160, 201, 162, 145, 20, + 138, 154, 18, 37, 180, 209, 165, 165, 51, 187, 78, 193, 175, 135, 6, 55, 216, 178, 10, + 40, 246, 98, 128, 80, 14, 38, 69, 113, 123, 54, 94, 43, 50, 106, 167, 17, 77, 163, 148, + 117, 225, 9, 7, 253, 240, 157, 96, 103, 33, 100, 37, 37, 20, 53, 138, 234, 55, 45, 232, + 154, 9, 150, 192, 116, 36, 119, 106, 95, 119, 34, 220, 84, 174, 19, 227, 33, 209, 96, + 197, 148, 230, 197, 59, 117, 130, 7, 116, 11, 0, 197, 16, 249, 151, 31, 4, 64, 29, 165, + 247, 110, 176, 166, 4, 112, 136, 101, 208, 7, 179, 38, 183, 134, 58, 107, 207, 160, 38, + 159, 67, 112, 20, 225, 199, 179, 133, 117, 144, 54, 199, 15, 204, 80, 154, 116, 84, 88, + 109, 113, 5, 207, 226, 21, 62, 247, 122, 14, 156, 9, 8, 76, 26, 148, 67, 196, 128, 176, + 78, 51, 161, 151, 75, 248, 154, 31, 168, 9, 4, 3, 107, 222, 245, 178, 21, 84, 7, 25, + 155, 118, 97, 135, 63, 89, 233, 11, 207, 148, 155, 38, 106, 104, 102, 140, 104, 67, + 149, 20, 30, 196, 44, 197, 128, 34, 182, 80, 30, 32, 137, 34, 212, 164, 177, 164, 12, + 115, 41, 156, 111, 71, 230, 120, 111, 218, 25, 117, 218, 75, 167, 32, 37, 57, 50, 99, + 181, 203, 40, 105, 248, 150, 114, 121, 73, 127, 198, 191, 161, 44, 56, 213, 243, 71, 2, + 56, 192, 243, 107, 179, 27, 96, 21, 116, 169, 64, 15, 97, 166, 151, 200, 11, 40, 204, + 71, 168, 220, 9, 55, 43, 146, 244, 212, 166, 192, 180, 189, 237, 162, 42, 29, 33, 52, + 193, 4, 178, 157, 244, 28, 209, 44, 26, 36, 147, 126, 94, 164, 37, 47, 115, 38, 23, + 165, 96, 106, 140, 42, 69, 146, 194, 93, 71, 175, 49, 147, 32, 246, 97, 94, 41, 116, + 127, 174, 18, 16, 14, 163, 17, 180, 213, 203, 166, 33, 139, 214, 18, 170, 27, 41, 59, + 175, 200, 101, 14, 128, 45, 179, 167, 136, 232, 138, 56, 124, 145, 75, 233, 132, 161, + 196, 164, 72, 80, 60, 187, 38, 90, 90, 17, 66, 134, 59, 2, 165, 29, 76, 24, 38, 211, + 177, 83, 119, 20, 239, 59, 77, 34, 3, 42, 47, 60, 89, 46, 103, 168, 120, 17, 199, 50, + 17, 103, 107, 48, 8, 53, 220, 159, 212, 65, 198, 80, 8, 11, 235, 97, 203, 196, 240, 44, + 56, 121, 77, 91, 196, 160, 129, 242, 149, 226, 57, 106, 180, 76, 161, 203, 18, 37, 166, + 153, 44, 40, 28, 74, 8, 11, 6, 166, 54, 10, 103, 247, 23, 35, 7, 47, 173, 133, 71, 85, + 3, 168, 250, 120, 126, 174, 37, 80, 128, 107, 7, 161, 130, 155, 136, 92, 48, 215, 119, + 196, 124, 85, 157, 234, 2, 166, 137, 65, 121, 222, 112, 47, 17, 43, 23, 111, 88, 5, + 195, 41, 8, 191, 227, 21, 173, 35, 199, 196, 188, 162, 191, 195, 204, 137, 54, 16, 73, + 178, 150, 249, 234, 22, 216, 123, 157, 144, 218, 118, 53, 193, 67, 65, 84, 162, 244, + 165, 24, 110, 246, 146, 228, 212, 180, 150, 116, 201, 37, 128, 76, 41, 188, 42, 79, + 148, 52, 196, 176, 178, 224, 48, 168, 13, 129, 193, 131, 185, 131, 93, 40, 145, 56, + 180, 29, 153, 83, 39, 69, 232, 96, 238, 137, 104, 150, 2, 202, 239, 149, 248, 154, 115, + 115, 127, 3, 8, 32, 61, 96, 66, 25, 181, 14, 72, 73, 97, 186, 134, 140, 33, 69, 33, 74, + ]; assert!(expected_ssk == keys.value.0.secret_spending_key); assert!(expected_ccc == keys.ccc); assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == keys.value.0.nullifier_public_key); assert!(expected_vsk == keys.value.0.private_key_holder.viewing_secret_key); - assert!(expected_vpk_as_bytes == keys.value.0.viewing_public_key.to_bytes()); + assert!(expected_vpk == keys.value.0.viewing_public_key.to_bytes()); } #[test] @@ -194,33 +263,107 @@ mod tests { let root_node = ChildKeysPrivate::root(seed); let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32); - let expected_ccc: [u8; 32] = [ - 27, 73, 133, 213, 214, 63, 217, 184, 164, 17, 172, 140, 223, 95, 255, 157, 11, 0, 58, - 53, 82, 147, 121, 120, 199, 50, 30, 28, 103, 24, 121, 187, + let expected_ssk = key_management::secret_holders::SecretSpendingKey([ + 151, 183, 113, 151, 215, 187, 207, 64, 197, 182, 207, 32, 5, 49, 180, 98, 119, 14, 248, + 175, 39, 100, 47, 109, 148, 173, 217, 253, 159, 234, 209, 113, + ]); + + let expected_ccc = [ + 138, 243, 142, 163, 62, 107, 63, 131, 230, 158, 185, 60, 204, 50, 243, 222, 13, 123, + 98, 116, 131, 194, 7, 25, 129, 209, 163, 72, 178, 143, 192, 240, ]; let expected_nsk: NullifierSecretKey = [ - 124, 61, 40, 92, 33, 135, 3, 41, 200, 234, 3, 69, 102, 184, 57, 191, 106, 151, 194, - 192, 103, 132, 141, 112, 249, 108, 192, 117, 24, 48, 70, 216, + 196, 33, 11, 39, 220, 84, 119, 182, 187, 194, 135, 20, 124, 33, 244, 205, 96, 58, 102, + 52, 74, 67, 110, 213, 24, 16, 160, 64, 247, 3, 107, 235, ]; let expected_npk = lee_core::NullifierPublicKey([ - 116, 231, 246, 189, 145, 240, 37, 59, 219, 223, 216, 246, 116, 171, 223, 55, 197, 200, - 134, 192, 221, 40, 218, 167, 239, 5, 11, 95, 147, 247, 162, 226, + 247, 253, 217, 86, 157, 208, 39, 172, 59, 190, 88, 165, 7, 173, 183, 106, 172, 211, 4, + 180, 51, 107, 177, 107, 51, 117, 231, 176, 200, 103, 1, 121, ]); - let expected_vsk: ViewingSecretKey = [ - 33, 155, 68, 60, 102, 70, 47, 105, 194, 129, 44, 26, 143, 198, 44, 244, 185, 31, 236, - 252, 205, 89, 138, 107, 39, 38, 154, 73, 109, 166, 41, 114, - ]; - let expected_vpk_as_bytes: [u8; 33] = [ - 2, 78, 213, 113, 117, 105, 162, 248, 175, 68, 128, 232, 106, 204, 208, 159, 11, 78, 48, - 244, 127, 112, 46, 0, 93, 184, 1, 77, 132, 160, 75, 152, 88, + let expected_vsk = ViewingSecretKey::new( + [ + 185, 209, 179, 92, 7, 131, 98, 121, 215, 46, 154, 56, 238, 106, 162, 225, 83, 82, + 134, 3, 80, 186, 35, 178, 161, 204, 205, 163, 28, 19, 149, 18, + ], + [ + 174, 24, 72, 205, 129, 123, 131, 9, 146, 152, 224, 151, 10, 184, 224, 109, 94, 149, + 117, 60, 26, 10, 212, 125, 113, 147, 87, 67, 73, 26, 101, 193, + ], + ); + + let expected_vpk: [u8; 1184] = [ + 215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84, + 232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57, + 71, 11, 204, 156, 130, 30, 170, 199, 201, 25, 2, 21, 34, 155, 136, 124, 145, 223, 128, + 177, 207, 92, 38, 252, 165, 118, 61, 128, 71, 154, 242, 105, 165, 52, 7, 6, 244, 120, + 227, 134, 191, 25, 169, 150, 123, 246, 138, 25, 196, 126, 156, 144, 33, 123, 120, 44, + 142, 89, 201, 49, 219, 205, 87, 236, 110, 64, 129, 102, 100, 155, 26, 101, 121, 42, + 236, 82, 111, 141, 117, 75, 71, 194, 73, 123, 170, 110, 69, 149, 107, 96, 195, 55, 122, + 140, 131, 106, 140, 156, 147, 75, 28, 128, 138, 113, 86, 37, 63, 173, 214, 200, 2, 214, + 84, 234, 176, 120, 252, 184, 99, 192, 65, 112, 150, 99, 26, 174, 187, 183, 187, 64, 90, + 248, 100, 66, 63, 195, 3, 44, 43, 128, 59, 149, 107, 66, 180, 67, 200, 183, 200, 36, + 91, 7, 65, 228, 159, 79, 44, 89, 35, 163, 145, 92, 227, 104, 2, 72, 5, 7, 193, 21, 51, + 116, 198, 184, 6, 192, 188, 68, 183, 163, 193, 142, 244, 217, 155, 197, 187, 189, 174, + 225, 45, 126, 112, 93, 194, 156, 102, 150, 1, 188, 222, 76, 108, 73, 149, 44, 28, 219, + 66, 95, 215, 204, 148, 217, 16, 36, 121, 112, 2, 51, 10, 195, 137, 12, 93, 203, 146, + 138, 211, 15, 201, 42, 72, 146, 186, 160, 222, 235, 127, 83, 48, 182, 49, 248, 29, 138, + 16, 32, 232, 179, 163, 187, 161, 174, 152, 187, 93, 76, 166, 48, 230, 219, 111, 123, + 181, 103, 130, 28, 109, 235, 115, 45, 57, 193, 206, 160, 17, 52, 92, 194, 25, 3, 80, + 97, 142, 249, 151, 94, 250, 95, 12, 57, 11, 165, 92, 47, 85, 182, 48, 22, 60, 97, 244, + 59, 194, 135, 180, 133, 106, 227, 56, 192, 60, 91, 15, 241, 146, 89, 240, 130, 219, + 202, 187, 43, 85, 98, 50, 104, 64, 114, 113, 80, 54, 69, 69, 5, 43, 90, 19, 0, 0, 188, + 251, 184, 70, 160, 18, 117, 76, 53, 209, 166, 96, 34, 224, 137, 115, 183, 168, 243, 19, + 1, 255, 4, 97, 162, 199, 104, 72, 213, 111, 62, 54, 172, 82, 184, 82, 143, 71, 99, 25, + 104, 74, 120, 70, 84, 235, 32, 22, 20, 218, 163, 77, 194, 125, 75, 22, 72, 236, 192, + 200, 107, 91, 156, 201, 10, 178, 87, 19, 181, 211, 91, 17, 145, 200, 17, 179, 65, 75, + 200, 186, 89, 144, 91, 184, 116, 214, 51, 91, 42, 162, 243, 202, 92, 18, 54, 0, 213, + 67, 149, 151, 51, 29, 220, 196, 160, 201, 68, 113, 210, 164, 175, 152, 121, 168, 231, + 161, 91, 132, 218, 1, 171, 176, 84, 100, 57, 1, 3, 2, 196, 194, 76, 181, 79, 171, 157, + 35, 162, 155, 192, 210, 149, 142, 120, 189, 127, 151, 96, 202, 225, 73, 242, 81, 112, + 237, 224, 155, 130, 130, 34, 196, 153, 131, 161, 113, 163, 172, 114, 48, 207, 32, 151, + 172, 83, 145, 79, 210, 100, 161, 92, 82, 216, 90, 104, 238, 212, 38, 50, 107, 17, 228, + 195, 190, 6, 151, 165, 148, 245, 102, 51, 8, 185, 8, 85, 59, 247, 219, 95, 219, 170, + 155, 233, 123, 27, 64, 251, 56, 24, 200, 16, 181, 212, 146, 61, 116, 106, 215, 214, 62, + 118, 27, 68, 233, 148, 73, 135, 199, 74, 184, 89, 159, 217, 139, 24, 208, 250, 30, 224, + 97, 185, 237, 193, 8, 216, 23, 186, 5, 50, 41, 161, 203, 22, 217, 23, 194, 191, 148, + 124, 10, 212, 171, 209, 210, 145, 184, 171, 74, 35, 220, 43, 145, 241, 23, 43, 92, 171, + 216, 43, 114, 77, 155, 147, 156, 86, 56, 170, 27, 1, 54, 182, 169, 96, 22, 201, 51, + 145, 94, 143, 133, 106, 47, 176, 112, 197, 197, 96, 80, 73, 164, 207, 179, 22, 229, + 171, 201, 223, 219, 13, 219, 1, 91, 224, 252, 171, 199, 217, 25, 60, 128, 135, 9, 71, + 105, 231, 86, 34, 21, 155, 50, 0, 105, 72, 117, 108, 175, 140, 9, 181, 249, 139, 97, 3, + 161, 66, 248, 42, 67, 113, 132, 8, 119, 232, 6, 169, 18, 157, 222, 53, 176, 56, 137, + 120, 18, 115, 199, 187, 112, 48, 223, 211, 206, 152, 252, 108, 179, 129, 20, 227, 248, + 183, 234, 87, 202, 49, 17, 69, 215, 118, 89, 188, 180, 33, 238, 245, 206, 40, 179, 129, + 242, 59, 73, 254, 117, 114, 250, 179, 103, 109, 250, 202, 99, 152, 2, 167, 130, 169, + 35, 71, 89, 211, 140, 71, 103, 154, 121, 108, 147, 191, 186, 73, 10, 73, 203, 23, 55, + 106, 144, 98, 227, 157, 25, 27, 81, 67, 11, 57, 88, 227, 116, 61, 100, 94, 23, 166, + 146, 57, 226, 72, 124, 33, 65, 226, 35, 167, 206, 156, 202, 213, 213, 158, 89, 249, + 181, 19, 113, 109, 217, 71, 168, 142, 180, 122, 30, 5, 54, 170, 155, 73, 56, 170, 124, + 139, 4, 165, 103, 82, 32, 183, 84, 7, 239, 117, 135, 239, 48, 24, 28, 210, 49, 137, 6, + 158, 65, 211, 113, 205, 135, 146, 83, 10, 46, 90, 27, 97, 135, 135, 185, 173, 69, 58, + 34, 247, 141, 150, 6, 158, 117, 23, 198, 139, 65, 81, 179, 187, 194, 247, 203, 127, + 106, 232, 119, 122, 215, 197, 110, 69, 203, 174, 227, 63, 185, 106, 14, 184, 104, 113, + 233, 83, 92, 104, 38, 188, 9, 135, 107, 108, 121, 193, 33, 209, 89, 39, 137, 17, 208, + 26, 21, 238, 169, 86, 181, 193, 153, 82, 8, 151, 53, 39, 88, 91, 252, 3, 33, 75, 127, + 9, 168, 53, 34, 1, 173, 202, 123, 157, 174, 170, 199, 254, 187, 196, 144, 37, 29, 48, + 112, 173, 107, 147, 155, 69, 134, 137, 156, 247, 123, 242, 72, 5, 43, 106, 89, 179, + 204, 41, 15, 60, 48, 78, 214, 180, 26, 170, 67, 71, 66, 146, 113, 220, 159, 153, 201, + 176, 116, 154, 21, 186, 33, 180, 72, 39, 187, 240, 80, 112, 132, 144, 173, 210, 12, 76, + 184, 146, 89, 178, 178, 82, 109, 71, 201, 241, 160, 207, 219, 124, 77, 2, 105, 124, + 178, 71, 3, 38, 64, 41, 83, 170, 137, 82, 242, 144, 76, 102, 82, 7, 25, 149, 141, 169, + 46, 4, 68, 40, 244, 146, 131, 107, 148, 18, 111, 85, 104, 243, 28, 75, 176, 249, 88, + 82, 123, 89, 29, 104, 135, 230, 117, 67, 26, 249, 108, 145, 76, 38, 175, 89, 185, 94, + 106, 128, 201, 150, 151, 194, 133, 21, 81, 213, 231, 15, 117, 44, 61, 86, 223, 162, 56, + 190, 166, 177, 157, 137, 60, 208, 155, 234, 158, 252, 30, ]; + assert!(expected_ssk == child_node.value.0.secret_spending_key); assert!(expected_ccc == child_node.ccc); assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == child_node.value.0.nullifier_public_key); assert!(expected_vsk == child_node.value.0.private_key_holder.viewing_secret_key); - assert!(expected_vpk_as_bytes == child_node.value.0.viewing_public_key.to_bytes()); + assert!(expected_vpk == child_node.value.0.viewing_public_key.to_bytes()); } } diff --git a/lee/key_protocol/src/key_management/mod.rs b/lee/key_protocol/src/key_management/mod.rs index 61d4b40d..459badf0 100644 --- a/lee/key_protocol/src/key_management/mod.rs +++ b/lee/key_protocol/src/key_management/mod.rs @@ -69,21 +69,15 @@ impl KeyChain { pub fn calculate_shared_secret_receiver( &self, ephemeral_public_key_sender: &EphemeralPublicKey, - index: Option, - ) -> SharedSecretKey { - SharedSecretKey::new( - self.secret_spending_key.generate_viewing_secret_key(index), - ephemeral_public_key_sender, - ) + ) -> Option { + let vsk = &self.private_key_holder.viewing_secret_key; + SharedSecretKey::decapsulate(ephemeral_public_key_sender, &vsk.d, &vsk.z) } } #[cfg(test)] mod tests { - use aes_gcm::aead::OsRng; use base58::ToBase58 as _; - use k256::{AffinePoint, elliptic_curve::group::GroupEncoding as _}; - use rand::RngCore as _; use super::*; use crate::key_management::{ @@ -106,14 +100,31 @@ mod tests { fn calculate_shared_secret_receiver() { let account_id_key_holder = KeyChain::new_os_random(); - // Generate a random ephemeral public key sender - let mut scalar = [0; 32]; - OsRng.fill_bytes(&mut scalar); - let ephemeral_public_key_sender = EphemeralPublicKey::from_scalar(scalar); + // Create a proper KEM ciphertext by encapsulating toward this key chain's VPK. + let (_, epk) = SharedSecretKey::encapsulate(&account_id_key_holder.viewing_public_key); - // Calculate shared secret - let _shared_secret = account_id_key_holder - .calculate_shared_secret_receiver(&ephemeral_public_key_sender, None); + let _shared_secret = account_id_key_holder.calculate_shared_secret_receiver(&epk); + } + + #[test] + fn calculate_shared_secret_receiver_returns_none_for_malformed_epk() { + let key_chain = KeyChain::new_os_random(); + + let short_epk = EphemeralPublicKey(vec![42_u8; 100]); + assert!( + key_chain + .calculate_shared_secret_receiver(&short_epk) + .is_none(), + "short EphemeralPublicKey must return None" + ); + + let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + assert!( + key_chain + .calculate_shared_secret_receiver(&long_epk) + .is_none(), + "long EphemeralPublicKey must return None" + ); } #[test] @@ -135,12 +146,6 @@ mod tests { println!("======Prerequisites======"); println!(); - println!( - "Group generator {:?}", - hex::encode(AffinePoint::GENERATOR.to_bytes()) - ); - println!(); - println!("======Holders======"); println!(); @@ -188,14 +193,12 @@ mod tests { fn non_trivial_chain_index() { let keys = account_with_chain_index_2_for_tests(); - let eph_key_holder = EphemeralKeyHolder::new(&keys.nullifier_public_key); + let eph_key_holder = EphemeralKeyHolder::new(&keys.viewing_public_key); - let key_sender = eph_key_holder.calculate_shared_secret_sender(&keys.viewing_public_key); - let key_receiver = keys.calculate_shared_secret_receiver( - &eph_key_holder.generate_ephemeral_public_key(), - Some(2), - ); + let key_sender = eph_key_holder.calculate_shared_secret_sender(); + let key_receiver = + keys.calculate_shared_secret_receiver(eph_key_holder.ephemeral_public_key()); - assert_eq!(key_sender.0, key_receiver.0); + assert_eq!(key_sender.0, key_receiver.unwrap().0); } } diff --git a/lee/key_protocol/src/key_management/secret_holders.rs b/lee/key_protocol/src/key_management/secret_holders.rs index 50e5657b..7bda4ffb 100644 --- a/lee/key_protocol/src/key_management/secret_holders.rs +++ b/lee/key_protocol/src/key_management/secret_holders.rs @@ -1,9 +1,7 @@ use bip39::Mnemonic; use common::HashType; -use lee_core::{ - NullifierPublicKey, NullifierSecretKey, - encryption::{Scalar, ViewingPublicKey}, -}; +use lee_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey}; +use ml_kem; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -19,8 +17,20 @@ pub struct SeedHolder { /// Secret spending key object. Can produce `PrivateKeyHolder` objects. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct SecretSpendingKey(pub [u8; 32]); +/// Viewing secret key: the FIPS 203 KEM seed split into its two 32-byte halves `d` and `z`, +/// from which the ML-KEM-768 decapsulation key is derived deterministically. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ViewingSecretKey { + pub d: [u8; 32], + pub z: [u8; 32], +} -pub type ViewingSecretKey = Scalar; +impl ViewingSecretKey { + #[must_use] + pub const fn new(d: [u8; 32], z: [u8; 32]) -> Self { + Self { d, z } + } +} /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// for recepient. @@ -114,7 +124,7 @@ impl SecretSpendingKey { #[must_use] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] - pub fn generate_viewing_secret_key(&self, index: Option) -> ViewingSecretKey { + pub fn generate_viewing_secret_seed_key(&self, index: Option) -> ViewingSecretKey { const PREFIX: &[u8; 8] = b"LEE/keys"; const SUFFIX_1: &[u8; 1] = &[2]; const SUFFIX_2: &[u8; 19] = &[0; 19]; @@ -124,25 +134,57 @@ impl SecretSpendingKey { _ => index.expect("Expect a valid u32"), }; - let mut hasher = sha2::Sha256::new(); - hasher.update(PREFIX); - hasher.update(self.0); - hasher.update(SUFFIX_1); - hasher.update(index.to_be_bytes()); - hasher.update(SUFFIX_2); + let mut bytes: Vec = Vec::with_capacity(64); + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(&self.0); + bytes.extend_from_slice(SUFFIX_1); + bytes.extend_from_slice(&index.to_be_bytes()); + bytes.extend_from_slice(SUFFIX_2); + let bytes: [u8; 64] = bytes + .try_into() + .expect("`generate_viewing_secret_seed_key`: bytes must be exactly 64"); - hasher.finalize_fixed().into() + let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); + + ViewingSecretKey::new( + *full_seed + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + *full_seed + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"), + ) + } + + #[must_use] + pub const fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey { + ViewingSecretKey::new( + *seed.first_chunk::<32>().expect("seed is 64 bytes"), + *seed.last_chunk::<32>().expect("seed is 64 bytes"), + ) } #[must_use] pub fn produce_private_key_holder(&self, index: Option) -> PrivateKeyHolder { PrivateKeyHolder { nullifier_secret_key: self.generate_nullifier_secret_key(index), - viewing_secret_key: self.generate_viewing_secret_key(index), + viewing_secret_key: self.generate_viewing_secret_seed_key(index), } } } +impl From<&ViewingSecretKey> for ViewingPublicKey { + fn from(sk: &ViewingSecretKey) -> Self { + use ml_kem::{Kem, KeyExport as _, MlKem768, Seed}; + let mut seed_bytes = [0_u8; 64]; + seed_bytes[..32].copy_from_slice(&sk.d); + seed_bytes[32..].copy_from_slice(&sk.z); + let dk = ::DecapsulationKey::from_seed(Seed::from(seed_bytes)); + Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec()) + .expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes") + } +} + impl PrivateKeyHolder { #[must_use] pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey { @@ -151,7 +193,7 @@ impl PrivateKeyHolder { #[must_use] pub fn generate_viewing_public_key(&self) -> ViewingPublicKey { - ViewingPublicKey::from_scalar(self.viewing_secret_key) + ViewingPublicKey::from(&self.viewing_secret_key) } } @@ -183,8 +225,7 @@ mod tests { assert_eq!(seed_holder.seed.len(), 64); let top_secret_key_holder = seed_holder.produce_top_secret_key_holder(); - - let _vsk = top_secret_key_holder.generate_viewing_secret_key(None); + let _vsk = top_secret_key_holder.generate_viewing_secret_seed_key(None); } #[test] diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index 194c402f..8777db09 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -31,6 +31,7 @@ risc0-build = "3.0.3" risc0-binfmt = "3.0.2" [dev-dependencies] +lee_core = { workspace = true, features = ["test_utils"] } token_core.workspace = true authenticated_transfer_core.workspace = true test_program_methods.workspace = true diff --git a/lee/state_machine/core/Cargo.toml b/lee/state_machine/core/Cargo.toml index 4447d859..6e1f0ff0 100644 --- a/lee/state_machine/core/Cargo.toml +++ b/lee/state_machine/core/Cargo.toml @@ -16,7 +16,7 @@ thiserror.workspace = true bytemuck.workspace = true bytesize.workspace = true base58.workspace = true -k256 = { workspace = true, optional = true } +ml-kem = { workspace = true, optional = true, features = ["getrandom"] } chacha20 = { version = "0.10" } [dev-dependencies] @@ -24,4 +24,5 @@ serde_json.workspace = true [features] default = [] -host = ["dep:k256"] +host = ["dep:ml-kem"] +test_utils = ["host"] diff --git a/lee/state_machine/core/src/encoding.rs b/lee/state_machine/core/src/encoding.rs index f4b081bf..59df4b06 100644 --- a/lee/state_machine/core/src/encoding.rs +++ b/lee/state_machine/core/src/encoding.rs @@ -7,7 +7,7 @@ use std::io::Read as _; #[cfg(feature = "host")] use crate::Nullifier; #[cfg(feature = "host")] -use crate::encryption::shared_key_derivation::Secp256k1Point; +use crate::encryption::EphemeralPublicKey; #[cfg(feature = "host")] use crate::error::LeeCoreError; use crate::{ @@ -158,16 +158,17 @@ impl Ciphertext { } #[cfg(feature = "host")] -impl Secp256k1Point { - /// Converts the point to bytes. +impl EphemeralPublicKey { + /// Serializes the ML-KEM-768 ciphertext to bytes (always 1088 bytes). #[must_use] - pub fn to_bytes(&self) -> [u8; 33] { - self.0.clone().try_into().unwrap() + pub fn to_bytes(&self) -> Vec { + self.0.clone() } - /// Deserializes a secp256k1 point from a cursor. + /// Deserializes an ML-KEM-768 ciphertext from a cursor. + /// Reads exactly 1088 bytes — the fixed ciphertext size for ML-KEM-768. pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut value = vec![0; 33]; + let mut value = vec![0_u8; 1088]; cursor.read_exact(&mut value)?; Ok(Self(value)) } diff --git a/lee/state_machine/core/src/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index f97f9021..37745d4f 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -6,7 +6,7 @@ use chacha20::{ use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] -pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}; +pub use shared_key_derivation::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; #[cfg(feature = "host")] @@ -154,4 +154,41 @@ mod tests { assert_eq!(account_ct.0.len(), pda_ct.0.len()); } + + /// Verifies the full account-note pipeline: ML-KEM-768 encapsulation/decapsulation + /// feeds the correct shared secret into the SHA-256 KDF and `ChaCha20` round-trip. + #[cfg(feature = "host")] + #[test] + fn kem_to_chacha20_round_trip() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + let vpk = shared_key_derivation::ViewingPublicKey::from_seed(&d, &z); + + let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk); + let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); + + let account = Account { + program_owner: [12_u32; 8], + balance: 999, + ..Account::default() + }; + 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 (decoded_kind, decoded_account) = + EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0) + .expect("decryption must succeed with correct shared secret"); + + assert_eq!(decoded_account, account); + assert_eq!(decoded_kind, kind); + + // Wrong shared secret must not decrypt correctly. + let wrong_ss = SharedSecretKey([0_u8; 32]); + let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0); + assert!( + bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999), + "wrong shared secret must not produce the correct plaintext" + ); + } } diff --git a/lee/state_machine/core/src/encryption/shared_key_derivation.rs b/lee/state_machine/core/src/encryption/shared_key_derivation.rs index 8ea5aac8..a3b2fd32 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -1,78 +1,232 @@ -#![expect( - clippy::arithmetic_side_effects, - reason = "Multiplication of finite field elements can't overflow" -)] - -use std::fmt::Write as _; - use borsh::{BorshDeserialize, BorshSerialize}; -use k256::{ - AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, - elliptic_curve::{ - PrimeField as _, - sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, - }, -}; +use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; use serde::{Deserialize, Serialize}; -use crate::{SharedSecretKey, encryption::Scalar}; +use crate::SharedSecretKey; +/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the +/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct EphemeralPublicKey(pub Vec); + +/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate). #[derive( - Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize, + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, )] -pub struct Secp256k1Point(pub Vec); +pub struct MlKem768EncapsulationKey(Vec); -impl std::fmt::Debug for Secp256k1Point { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let hex: String = self.0.iter().fold(String::new(), |mut acc, b| { - write!(acc, "{b:02x}").expect("writing to string should not fail"); - acc - }); - write!(f, "Secp256k1Point({hex})") +pub type ViewingPublicKey = MlKem768EncapsulationKey; + +impl MlKem768EncapsulationKey { + /// Expected byte length of an ML-KEM-768 encapsulation key. + pub const LEN: usize = 1184; + + /// Construct from raw bytes, returning an error if the length is not [`Self::LEN`]. + pub fn from_bytes(bytes: Vec) -> Result { + if bytes.len() != Self::LEN { + return Err(crate::error::LeeCoreError::DeserializationError(format!( + "MlKem768EncapsulationKey must be {} bytes, got {}", + Self::LEN, + bytes.len() + ))); + } + Ok(Self(bytes)) } -} -impl Secp256k1Point { #[must_use] - pub fn from_scalar(value: Scalar) -> Self { - let x_bytes: FieldBytes = value.into(); - let x = k256::Scalar::from_repr(x_bytes).unwrap(); - - let p = ProjectivePoint::GENERATOR * x; - let q = AffinePoint::from(p); - let enc = q.to_encoded_point(true); - - Self(enc.as_bytes().to_vec()) + pub fn to_bytes(&self) -> &[u8] { + &self.0 } -} -pub type EphemeralSecretKey = Scalar; -pub type EphemeralPublicKey = Secp256k1Point; -pub type ViewingPublicKey = Secp256k1Point; -impl From<&EphemeralSecretKey> for EphemeralPublicKey { - fn from(value: &EphemeralSecretKey) -> Self { - Self::from_scalar(*value) + /// Derive the ML-KEM-768 encapsulation key from the FIPS 203 seed halves `d` and `z`. + #[must_use] + pub fn from_seed(d: &[u8; 32], z: &[u8; 32]) -> Self { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(z); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + Self(dk.encapsulation_key().to_bytes().to_vec()) } } impl SharedSecretKey { - /// Creates a new shared secret key from a scalar and a point. + /// Sender: encapsulate a fresh shared secret toward `ek`. + /// + /// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction + /// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via + /// [`Self::decapsulate`]. #[must_use] - pub fn new(scalar: Scalar, point: &Secp256k1Point) -> Self { - let scalar = k256::Scalar::from_repr(scalar.into()).unwrap(); - let point: [u8; 33] = point.0.clone().try_into().unwrap(); + pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) { + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate(); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - let encoded = EncodedPoint::from_bytes(point).unwrap(); - let pubkey_affine = AffinePoint::from_encoded_point(&encoded).unwrap(); + /// Deterministically encapsulate a shared secret toward `ek` for use in tests. + /// + /// The shared secret has no secret entropy — it is fully determined by `ek`, + /// `message_hash`, and `output_index`, all of which are public. This makes it + /// unsuitable for real encryption but useful for producing stable, reproducible + /// shared secrets in unit tests. Use a distinct `output_index` per output to + /// avoid EPK collisions across multiple outputs in the same test. + /// + /// For production use [`Self::encapsulate`], which draws randomness from the OS. + #[cfg(any(test, feature = "test_utils"))] + #[must_use] + pub fn encapsulate_deterministic( + ek: &MlKem768EncapsulationKey, + message_hash: &[u8; 32], + output_index: u32, + ) -> (Self, EphemeralPublicKey) { + use risc0_zkvm::sha::{Impl, Sha256 as _}; - let shared = ProjectivePoint::from(pubkey_affine) * scalar; - let shared_affine = shared.to_affine(); + let mut input = Vec::with_capacity(36); + input.extend_from_slice(message_hash); + input.extend_from_slice(&output_index.to_le_bytes()); + let hash = Impl::hash_bytes(&input); + let m: ml_kem::B32 = + ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes"); - let shared_affine_encoded = shared_affine.to_encoded_point(false); - let x_bytes_slice = shared_affine_encoded.x().unwrap(); - let mut x_bytes = [0_u8; 32]; - x_bytes.copy_from_slice(x_bytes_slice); + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate_deterministic(&m); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - Self(x_bytes) + /// Receiver: decapsulate the shared secret from a KEM ciphertext. + /// + /// Returns `None` if the `EphemeralPublicKey` is not exactly 1088 bytes — callers on + /// the wallet scan path should skip the output rather than panic on malformed chain data. + /// + /// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed. + #[must_use] + pub fn decapsulate( + ciphertext: &EphemeralPublicKey, + d: &[u8; 32], + z: &[u8; 32], + ) -> Option { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(z); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ss = dk.decapsulate_slice(&ciphertext.0).ok()?; + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + Some(Self(ss_bytes)) + } +} + +#[cfg(test)] +mod tests { + use ml_kem::KeyExport as _; + + use super::*; + + #[test] + fn encapsulate_decapsulate_round_trip() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d); + seed[32..].copy_from_slice(&z); + + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ek_bytes = dk.encapsulation_key().to_bytes(); + let ek = MlKem768EncapsulationKey(ek_bytes.to_vec()); + + let (sender_ss, epk) = SharedSecretKey::encapsulate(&ek); + let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); + + assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match"); + assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes"); + assert_eq!( + ek.0.len(), + 1184, + "ML-KEM-768 encapsulation key is 1184 bytes" + ); + } + + #[test] + fn decapsulate_returns_none_for_malformed_epk() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + + // Too short — 100 bytes instead of 1088. + let short_epk = EphemeralPublicKey(vec![42_u8; 100]); + assert!( + SharedSecretKey::decapsulate(&short_epk, &d, &z).is_none(), + "short EphemeralPublicKey must return None" + ); + + // Too long — 1089 bytes instead of 1088. + let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + assert!( + SharedSecretKey::decapsulate(&long_epk, &d, &z).is_none(), + "long EphemeralPublicKey must return None" + ); + + // Empty. + let empty_epk = EphemeralPublicKey(vec![]); + assert!( + SharedSecretKey::decapsulate(&empty_epk, &d, &z).is_none(), + "empty EphemeralPublicKey must return None" + ); + } + + #[test] + fn different_vpks_produce_different_shared_secrets() { + let (d1, z1) = ([1_u8; 32], [2_u8; 32]); + let (d2, z2) = ([3_u8; 32], [4_u8; 32]); + + let ek1 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d1); + seed[32..].copy_from_slice(&z1); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + let ek2 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d2); + seed[32..].copy_from_slice(&z2); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + + let (ss1, _) = SharedSecretKey::encapsulate(&ek1); + let (ss2, _) = SharedSecretKey::encapsulate(&ek2); + + assert_ne!(ss1.0, ss2.0); } } diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index fee77581..50202a38 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -243,8 +243,8 @@ mod tests { let expected_sender_pre = sender.clone(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; let (output, proof) = execute_and_prove( vec![sender, recipient], @@ -340,11 +340,11 @@ mod tests { Commitment::new(&recipient_account_id, &expected_private_account_2), ]; - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); + let shared_secret_1 = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; - let esk_2 = [5; 32]; - let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); + let shared_secret_2 = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0; let (output, proof) = execute_and_prove( vec![sender_pre, recipient], @@ -418,8 +418,8 @@ mod tests { )) .unwrap(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0; let program_with_deps = ProgramWithDependencies::new( validity_window_chain_caller, @@ -449,7 +449,8 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -487,7 +488,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -526,7 +528,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -581,7 +584,8 @@ mod tests { let shared_keys = test_private_account_keys_1(); let shared_npk = shared_keys.npk(); let shared_identifier: u128 = 42; - let shared_secret = SharedSecretKey::new([55; 32], &shared_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0; // Sender: public account with balance, owned by auth-transfer let sender_id = AccountId::new([99; 32]); @@ -632,7 +636,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let pre = AccountWithMetadata::new(Account::default(), true, account_id); @@ -662,7 +666,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let sender = AccountWithMetadata::new( Account { @@ -707,7 +711,7 @@ mod tests { let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let account = Account { program_owner: program.id(), @@ -756,7 +760,7 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); @@ -811,7 +815,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -838,7 +843,7 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index 9065ccc4..6a289a0a 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -143,7 +143,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::ViewingPublicKey, program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -208,7 +208,7 @@ pub mod tests { let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // all remaining vec fields are empty: u32 len=0 let empty_vec_bytes: &[u8] = &[0_u8; 4]; - // validity windows: unbounded = {from: None (0u8), to: None (0u8)} + // validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} let unbounded_window_bytes: &[u8] = &[0_u8; 2]; let expected_borsh_vec: Vec = [ @@ -246,13 +246,11 @@ pub mod tests { #[test] fn encrypted_account_data_constructor() { let npk = NullifierPublicKey::from(&[1; 32]); - let vpk = ViewingPublicKey::from_scalar([2; 32]); + let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]); let account = Account::default(); let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &vpk); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0); let ciphertext = EncryptionScheme::encrypt( &account, &PrivateAccountKind::Regular(0), diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 33cb12d2..4b74cf55 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -421,7 +421,7 @@ pub mod tests { BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{ BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId, TimestampValidityWindow, WrappedBalanceSum, @@ -536,7 +536,8 @@ pub mod tests { pub struct TestPrivateKeys { pub nsk: NullifierSecretKey, - pub vsk: Scalar, + pub d: [u8; 32], + pub z: [u8; 32], } impl TestPrivateKeys { @@ -545,7 +546,7 @@ pub mod tests { } pub fn vpk(&self) -> ViewingPublicKey { - ViewingPublicKey::from_scalar(self.vsk) + ViewingPublicKey::from_seed(&self.d, &self.z) } } @@ -1333,14 +1334,16 @@ pub mod tests { pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { nsk: [13; 32], - vsk: [31; 32], + d: [31; 32], + z: [32; 32], } } pub fn test_private_account_keys_2() -> TestPrivateKeys { TestPrivateKeys { nsk: [38; 32], - vsk: [83; 32], + d: [83; 32], + z: [84; 32], } } @@ -1361,9 +1364,8 @@ pub mod tests { let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], @@ -1413,13 +1415,11 @@ pub mod tests { let recipient_pre = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); - let epk_1 = EphemeralPublicKey::from_scalar(esk_1); + let (shared_secret_1, epk_1) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); - let esk_2 = [3; 32]; - let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); - let epk_2 = EphemeralPublicKey::from_scalar(esk_2); + let (shared_secret_2, epk_2) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1483,9 +1483,8 @@ pub mod tests { *recipient_account_id, ); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1993,14 +1992,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: recipient_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2039,14 +2048,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2085,14 +2104,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2131,14 +2160,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2177,14 +2216,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2221,14 +2270,24 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2247,7 +2306,8 @@ pub mod tests { let program = Program::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2289,7 +2349,8 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2326,7 +2387,8 @@ pub mod tests { let npk_a = keys_a.npk(); let npk_b = keys_b.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys_b.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in @@ -2361,7 +2423,8 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([77; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2400,7 +2463,8 @@ pub mod tests { let npk = keys.npk(); let claim_seed = PdaSeed::new([77; 32]); let wrong_delegated_seed = PdaSeed::new([88; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2438,8 +2502,8 @@ pub mod tests { let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); let seed = PdaSeed::new([55; 32]); - let shared_a = SharedSecretKey::new([66; 32], &keys_a.vpk()); - let shared_b = SharedSecretKey::new([77; 32], &keys_b.vpk()); + let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0_u8; 32], 0).0; + let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX); let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX); @@ -2480,7 +2544,8 @@ pub mod tests { let program = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let seed = PdaSeed::new([99; 32]); // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = @@ -2580,7 +2645,8 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let shared_secret = SharedSecretKey::new([55; 32], &sender_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), @@ -2924,9 +2990,8 @@ pub mod tests { AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); let balance = 37; @@ -3030,13 +3095,11 @@ pub mod tests { None, ); - let from_esk = [3; 32]; - let from_ss = SharedSecretKey::new(from_esk, &from_keys.vpk()); - let from_epk = EphemeralPublicKey::from_scalar(from_esk); + let (from_ss, from_epk) = + SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0); - let to_esk = [3; 32]; - let to_ss = SharedSecretKey::new(to_esk, &to_keys.vpk()); - let to_epk = EphemeralPublicKey::from_scalar(to_esk); + let (to_ss, to_epk) = + SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1); let mut dependencies = HashMap::new(); @@ -3333,9 +3396,8 @@ pub mod tests { let program = Program::authenticated_transfer_program(); // Set up parameters for the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction = authenticated_transfer_core::Instruction::Initialize; @@ -3385,9 +3447,8 @@ pub mod tests { AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0)); let program = Program::claimer(); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = execute_and_prove( vec![unauthorized_account], @@ -3435,9 +3496,8 @@ pub mod tests { let claimer_program = Program::claimer(); // Set up parameters for claiming the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction = authenticated_transfer_core::Instruction::Initialize; @@ -3485,8 +3545,8 @@ pub mod tests { }; let noop_program = Program::noop(); - let esk2 = [4; 32]; - let shared_secret2 = SharedSecretKey::new(esk2, &private_keys.vpk()); + let shared_secret2 = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0).0; // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( @@ -3570,7 +3630,8 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3596,7 +3657,8 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3642,8 +3704,8 @@ pub mod tests { let balance_to_transfer = 10_u128; let instruction = (balance_to_transfer, auth_transfers.id()); - let recipient_esk = [3; 32]; - let recipient = SharedSecretKey::new(recipient_esk, &recipient_keys.vpk()); + let recipient = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); @@ -3799,9 +3861,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); let instruction = ( block_validity_window, @@ -3869,9 +3930,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); let instruction = ( BlockValidityWindow::new_unbounded(), @@ -4425,8 +4485,10 @@ pub mod tests { ..Account::default() }; - let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk()); - let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk()); + let (alice_shared_0, alice_epk_0) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 0); + let (alice_shared_1, alice_epk_1) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 1); // Fund alice_pda_0 via authenticated_transfer directly. { @@ -4454,11 +4516,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![funder_id], vec![funder_nonce], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([10; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())], output, ) .unwrap(); @@ -4498,11 +4556,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![funder_id], vec![funder_nonce], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([11; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())], output, ) .unwrap(); @@ -4549,11 +4603,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![recipient_id], vec![Nonce(0)], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([10; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_0)], output, ) .unwrap(); @@ -4594,11 +4644,7 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![recipient_id], vec![], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([11; 32]), - )], + vec![(alice_npk, alice_keys.vpk(), alice_epk_1)], output, ) .unwrap(); @@ -4626,7 +4672,7 @@ pub mod tests { }; let commitment_pda_1_after_spend = Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); - let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk()); + let alice_shared_1_refund = SharedSecretKey([12; 32]); { let recipient_account = state.get_account_by_id(recipient_id); let recipient_nonce = recipient_account.nonce; @@ -4662,7 +4708,7 @@ pub mod tests { vec![( alice_npk, alice_keys.vpk(), - EphemeralPublicKey::from_scalar([12; 32]), + EphemeralPublicKey(vec![12_u8; 1088]), )], output, ) diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs index bf42ad49..c26ac3e7 100644 --- a/lee/state_machine/src/validated_state_diff.rs +++ b/lee/state_machine/src/validated_state_diff.rs @@ -544,7 +544,6 @@ mod tests { use lee_core::{ Commitment, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, - encryption::EphemeralPublicKey, }; use crate::{ @@ -571,9 +570,7 @@ mod tests { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let attacker_esk = [12_u8; 32]; - let attacker_ssk = SharedSecretKey::new(attacker_esk, &attacker_keys.vpk()); - let attacker_epk = EphemeralPublicKey::from_scalar(attacker_esk); + let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); let victim_id = AccountId::new([20_u8; 32]); let recipient_id = AccountId::new([42_u8; 32]); @@ -695,7 +692,6 @@ mod tests { use lee_core::{ Commitment, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, - encryption::EphemeralPublicKey, }; use crate::{ @@ -725,9 +721,7 @@ mod tests { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let attacker_esk = [12_u8; 32]; - let attacker_ssk = SharedSecretKey::new(attacker_esk, &attacker_keys.vpk()); - let attacker_epk = EphemeralPublicKey::from_scalar(attacker_esk); + let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); // Victim is a private account — not registered in public chain state. let victim_keys = test_private_account_keys_2(); diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index 488c8580..5bb6e1b4 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -2,10 +2,10 @@ use common::PINATA_BASE58; use key_protocol::key_management::{ KeyChain, key_tree::chain_index::ChainIndex, - secret_holders::{PrivateKeyHolder, SecretSpendingKey}, + secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}, }; use lee::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; -use lee_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; +use lee_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; const PRIVATE_KEY_PUB_ACC_A: [u8; 32] = [ @@ -38,24 +38,24 @@ const NSK_PRIV_ACC_B: [u8; 32] = [ 23, 99, 9, 4, 177, 230, 125, 109, 91, 160, 30, ]; -const VSK_PRIV_ACC_A: [u8; 32] = [ - 5, 85, 114, 119, 141, 187, 202, 170, 122, 253, 198, 81, 150, 8, 155, 21, 192, 65, 24, 124, 116, - 98, 110, 106, 137, 90, 165, 239, 80, 13, 222, 30, +const VSK_D_PRIV_ACC_A: [u8; 32] = [ + 255, 250, 140, 26, 222, 223, 174, 95, 132, 108, 124, 88, 30, 247, 82, 72, 52, 70, 84, 139, 241, + 187, 41, 163, 19, 231, 232, 122, 225, 55, 134, 184, ]; -const VSK_PRIV_ACC_B: [u8; 32] = [ - 205, 32, 76, 251, 255, 236, 96, 119, 61, 111, 65, 100, 75, 218, 12, 22, 17, 170, 55, 226, 21, - 154, 161, 34, 208, 74, 27, 1, 119, 13, 88, 128, +const VSK_Z_PRIV_ACC_A: [u8; 32] = [ + 225, 24, 98, 78, 31, 203, 175, 248, 213, 17, 133, 207, 10, 135, 132, 151, 59, 184, 5, 81, 28, + 238, 137, 62, 233, 227, 99, 17, 236, 159, 244, 63, ]; -const VPK_PRIV_ACC_A: [u8; 33] = [ - 2, 210, 206, 38, 213, 4, 182, 198, 220, 47, 93, 148, 61, 84, 148, 250, 158, 45, 8, 81, 48, 80, - 46, 230, 87, 210, 47, 204, 76, 58, 214, 167, 81, +const VSK_D_PRIV_ACC_B: [u8; 32] = [ + 128, 85, 85, 103, 226, 218, 119, 56, 60, 252, 31, 113, 232, 215, 156, 2, 159, 247, 156, 192, + 12, 178, 229, 236, 255, 120, 146, 211, 169, 117, 153, 180, ]; -const VPK_PRIV_ACC_B: [u8; 33] = [ - 2, 79, 110, 46, 203, 29, 206, 205, 18, 86, 27, 189, 104, 103, 113, 181, 110, 53, 78, 172, 11, - 171, 190, 18, 126, 214, 81, 77, 192, 154, 58, 195, 238, +const VSK_Z_PRIV_ACC_B: [u8; 32] = [ + 165, 80, 169, 87, 248, 88, 167, 154, 27, 67, 131, 122, 50, 130, 111, 40, 164, 180, 204, 75, + 188, 140, 110, 132, 113, 133, 222, 8, 49, 123, 187, 18, ]; const NPK_PRIV_ACC_A: [u8; 32] = [ @@ -136,20 +136,20 @@ pub fn initial_priv_accounts_private_keys() -> Vec Result { - if self.viewing_public_key_len == 33 { + if self.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts(self.viewing_public_key, self.viewing_public_key_len) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok( + lee_core::encryption::ViewingPublicKey::from_bytes(slice.to_vec()) + .expect("wallet_ffi: length already validated to 1184 bytes"), + ) } else { Err(WalletFfiError::InvalidKeyValue) } diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index d419ea47..29ca5a5e 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -135,11 +135,11 @@ typedef struct FfiPrivateAccountKeys { */ struct FfiBytes32 nullifier_public_key; /** - * viewing public key (compressed secp256k1 point). + * Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes). */ const uint8_t *viewing_public_key; /** - * Length of viewing public key (typically 33 bytes). + * Length of viewing public key (always 1184 bytes for ML-KEM-768). */ uintptr_t viewing_public_key_len; } FfiPrivateAccountKeys; diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index b3570856..29cb514f 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -136,9 +136,9 @@ impl AccountManager { } => { let acc = lee_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -165,9 +165,9 @@ impl AccountManager { } => { let acc = lee_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -365,9 +365,9 @@ async fn private_key_tree_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id); - let eph_holder = EphemeralKeyHolder::new(&from_npk); - let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&from_vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); Ok(AccountPreparedData { nsk: Some(nsk), @@ -405,9 +405,10 @@ async fn private_shared_acc_preparation( .await .unwrap_or(None); - let eph_holder = EphemeralKeyHolder::new(&npk); - let ssk = eph_holder.calculate_shared_secret_sender(&vpk); - let epk = eph_holder.generate_ephemeral_public_key(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + Ok(AccountPreparedData { nsk: Some(nsk), npk, @@ -430,7 +431,7 @@ mod tests { let acc = AccountIdentity::PrivateShared { nsk: [0; 32], npk: NullifierPublicKey([1; 32]), - vpk: ViewingPublicKey::from_scalar([2; 32]), + vpk: ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]), identifier: 42, }; assert!(acc.is_private()); diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index 53c55537..c42a2378 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -51,6 +51,20 @@ pub enum AccountSubcommand { /// Import external account. #[command(subcommand)] Import(ImportSubcommand), + /// Print the npk and vpk for a private account, one per line. + /// + /// Outputs two lines: npk (hex) then vpk (hex). Save to a file and share it + /// with senders so they can reference it with `--to-keys /path/to/file`. + /// + /// ```text + /// wallet account show-keys --account-id Private/... > alice.keys + /// ``` + #[command(name = "show-keys")] + ShowKeys { + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(long)] + account_id: CliAccountMention, + }, } /// Represents generic register CLI subcommand. @@ -225,7 +239,7 @@ impl WalletSubcommand for NewSubcommand { println!("Shared account from group '{group}'"); println!("AccountId: Private/{}", info.account_id); println!("NPK: {}", hex::encode(info.npk.0)); - println!("VPK: {}", hex::encode(&info.vpk.0)); + println!("VPK: {}", hex::encode(info.vpk.to_bytes())); wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::RegisterAccount { @@ -419,6 +433,25 @@ impl WalletSubcommand for AccountSubcommand { Self::Import(import_subcommand) => { import_subcommand.handle_subcommand(wallet_core).await } + Self::ShowKeys { account_id } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Private(account_id) = resolved else { + anyhow::bail!( + "wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts" + ); + }; + let entry = wallet_core + .storage() + .key_chain() + .private_account(account_id) + .ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?; + println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0)); + println!( + "{}", + hex::encode(entry.key_chain.viewing_public_key.to_bytes()) + ); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/lez/wallet/src/cli/group.rs b/lez/wallet/src/cli/group.rs index 3f1f5c67..a4bb12c9 100644 --- a/lez/wallet/src/cli/group.rs +++ b/lez/wallet/src/cli/group.rs @@ -1,6 +1,9 @@ use anyhow::{Context as _, Result}; use clap::Subcommand; -use key_protocol::key_management::group_key_holder::{GroupKeyHolder, SealingPublicKey}; +use key_protocol::key_management::{ + group_key_holder::{GroupKeyHolder, SealingPublicKey}, + secret_holders::ViewingSecretKey, +}; use crate::{ WalletCore, @@ -149,9 +152,15 @@ impl WalletSubcommand for GroupSubcommand { anyhow::bail!("Sealing key already exists. Each wallet has one sealing key."); } - let mut secret: lee_core::encryption::Scalar = [0_u8; 32]; - rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut secret); - let public_key = SealingPublicKey::from_scalar(secret); + let mut d = [0_u8; 32]; + let mut r = [0_u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d); + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut r); + let secret = ViewingSecretKey::new(d, r); + let ek_bytes = lee_core::encryption::ViewingPublicKey::from_seed(&d, &r) + .to_bytes() + .to_vec(); + let public_key = SealingPublicKey::from_bytes(ek_bytes); wallet_core.set_sealing_secret_key(secret); wallet_core.store_persistent_data()?; diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index c6e5ef3e..99b1f0b2 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -285,6 +285,31 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +/// Parse a keys file exported by `wallet account show-keys`. +/// +/// The file format is two lines: +/// - Line 1: npk as hex (64 chars, 32 bytes). +/// - Line 2: vpk as hex (2368 chars, 1184 bytes). +/// +/// Returns `(npk_bytes, vpk_bytes)`. +pub fn read_keys_file(path: &str) -> Result<(Vec, Vec)> { + let content = std::fs::read_to_string(path).with_context(|| { + format!("wallet::cli::read_keys_file: failed to read keys file: {path}") + })?; + let mut lines = content.lines().filter(|l| !l.trim().is_empty()); + let npk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing npk (line 1)") + })?; + let vpk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing vpk (line 2)") + })?; + let npk = hex::decode(npk_hex.trim()) + .context("wallet::cli::read_keys_file: npk in keys file must be valid hex")?; + let vpk = hex::decode(vpk_hex.trim()) + .context("wallet::cli::read_keys_file: vpk in keys file must be valid hex")?; + Ok((npk, vpk)) +} + pub fn read_mnemonic_from_stdin() -> Result { let mut phrase = String::new(); @@ -328,3 +353,77 @@ pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_keys_file_roundtrip() { + let npk = [0xab_u8; 32]; + let vpk = [0xcd_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.keys"); + + // Simulate what `wallet account show-keys` writes. + std::fs::write( + &path, + format!("{}\n{}\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + + assert_eq!(parsed_npk, npk, "npk must round-trip through the keys file"); + assert_eq!( + parsed_vpk, + vpk.to_vec(), + "vpk must round-trip through the keys file" + ); + } + + #[test] + fn read_keys_file_missing_vpk_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("incomplete.keys"); + std::fs::write(&path, format!("{}\n", hex::encode([0xab_u8; 32]))).unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "missing vpk line must return an error"); + assert!( + result.unwrap_err().to_string().contains("missing vpk"), + "error must mention missing vpk" + ); + } + + #[test] + fn read_keys_file_invalid_hex_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("badhex.keys"); + std::fs::write(&path, "not-hex\nalso-not-hex\n").unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "invalid hex must return an error"); + } + + #[test] + fn read_keys_file_ignores_blank_lines() { + let npk = [0x11_u8; 32]; + let vpk = [0x22_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("blanks.keys"); + + // Extra blank lines around the data should be tolerated. + std::fs::write( + &path, + format!("\n{}\n\n{}\n\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + assert_eq!(parsed_npk, npk); + assert_eq!(parsed_vpk, vpk.to_vec()); + } +} diff --git a/lez/wallet/src/cli/programs/native_token_transfer.rs b/lez/wallet/src/cli/programs/native_token_transfer.rs index 144d7603..695dae5f 100644 --- a/lez/wallet/src/cli/programs/native_token_transfer.rs +++ b/lez/wallet/src/cli/programs/native_token_transfer.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; @@ -34,13 +34,17 @@ pub enum AuthTransferSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -100,9 +104,18 @@ impl WalletSubcommand for AuthTransferSubcommand { to: to_account, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + // Resolve --to-keys into --to-npk / --to-vpk equivalents. + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from_account.resolve(wallet_core.storage())?; let to = to_account .as_ref() @@ -258,7 +271,7 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -298,7 +311,7 @@ pub enum NativeTokenTransferProgramSubcommandPrivate { /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -350,11 +363,10 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - lee_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; + let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core) .send_private_transfer_to_outer_account( @@ -427,11 +439,10 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - lee_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; + let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = NativeTokenTransfer(wallet_core) .send_shielded_transfer_to_outer_account( diff --git a/lez/wallet/src/cli/programs/token.rs b/lez/wallet/src/cli/programs/token.rs index 45e73623..5fb8a351 100644 --- a/lez/wallet/src/cli/programs/token.rs +++ b/lez/wallet/src/cli/programs/token.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; @@ -41,13 +41,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -87,13 +91,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] holder: Option, /// `holder_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "holder_keys")] holder_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `holder_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "holder_keys")] holder_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--holder-npk` and `--holder-vpk`. + #[arg(long, conflicts_with_all = ["holder_npk", "holder_vpk"])] + holder_keys: Option, /// Identifier for the holder's private account (only used when minting to a foreign - /// private account via `--holder-npk`/`--holder-vpk`). + /// private account via `--holder-npk`/`--holder-vpk` or `--holder-keys`). #[arg(long)] holder_identifier: Option, /// amount - amount of balance to mint. @@ -170,9 +178,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from.resolve(wallet_core.storage())?; let to = to .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -309,9 +325,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, holder_npk, holder_vpk, + holder_keys, holder_identifier, amount, } => { + let (holder_npk, holder_vpk) = if let Some(path) = holder_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (holder_npk, holder_vpk) + }; + let definition = definition.resolve(wallet_core.storage())?; let holder = holder .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -475,7 +499,7 @@ pub enum TokenProgramSubcommandPrivate { /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -569,7 +593,7 @@ pub enum TokenProgramSubcommandShielded { /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -764,12 +788,12 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; + let recipient_vpk = + lee_core::encryption::ViewingPublicKey::from_bytes(recipient_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_sender, _]) = Token(wallet_core) .send_transfer_transaction_private_foreign_account( @@ -876,12 +900,11 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; + let holder_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(holder_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_definition, _]) = Token(wallet_core) .send_mint_transaction_private_foreign_account( @@ -1032,12 +1055,12 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; + let recipient_vpk = + lee_core::encryption::ViewingPublicKey::from_bytes(recipient_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = Token(wallet_core) .send_transfer_transaction_shielded_foreign_account( @@ -1163,12 +1186,11 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; + let holder_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(holder_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = Token(wallet_core) .send_mint_transaction_shielded_foreign_account( diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index a31fd4db..79a223fe 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -269,7 +269,10 @@ impl WalletCore { } /// Set the wallet's dedicated sealing secret key. - pub const fn set_sealing_secret_key(&mut self, key: lee_core::encryption::Scalar) { + pub const fn set_sealing_secret_key( + &mut self, + key: key_protocol::key_management::secret_holders::ViewingSecretKey, + ) { self.storage.key_chain_mut().set_sealing_secret_key(key); } @@ -723,7 +726,7 @@ impl WalletCore { .storage .key_chain() .private_account_key_chains() - .flat_map(|(_account_id, key_chain, index)| { + .flat_map(|(_account_id, key_chain, _index)| { let view_tag = EncryptedAccountData::compute_view_tag( &key_chain.nullifier_public_key, &key_chain.viewing_public_key, @@ -738,10 +741,8 @@ impl WalletCore { .filter_map(move |(ciph_id, encrypted_data)| { let ciphertext = &encrypted_data.ciphertext; let commitment = &new_commitments[ciph_id]; - let shared_secret = key_chain.calculate_shared_secret_receiver( - &encrypted_data.epk, - index.and_then(ChainIndex::index), - ); + let shared_secret = + key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?; lee_core::EncryptionScheme::decrypt( ciphertext, @@ -823,7 +824,11 @@ impl WalletCore { continue; } - let shared_secret = SharedSecretKey::new(vsk, &encrypted_data.epk); + let Some(shared_secret) = + SharedSecretKey::decapsulate(&encrypted_data.epk, &vsk.d, &vsk.z) + else { + continue; + }; let commitment = &tx.message.new_commitments[ciph_id]; if let Some((_kind, new_acc)) = lee_core::EncryptionScheme::decrypt( diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index c85f5551..f73833eb 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -6,7 +6,7 @@ use key_protocol::key_management::{ KeyChain, group_key_holder::GroupKeyHolder, key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex, traits::KeyTreeNode as _}, - secret_holders::SeedHolder, + secret_holders::{SeedHolder, ViewingSecretKey}, }; use lee::{Account, AccountId}; use lee_core::{Identifier, PrivateAccountKind}; @@ -79,7 +79,7 @@ pub struct UserKeyChain { /// Dedicated sealing secret key for GMS distribution. Generated once via /// `wallet group new-sealing-key`. The corresponding public key is shared with /// group members so they can seal GMS for this wallet. - sealing_secret_key: Option, + sealing_secret_key: Option, } impl UserKeyChain { @@ -509,12 +509,12 @@ impl UserKeyChain { /// Returns the sealing secret key for GMS distribution, if it exists. #[must_use] - pub const fn sealing_secret_key(&self) -> Option { - self.sealing_secret_key + pub const fn sealing_secret_key(&self) -> Option<&ViewingSecretKey> { + self.sealing_secret_key.as_ref() } /// Sets the sealing secret key for GMS distribution. - pub const fn set_sealing_secret_key(&mut self, key: lee_core::encryption::Scalar) { + pub const fn set_sealing_secret_key(&mut self, key: ViewingSecretKey) { self.sealing_secret_key = Some(key); } @@ -584,7 +584,7 @@ impl UserKeyChain { KeyChainPersistentData { accounts, - sealing_secret_key: *sealing_secret_key, + sealing_secret_key: sealing_secret_key.clone(), group_key_holders: group_key_holders.clone(), shared_private_accounts: shared_private_accounts.clone(), } diff --git a/lez/wallet/src/storage/persistent.rs b/lez/wallet/src/storage/persistent.rs index 766e9a90..01d6b166 100644 --- a/lez/wallet/src/storage/persistent.rs +++ b/lez/wallet/src/storage/persistent.rs @@ -5,6 +5,7 @@ use key_protocol::key_management::{ key_tree::{ chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, }, + secret_holders::ViewingSecretKey, }; use serde::{Deserialize, Serialize}; use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData}; @@ -26,7 +27,7 @@ pub struct PersistentStorage { pub struct KeyChainPersistentData { pub accounts: Vec, #[serde(default)] - pub sealing_secret_key: Option, + pub sealing_secret_key: Option, #[serde(default)] pub group_key_holders: BTreeMap, #[serde(default)] diff --git a/tools/crypto_primitives_bench/benches/primitives.rs b/tools/crypto_primitives_bench/benches/primitives.rs index c7584caa..11c11d9b 100644 --- a/tools/crypto_primitives_bench/benches/primitives.rs +++ b/tools/crypto_primitives_bench/benches/primitives.rs @@ -3,7 +3,7 @@ //! Measures: //! - `KeyChain::new_os_random` (mnemonic → SSK → NSK/VSK + public keys) //! - `KeyChain::new_mnemonic` (same, but mnemonic exposed) -//! - `SharedSecretKey::new` (Diffie-Hellman shared key derivation, the per-recipient cost) +//! - `SharedSecretKey::encapsulate` (ML-KEM-768 encapsulation, the per-recipient cost) //! - `EncryptionScheme::encrypt` / `decrypt` (Account note encryption) use std::time::Duration; @@ -13,10 +13,8 @@ use key_protocol::key_management::KeyChain; use lee_core::{ Commitment, EncryptionScheme, SharedSecretKey, account::{Account, AccountId}, - encryption::{EphemeralPublicKey, EphemeralSecretKey}, program::PrivateAccountKind, }; -use rand::{RngCore as _, rngs::OsRng}; fn bench_keychain(c: &mut Criterion) { let mut g = c.benchmark_group("keychain"); @@ -37,34 +35,22 @@ fn bench_shared_secret_key(c: &mut Criterion) { let mut g = c.benchmark_group("shared_secret_key"); g.sample_size(50).noise_threshold(0.05); - g.bench_function("sender_dh", |b| { - b.iter(|| { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - let _epk = EphemeralPublicKey::from(&esk); - SharedSecretKey::new(esk, &vpk) - }); + g.bench_function("sender_encapsulate", |b| { + b.iter(|| SharedSecretKey::encapsulate(&vpk)); }); g.finish(); } fn bench_encryption(c: &mut Criterion) { // One-time setup: a fixed Account/Commitment and a SharedSecretKey to bench - // encrypt/decrypt over a representative note. ESK gen is excluded from the - // measured loop (covered by the SharedSecretKey bench above). + // encrypt/decrypt over a representative note. Encapsulation cost is covered + // by the SharedSecretKey bench above. let recipient_kc = KeyChain::new_os_random(); - let vpk = recipient_kc.viewing_public_key; let npk = recipient_kc.nullifier_public_key; let account = Account::default(); let account_id = AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); - let shared = { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - SharedSecretKey::new(esk, &vpk) - }; + let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key); let kind = PrivateAccountKind::Regular(0_u128); let output_index: u32 = 0; @@ -73,7 +59,6 @@ fn bench_encryption(c: &mut Criterion) { g.bench_function("encrypt", |b| { b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index)); }); - // One ciphertext for the decrypt bench (encrypt is deterministic given inputs). let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index); g.bench_function("decrypt", |b| { b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index)); diff --git a/tools/integration_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs index a6001ebe..483010eb 100644 --- a/tools/integration_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -180,6 +180,7 @@ async fn timed_token_send( to: Some(public_mention(to_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount, }), diff --git a/tools/integration_bench/src/scenarios/fanout.rs b/tools/integration_bench/src/scenarios/fanout.rs index 332e83f6..d230523b 100644 --- a/tools/integration_bench/src/scenarios/fanout.rs +++ b/tools/integration_bench/src/scenarios/fanout.rs @@ -50,6 +50,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/parallel.rs b/tools/integration_bench/src/scenarios/parallel.rs index 24265bc3..3e69ad2b 100644 --- a/tools/integration_bench/src/scenarios/parallel.rs +++ b/tools/integration_bench/src/scenarios/parallel.rs @@ -69,6 +69,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(sender_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER * 5, }), @@ -97,6 +98,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(*recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/private.rs b/tools/integration_bench/src/scenarios/private.rs index e46d6059..be6bb33b 100644 --- a/tools/integration_bench/src/scenarios/private.rs +++ b/tools/integration_bench/src/scenarios/private.rs @@ -46,6 +46,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_a)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -64,6 +65,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(public_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }), @@ -82,6 +84,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_b)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 200, }), diff --git a/tools/integration_bench/src/scenarios/token.rs b/tools/integration_bench/src/scenarios/token.rs index bfc41d5d..3cff19e9 100644 --- a/tools/integration_bench/src/scenarios/token.rs +++ b/tools/integration_bench/src/scenarios/token.rs @@ -41,6 +41,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -61,6 +62,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 500, }),