mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-02 07:09:29 +00:00
Merge ee3cfb6ec64ed3ce00be4dd6cdd8d43f30f3fd18 into d3390efc6db215cef35ba1d6d1f5e13277fe9597
This commit is contained in:
commit
9e3ce2d071
221
Cargo.lock
generated
221
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
@ -1192,7 +1192,7 @@ version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1622,6 +1622,12 @@ dependencies = [
|
||||
"nssa_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"
|
||||
@ -1757,6 +1763,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"
|
||||
@ -2023,17 +2035,22 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"hybrid-array",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto_primitives_bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"criterion",
|
||||
"key_protocol",
|
||||
"nssa_core",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2045,6 +2062,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"
|
||||
@ -2054,7 +2080,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
@ -2186,7 +2212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2195,11 +2221,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"
|
||||
@ -2318,11 +2354,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.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@ -2416,7 +2462,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.5",
|
||||
"reqwest",
|
||||
@ -2448,13 +2494,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]]
|
||||
@ -2463,7 +2509,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",
|
||||
]
|
||||
@ -2525,12 +2571,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",
|
||||
@ -2670,7 +2716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3436,7 +3482,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3558,6 +3604,7 @@ version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
|
||||
dependencies = [
|
||||
"ctutils",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@ -4504,6 +4551,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.1",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "key_protocol"
|
||||
version = "0.1.0"
|
||||
@ -4518,6 +4585,7 @@ dependencies = [
|
||||
"hmac-sha512",
|
||||
"itertools 0.14.0",
|
||||
"k256",
|
||||
"ml-kem",
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"rand 0.8.5",
|
||||
@ -6182,7 +6250,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",
|
||||
]
|
||||
@ -6245,6 +6313,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"
|
||||
@ -6497,10 +6590,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.5",
|
||||
"zeroize",
|
||||
@ -6590,7 +6683,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"bytesize",
|
||||
"chacha20",
|
||||
"k256",
|
||||
"ml-kem",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -7142,9 +7235,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]]
|
||||
@ -7153,8 +7246,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]]
|
||||
@ -7627,7 +7730,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@ -7664,9 +7767,9 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.3",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8126,7 +8229,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"keccak",
|
||||
"keccak 0.1.6",
|
||||
"liblzma",
|
||||
"paste",
|
||||
"rayon",
|
||||
@ -8309,7 +8412,7 @@ dependencies = [
|
||||
"borsh",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"hex",
|
||||
"hex-literal 0.4.1",
|
||||
@ -8348,7 +8451,7 @@ dependencies = [
|
||||
"gdbstub_arch",
|
||||
"gimli",
|
||||
"hex",
|
||||
"keccak",
|
||||
"keccak 0.1.6",
|
||||
"lazy-regex",
|
||||
"num-bigint 0.4.6",
|
||||
"num-traits",
|
||||
@ -8466,16 +8569,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",
|
||||
]
|
||||
@ -8585,7 +8688,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8643,7 +8746,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8791,9 +8894,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",
|
||||
@ -9181,7 +9284,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9192,7 +9295,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]]
|
||||
@ -9226,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",
|
||||
]
|
||||
|
||||
@ -9325,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]]
|
||||
@ -9570,7 +9693,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -10986,7 +11109,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -157,6 +157,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",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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/<account-id> > 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
|
||||
```
|
||||
|
||||
169
docs/specs.md
Normal file
169
docs/specs.md
Normal file
@ -0,0 +1,169 @@
|
||||
# LEE v0.3 specifications for Key Agreement
|
||||
|
||||
## LEE v0.3 basic types and constants
|
||||
|
||||
```rust
|
||||
/// The ML-KEM-768 KEM ciphertext produced during encapsulation (1088 bytes) of a message.
|
||||
/// `EphemeralPublicKey` is used to not confuse the ciphertext of an encrypted private account.
|
||||
type EphemeralPublicKey = [u8; 1088];
|
||||
|
||||
/// Private account Viewing Public Key is a ML-KEM-768 encapsulation key (1184 bytes).
|
||||
type ViewingPublicKey = [u8; 1184];
|
||||
/// The ML-KEM-768 shared secret (32 bytes).
|
||||
type SharedSecretKey = [u8; 32];
|
||||
|
||||
struct EncryptedAccountData {
|
||||
ciphertext: Ciphertext,
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: u8, // 1-byte view tag
|
||||
}
|
||||
```
|
||||
|
||||
#### Key agreement and shared secret
|
||||
|
||||
When creating a private account output, the sender uses the recipient's viewing public key to encapsulate a random message that is used to establish a shared secret between the sender and recipient:
|
||||
|
||||
- Sender: $(\mathsf{ss},\, \mathsf{epk}) = \mathsf{encapsulate}(\mathsf{vpk\_recipient})$. The 1088-byte ciphertext `epk` is included in the transaction as the `EphemeralPublicKey` field.
|
||||
- Receiver: $\mathsf{ss} = \mathsf{decapsulate}(\mathsf{epk},\, vsk.d,\, vsk.z)$
|
||||
|
||||
where `vpk` is the receiver's `ViewingPublicKey` and `(vsk.d, vsk.z)` are the two 32-byte halves of the receiver's `ViewingSecretKey`.
|
||||
|
||||
#### KDF
|
||||
|
||||
```rust
|
||||
fn kdf(
|
||||
shared_secret: &SharedSecretKey, // 32-byte output of the KEM
|
||||
commitment: &Commitment, // 32-byte output commitment
|
||||
output_index: u32, // index of this output within the tx (LE)
|
||||
) -> [u8; 32] {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(b"NSSA/v0.2/KDF-SHA256/");
|
||||
bytes.extend_from_slice(&shared_secret.0);
|
||||
bytes.extend_from_slice(&commitment.to_byte_array());
|
||||
bytes.extend_from_slice(&output_index.to_le_bytes());
|
||||
sha256(bytes)
|
||||
}
|
||||
```
|
||||
|
||||
### Circuit input
|
||||
|
||||
```rust
|
||||
pub enum InputAccountIdentity {
|
||||
/// Public account. The guest reads pre/post state from program_outputs and emits no
|
||||
/// commitment, ciphertext, or nullifier.
|
||||
Public,
|
||||
|
||||
/// Initialization of a standalone private account the caller owns.
|
||||
/// Pre-state must be Account::default().
|
||||
/// AccountId = AccountId::from_private(npk(nsk), identifier).
|
||||
PrivateAuthorizedInit {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
|
||||
/// Update of an existing standalone private account the caller owns.
|
||||
/// Membership proof for the current on-chain commitment is required.
|
||||
PrivateAuthorizedUpdate {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
},
|
||||
|
||||
/// Initialization of a standalone private account the caller does not own
|
||||
/// (e.g. a recipient who does not yet exist on chain). No nsk, no membership proof.
|
||||
PrivateUnauthorized {
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
|
||||
/// Initialization of a private PDA.
|
||||
/// Authorization comes via Claim::Pda(seed) or the caller's pda_seeds.
|
||||
/// The identifier diversifies the PDA within the (program_id, seed, npk) family.
|
||||
PrivatePdaInit {
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == pre_state.account_id`
|
||||
/// rather than requiring a `Claim::Pda` or caller `pda_seeds`. The `pre_state` must
|
||||
/// have `is_authorized == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
},
|
||||
|
||||
/// Update of an existing private PDA. npk is derived from nsk.
|
||||
/// Membership proof is required.
|
||||
PrivatePdaUpdate {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == pre_state.account_id`
|
||||
/// rather than requiring a caller `pda_seeds`. The `pre_state` must have
|
||||
/// `is_authorized == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `ssk` field carries the **shared secret key** — the 32-byte shared secret used to encrypt the post-state. Note that the key protocol uses `ssk` for "spending secret key" (the master key that derives `nsk` and `vsk`); here `ssk` means the per-output KEM shared secret. It is established via ML-KEM-768:
|
||||
|
||||
- Sender: `(ssk, epk) = encapsulate(vpk)`
|
||||
- Receiver: `ssk = decapsulate(epk, vsk.d, vsk.z)`
|
||||
|
||||
where `epk` is the ML-KEM-768 ciphertext (1088 bytes) stored as the `EphemeralPublicKey`, `vpk` is the recipient's `ViewingPublicKey` (1184 bytes), and `(vsk.d, vsk.z)` are the 32-byte seed halves of the recipient's `ViewingSecretKey`.
|
||||
|
||||
## Encrypted private account discovery and tagging
|
||||
|
||||
### Ephemeral view tags
|
||||
|
||||
Each private account output includes a 1-byte view tag to allow wallets to quickly filter outputs before attempting decryption:
|
||||
|
||||
$$\mathsf{ViewTag} = \mathsf{SHA256}(\text{"/LEE/v0.3/ViewTag/"} \;||\; \mathsf{Npk} \;||\; \mathsf{Vpk})[0]$$
|
||||
|
||||
where `Npk` is the 32-byte nullifier public key and `Vpk` is the 1184 byte `ViewingPublicKey` of the recipient. On average only 1 in 256 outputs will pass this filter for a given account, avoiding expensive ML-KEM decapsulation on irrelevant outputs.
|
||||
|
||||
### Private account discovery with viewing keys
|
||||
|
||||
1. For each encrypted output, compute the expected view tag from `(Npk, Vpk)`. Skip if it does not match.
|
||||
2. Decapsulate using ML-KEM-768: `ss = decapsulate(epk, vsk.d, vsk.z)`.
|
||||
3. Run `kdf(ss, commitment, output_index)` to derive the symmetric key.
|
||||
4. Decrypt the ciphertext with ChaCha20.
|
||||
5. Parse the 81-byte header to recover `PrivateAccountKind`.
|
||||
6. Parse the remaining bytes to recover the `Account`.
|
||||
7. Recompute the account ID from the kind and verify that `Commitment::new(account_id, account)` equals the on-chain commitment. Discard on mismatch (false positive).
|
||||
|
||||
```rust
|
||||
fn private_account_discovery(
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
vsk: &ViewingSecretKey,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
) -> Vec<(PrivateAccountKind, Account)> {
|
||||
let expected_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
let mut discovered = Vec::new();
|
||||
|
||||
for (output_index, (encrypted_account, commitment)) in tx.message.encrypted_private_post_states
|
||||
.iter()
|
||||
.zip(&tx.message.new_commitments)
|
||||
.enumerate()
|
||||
{
|
||||
if encrypted_account.view_tag != expected_tag {
|
||||
continue;
|
||||
}
|
||||
let ss = SharedSecretKey::decapsulate(&encrypted_account.epk, &vsk.d, &vsk.z);
|
||||
if let Some((kind, account)) = EncryptionScheme::decrypt(
|
||||
&encrypted_account.ciphertext, &ss, commitment, output_index as u32
|
||||
) {
|
||||
let account_id = AccountId::for_private_account(npk, &kind);
|
||||
if Commitment::new(&account_id, &account) == *commitment {
|
||||
discovered.push((kind, account));
|
||||
}
|
||||
}
|
||||
}
|
||||
discovered
|
||||
}
|
||||
```
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -12,8 +12,9 @@ use nssa::{
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use nssa_core::{
|
||||
InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata,
|
||||
encryption::shared_key_derivation::Secp256k1Point,
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey},
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
@ -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: nssa_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 = MlKem768EncapsulationKey::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)
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -115,6 +115,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,
|
||||
});
|
||||
@ -150,6 +151,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,
|
||||
});
|
||||
@ -233,6 +235,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,
|
||||
});
|
||||
|
||||
@ -195,6 +195,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
@ -234,6 +235,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
@ -345,6 +347,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(
|
||||
nssa_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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,53 +1,61 @@
|
||||
use nssa_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", &"<redacted>")
|
||||
.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)
|
||||
}
|
||||
|
||||
@ -1,44 +1,39 @@
|
||||
use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use nssa_core::{
|
||||
SharedSecretKey,
|
||||
encryption::{Scalar, shared_key_derivation::Secp256k1Point},
|
||||
encryption::{EphemeralPublicKey, MlKem768EncapsulationKey},
|
||||
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<u8>);
|
||||
|
||||
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<u8>) -> 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<u8> {
|
||||
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 = MlKem768EncapsulationKey::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<Self, SealError> {
|
||||
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<Self, SealError> {
|
||||
// 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: nssa_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();
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use k256::{Scalar, elliptic_curve::PrimeField as _};
|
||||
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey};
|
||||
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::MlKem768EncapsulationKey};
|
||||
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 = MlKem768EncapsulationKey::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 = MlKem768EncapsulationKey::from(&vsk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
@ -128,12 +132,11 @@ impl KeyTreeNode for ChildKeysPrivate {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nssa_core::{NullifierPublicKey, NullifierSecretKey};
|
||||
use nssa_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 = nssa_core::NullifierPublicKey([
|
||||
let expected_npk = nssa_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([
|
||||
215, 207, 70, 52, 161, 220, 88, 88, 241, 149, 81, 130, 217, 214, 252, 170, 51, 232,
|
||||
230, 158, 195, 173, 174, 37, 27, 101, 49, 35, 79, 13, 44, 225,
|
||||
]);
|
||||
|
||||
let expected_ccc = [
|
||||
113, 136, 96, 232, 12, 136, 185, 254, 36, 103, 64, 44, 238, 176, 240, 92, 219, 184,
|
||||
143, 35, 183, 54, 170, 15, 126, 56, 115, 21, 89, 142, 236, 217,
|
||||
];
|
||||
|
||||
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,
|
||||
27, 167, 3, 140, 113, 16, 209, 83, 21, 77, 65, 91, 26, 191, 203, 102, 66, 140, 157,
|
||||
220, 101, 104, 227, 135, 216, 215, 216, 126, 194, 196, 43, 34,
|
||||
];
|
||||
let expected_npk = nssa_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,
|
||||
30, 208, 29, 96, 156, 95, 79, 16, 182, 0, 10, 194, 209, 90, 35, 177, 110, 224, 247, 67,
|
||||
219, 114, 113, 16, 42, 27, 220, 96, 151, 124, 8, 65,
|
||||
]);
|
||||
|
||||
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(
|
||||
[
|
||||
81, 154, 68, 152, 72, 163, 82, 17, 125, 156, 193, 135, 129, 93, 227, 55, 224, 104,
|
||||
119, 232, 13, 101, 241, 20, 175, 72, 192, 186, 176, 246, 140, 211,
|
||||
],
|
||||
[
|
||||
31, 40, 109, 41, 185, 61, 173, 79, 102, 171, 158, 245, 232, 71, 57, 157, 142, 117,
|
||||
184, 235, 216, 71, 55, 44, 33, 156, 167, 133, 184, 92, 47, 174,
|
||||
],
|
||||
);
|
||||
|
||||
let expected_vpk: [u8; 1184] = [
|
||||
67, 150, 145, 133, 41, 124, 194, 102, 104, 131, 195, 8, 168, 170, 200, 40, 210, 84, 85,
|
||||
117, 50, 99, 52, 23, 144, 23, 22, 140, 187, 76, 49, 224, 189, 64, 249, 72, 219, 35, 49,
|
||||
162, 146, 121, 27, 179, 183, 215, 84, 177, 62, 37, 103, 97, 209, 201, 8, 162, 38, 109,
|
||||
87, 44, 103, 136, 112, 236, 120, 60, 235, 130, 60, 212, 209, 77, 77, 220, 28, 156, 34,
|
||||
7, 31, 35, 179, 102, 21, 54, 77, 99, 157, 210, 247, 151, 214, 182, 30, 57, 219, 40, 42,
|
||||
188, 32, 30, 134, 126, 7, 22, 51, 241, 152, 8, 96, 5, 87, 168, 64, 62, 81, 247, 33,
|
||||
228, 44, 180, 203, 60, 49, 66, 247, 143, 113, 106, 189, 44, 11, 182, 213, 247, 9, 22,
|
||||
3, 208, 125, 2, 8, 103, 195, 202, 21, 33, 72, 139, 233, 19, 171, 172, 69, 253, 212, 37,
|
||||
197, 66, 165, 207, 168, 69, 18, 24, 1, 100, 200, 175, 163, 247, 115, 17, 124, 84, 183,
|
||||
92, 96, 142, 204, 149, 2, 90, 53, 110, 246, 188, 135, 240, 160, 231, 145, 23, 90, 209,
|
||||
93, 166, 17, 119, 240, 49, 67, 234, 41, 187, 71, 23, 152, 159, 54, 206, 207, 26, 11,
|
||||
32, 134, 202, 185, 201, 25, 59, 199, 182, 18, 236, 175, 254, 227, 195, 98, 52, 139,
|
||||
162, 172, 195, 102, 178, 115, 59, 113, 108, 96, 89, 175, 145, 71, 202, 231, 153, 69, 3,
|
||||
25, 60, 43, 215, 35, 70, 119, 16, 235, 98, 184, 252, 50, 36, 161, 244, 57, 13, 214,
|
||||
115, 106, 225, 166, 7, 59, 44, 130, 197, 85, 69, 220, 81, 10, 1, 130, 227, 225, 47, 78,
|
||||
251, 49, 232, 55, 2, 66, 64, 180, 220, 65, 140, 231, 188, 172, 153, 153, 152, 15, 186,
|
||||
74, 6, 39, 16, 251, 216, 165, 145, 134, 3, 88, 131, 80, 114, 156, 119, 72, 130, 54,
|
||||
159, 202, 23, 7, 130, 127, 156, 252, 113, 108, 85, 22, 120, 104, 12, 151, 187, 102, 64,
|
||||
96, 137, 184, 68, 201, 20, 196, 196, 226, 220, 139, 174, 76, 109, 1, 179, 81, 156, 26,
|
||||
136, 238, 106, 41, 197, 18, 16, 179, 91, 9, 8, 213, 123, 108, 58, 3, 102, 12, 87, 92,
|
||||
217, 207, 166, 131, 17, 218, 134, 170, 27, 129, 145, 0, 65, 85, 99, 163, 97, 78, 228,
|
||||
15, 54, 85, 201, 58, 204, 160, 250, 66, 41, 36, 165, 78, 50, 137, 78, 197, 103, 57, 79,
|
||||
26, 14, 167, 104, 165, 129, 128, 90, 104, 148, 121, 135, 24, 126, 139, 235, 84, 183,
|
||||
165, 115, 111, 83, 48, 184, 55, 84, 250, 115, 171, 195, 91, 114, 213, 104, 51, 110, 86,
|
||||
148, 37, 139, 83, 49, 165, 171, 144, 90, 19, 91, 195, 111, 82, 185, 133, 211, 24, 186,
|
||||
48, 230, 172, 190, 6, 65, 230, 26, 139, 8, 9, 34, 54, 28, 103, 84, 116, 38, 252, 105,
|
||||
86, 123, 40, 31, 39, 64, 14, 253, 215, 147, 182, 218, 111, 148, 2, 18, 3, 197, 4, 129,
|
||||
107, 136, 89, 122, 56, 47, 9, 179, 66, 227, 24, 193, 32, 4, 172, 210, 29, 152, 114,
|
||||
134, 65, 249, 201, 178, 16, 206, 209, 39, 193, 109, 91, 122, 194, 26, 206, 37, 227, 55,
|
||||
160, 214, 85, 196, 64, 97, 96, 66, 80, 34, 177, 83, 200, 44, 137, 175, 149, 114, 42,
|
||||
229, 168, 248, 96, 106, 110, 182, 155, 62, 27, 179, 229, 139, 9, 213, 181, 116, 59,
|
||||
118, 142, 91, 23, 165, 80, 43, 118, 18, 41, 143, 125, 59, 102, 61, 224, 120, 186, 10,
|
||||
63, 119, 241, 168, 196, 87, 117, 138, 3, 151, 1, 129, 76, 154, 87, 200, 114, 124, 90,
|
||||
212, 182, 54, 94, 20, 165, 243, 88, 77, 76, 152, 69, 19, 164, 106, 196, 204, 46, 239,
|
||||
116, 42, 179, 65, 79, 39, 145, 63, 169, 199, 142, 6, 103, 118, 130, 49, 184, 208, 203,
|
||||
36, 162, 216, 9, 188, 17, 86, 45, 35, 20, 178, 218, 121, 164, 243, 145, 57, 208, 130,
|
||||
26, 27, 28, 100, 161, 148, 195, 54, 66, 114, 108, 146, 135, 66, 69, 232, 33, 197, 213,
|
||||
131, 107, 31, 19, 162, 155, 164, 161, 103, 8, 192, 127, 188, 196, 252, 2, 155, 18, 130,
|
||||
105, 53, 235, 200, 87, 203, 162, 95, 50, 158, 96, 210, 1, 45, 8, 26, 3, 192, 201, 182,
|
||||
148, 192, 157, 106, 5, 161, 248, 66, 89, 56, 141, 126, 243, 143, 68, 90, 133, 193, 181,
|
||||
198, 3, 169, 72, 66, 215, 195, 38, 37, 196, 103, 229, 89, 162, 210, 118, 12, 233, 162,
|
||||
95, 164, 107, 97, 11, 120, 255, 164, 60, 117, 37, 108, 144, 185, 167, 40, 124, 69, 23,
|
||||
37, 148, 222, 233, 43, 50, 16, 58, 53, 252, 8, 102, 88, 109, 28, 18, 22, 5, 49, 66,
|
||||
149, 114, 203, 95, 216, 175, 10, 87, 206, 46, 9, 101, 212, 226, 84, 4, 231, 161, 106,
|
||||
185, 31, 6, 101, 27, 54, 49, 85, 54, 84, 12, 250, 4, 49, 184, 134, 186, 23, 146, 54,
|
||||
90, 186, 134, 129, 68, 10, 241, 201, 65, 251, 69, 110, 127, 220, 148, 38, 250, 148, 83,
|
||||
32, 100, 131, 83, 133, 195, 54, 132, 63, 229, 85, 34, 172, 126, 68, 99, 197, 18, 197,
|
||||
91, 221, 234, 66, 203, 156, 73, 46, 0, 54, 205, 11, 52, 172, 114, 193, 127, 171, 134,
|
||||
109, 92, 37, 124, 181, 167, 191, 209, 148, 232, 26, 136, 230, 133, 181, 248, 117, 11,
|
||||
45, 156, 136, 117, 144, 126, 239, 230, 144, 90, 57, 109, 158, 167, 19, 131, 215, 136,
|
||||
85, 136, 10, 49, 9, 146, 64, 81, 28, 171, 53, 78, 40, 225, 94, 238, 70, 174, 125, 186,
|
||||
155, 177, 202, 157, 63, 39, 152, 44, 105, 184, 140, 179, 204, 32, 210, 109, 35, 150,
|
||||
194, 14, 98, 148, 176, 73, 185, 49, 135, 135, 244, 151, 147, 17, 103, 35, 242, 130, 3,
|
||||
158, 198, 152, 83, 240, 198, 254, 145, 181, 67, 163, 14, 237, 249, 179, 252, 220, 67,
|
||||
239, 7, 118, 131, 229, 137, 172, 151, 57, 121, 138, 204, 6, 208, 52, 168, 236, 123,
|
||||
104, 68, 36, 141, 25, 168, 56, 199, 40, 200, 52, 97, 59, 55, 184, 196, 234, 204, 108,
|
||||
75, 65, 177, 82, 207, 127, 128, 157, 0, 68, 163, 127, 152, 85, 123, 209, 163, 21, 119,
|
||||
62, 250, 236, 58, 229, 220, 99, 209, 147, 10, 177, 115, 172, 96, 192, 80, 240, 66, 191,
|
||||
138, 91, 52, 200, 132, 126, 255, 69, 98, 12, 140, 8, 158, 2, 153, 66, 211, 74, 242,
|
||||
147, 148, 209, 6, 161, 76, 149, 158, 209, 163, 20, 76, 75, 192, 193, 162, 71, 134, 72,
|
||||
160, 192, 10, 203, 4, 213, 23, 140, 196, 39, 231, 39, 16, 209, 228, 112, 244, 29, 27,
|
||||
181, 190, 19, 134, 116, 173, 135, 190, 118, 4, 214, 194, 189, 224, 164, 91, 211, 182,
|
||||
162, 226,
|
||||
];
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,21 +69,15 @@ impl KeyChain {
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
&self,
|
||||
ephemeral_public_key_sender: &EphemeralPublicKey,
|
||||
index: Option<u32>,
|
||||
) -> SharedSecretKey {
|
||||
SharedSecretKey::new(
|
||||
self.secret_spending_key.generate_viewing_secret_key(index),
|
||||
ephemeral_public_key_sender,
|
||||
)
|
||||
) -> Option<SharedSecretKey> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
use bip39::Mnemonic;
|
||||
use common::HashType;
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, NullifierSecretKey,
|
||||
encryption::{Scalar, ViewingPublicKey},
|
||||
};
|
||||
use ml_kem;
|
||||
use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::MlKem768EncapsulationKey};
|
||||
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<u32>) -> ViewingSecretKey {
|
||||
pub fn generate_viewing_secret_seed_key(&self, index: Option<u32>) -> 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<u8> = 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<u32>) -> 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 MlKem768EncapsulationKey {
|
||||
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 = <MlKem768 as Kem>::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 {
|
||||
@ -150,8 +192,8 @@ impl PrivateKeyHolder {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_viewing_public_key(&self) -> ViewingPublicKey {
|
||||
ViewingPublicKey::from_scalar(self.viewing_secret_key)
|
||||
pub fn generate_viewing_public_key(&self) -> MlKem768EncapsulationKey {
|
||||
MlKem768EncapsulationKey::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]
|
||||
|
||||
@ -31,6 +31,7 @@ risc0-build = "3.0.3"
|
||||
risc0-binfmt = "3.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
nssa_core = { workspace = true, features = ["test_utils"] }
|
||||
token_core.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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::NssaCoreError;
|
||||
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<u8> {
|
||||
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<Self, NssaCoreError> {
|
||||
let mut value = vec![0; 33];
|
||||
let mut value = vec![0_u8; 1088];
|
||||
cursor.read_exact(&mut value)?;
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<u8>);
|
||||
|
||||
/// 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<u8>);
|
||||
pub struct MlKem768EncapsulationKey(Vec<u8>);
|
||||
|
||||
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<u8>) -> Result<Self, crate::error::NssaCoreError> {
|
||||
if bytes.len() != Self::LEN {
|
||||
return Err(crate::error::NssaCoreError::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<ml_kem::EncapsulationKey768> =
|
||||
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<ml_kem::EncapsulationKey768> =
|
||||
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<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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<u8> = [
|
||||
@ -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 = nssa_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),
|
||||
|
||||
@ -423,7 +423,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,
|
||||
@ -538,7 +538,8 @@ pub mod tests {
|
||||
|
||||
pub struct TestPrivateKeys {
|
||||
pub nsk: NullifierSecretKey,
|
||||
pub vsk: Scalar,
|
||||
pub d: [u8; 32],
|
||||
pub z: [u8; 32],
|
||||
}
|
||||
|
||||
impl TestPrivateKeys {
|
||||
@ -547,7 +548,7 @@ pub mod tests {
|
||||
}
|
||||
|
||||
pub fn vpk(&self) -> ViewingPublicKey {
|
||||
ViewingPublicKey::from_scalar(self.vsk)
|
||||
ViewingPublicKey::from_seed(&self.d, &self.z)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1335,14 +1336,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],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1363,9 +1366,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],
|
||||
@ -1415,13 +1417,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],
|
||||
@ -1485,9 +1485,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],
|
||||
@ -1995,14 +1994,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,
|
||||
},
|
||||
],
|
||||
@ -2041,14 +2050,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,
|
||||
},
|
||||
],
|
||||
@ -2087,14 +2106,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,
|
||||
},
|
||||
],
|
||||
@ -2133,14 +2162,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,
|
||||
},
|
||||
],
|
||||
@ -2179,14 +2218,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,
|
||||
},
|
||||
],
|
||||
@ -2223,14 +2272,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,
|
||||
},
|
||||
],
|
||||
@ -2249,7 +2308,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(),
|
||||
@ -2291,7 +2351,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);
|
||||
@ -2328,7 +2389,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
|
||||
@ -2363,7 +2425,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);
|
||||
@ -2402,7 +2465,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);
|
||||
@ -2440,8 +2504,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);
|
||||
@ -2482,7 +2546,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 =
|
||||
@ -2582,7 +2647,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(),
|
||||
@ -2926,9 +2992,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;
|
||||
|
||||
@ -3032,13 +3097,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();
|
||||
|
||||
@ -3335,9 +3398,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;
|
||||
|
||||
@ -3387,9 +3449,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],
|
||||
@ -3437,9 +3498,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;
|
||||
|
||||
@ -3487,8 +3547,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(
|
||||
@ -3572,7 +3632,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,
|
||||
@ -3598,7 +3659,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,
|
||||
@ -3644,8 +3706,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);
|
||||
@ -3801,9 +3863,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,
|
||||
@ -3871,9 +3932,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(),
|
||||
@ -4427,8 +4487,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.
|
||||
{
|
||||
@ -4456,11 +4518,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();
|
||||
@ -4500,11 +4558,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();
|
||||
@ -4551,11 +4605,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();
|
||||
@ -4596,11 +4646,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();
|
||||
@ -4628,7 +4674,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;
|
||||
@ -4664,7 +4710,7 @@ pub mod tests {
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([12; 32]),
|
||||
EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
|
||||
@ -544,7 +544,6 @@ mod tests {
|
||||
use nssa_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 nssa_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();
|
||||
|
||||
@ -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 nssa::{Account, AccountId, Data, PrivateKey, PublicKey, V03State};
|
||||
use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point};
|
||||
use nssa_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<PrivateAccountPrivateInitialD
|
||||
secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A),
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: NSK_PRIV_ACC_A,
|
||||
viewing_secret_key: VSK_PRIV_ACC_A,
|
||||
viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_A, VSK_Z_PRIV_ACC_A),
|
||||
},
|
||||
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_A),
|
||||
viewing_public_key: Secp256k1Point(VPK_PRIV_ACC_A.to_vec()),
|
||||
viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_A, &VSK_Z_PRIV_ACC_A),
|
||||
};
|
||||
|
||||
let key_chain_2 = KeyChain {
|
||||
secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_B),
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: NSK_PRIV_ACC_B,
|
||||
viewing_secret_key: VSK_PRIV_ACC_B,
|
||||
viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_B, VSK_Z_PRIV_ACC_B),
|
||||
},
|
||||
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_B),
|
||||
viewing_public_key: Secp256k1Point(VPK_PRIV_ACC_B.to_vec()),
|
||||
viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_B, &VSK_Z_PRIV_ACC_B),
|
||||
};
|
||||
|
||||
vec![
|
||||
|
||||
@ -11,6 +11,10 @@ workspace = true
|
||||
[dev-dependencies]
|
||||
key_protocol.workspace = true
|
||||
nssa_core = { workspace = true, features = ["host"] }
|
||||
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
rand = { workspace = true }
|
||||
criterion.workspace = true
|
||||
|
||||
|
||||
@ -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 nssa_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));
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -50,6 +50,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: AMOUNT_PER_TRANSFER,
|
||||
}),
|
||||
|
||||
@ -69,6 +69,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
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<ScenarioOutput> {
|
||||
to: Some(public_mention(*recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: AMOUNT_PER_TRANSFER,
|
||||
}),
|
||||
|
||||
@ -46,6 +46,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
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<ScenarioOutput> {
|
||||
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<ScenarioOutput> {
|
||||
to: Some(private_mention(private_b)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 200,
|
||||
}),
|
||||
|
||||
@ -41,6 +41,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
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<ScenarioOutput> {
|
||||
to: Some(private_mention(private_recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 500,
|
||||
}),
|
||||
|
||||
@ -125,7 +125,7 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys(
|
||||
// NPK is a 32-byte array
|
||||
let npk_bytes = key_chain.nullifier_public_key.0;
|
||||
|
||||
// VPK is a compressed secp256k1 point (33 bytes)
|
||||
// VPK is an ML-KEM-768 encapsulation key (1184 bytes)
|
||||
let vpk_bytes = key_chain.viewing_public_key.to_bytes();
|
||||
let vpk_len = vpk_bytes.len();
|
||||
let vpk_vec = vpk_bytes.to_vec();
|
||||
|
||||
@ -4,7 +4,6 @@ use core::slice;
|
||||
use std::{ffi::c_char, ptr};
|
||||
|
||||
use nssa::Data;
|
||||
use nssa_core::encryption::shared_key_derivation::Secp256k1Point;
|
||||
|
||||
use crate::error::WalletFfiError;
|
||||
|
||||
@ -72,9 +71,9 @@ impl Default for FfiAccount {
|
||||
pub struct FfiPrivateAccountKeys {
|
||||
/// Nullifier public key (32 bytes).
|
||||
pub nullifier_public_key: FfiBytes32,
|
||||
/// viewing public key (compressed secp256k1 point).
|
||||
/// Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes).
|
||||
pub viewing_public_key: *const u8,
|
||||
/// Length of viewing public key (typically 33 bytes).
|
||||
/// Length of viewing public key (always 1184 bytes for ML-KEM-768).
|
||||
pub viewing_public_key_len: usize,
|
||||
}
|
||||
|
||||
@ -161,11 +160,15 @@ impl FfiPrivateAccountKeys {
|
||||
}
|
||||
|
||||
pub fn vpk(&self) -> Result<nssa_core::encryption::ViewingPublicKey, WalletFfiError> {
|
||||
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(
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(slice.to_vec()).expect(
|
||||
"wallet_ffi::types::FfiPrivateAccountKeys::vpk: length already validated above",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Err(WalletFfiError::InvalidKeyValue)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -136,9 +136,9 @@ impl AccountManager {
|
||||
} => {
|
||||
let acc = nssa_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 = nssa_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());
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: nssa_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 z = [0_u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d);
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut z);
|
||||
let secret = ViewingSecretKey::new(d, z);
|
||||
let ek_bytes = nssa_core::encryption::ViewingPublicKey::from_seed(&d, &z)
|
||||
.to_bytes()
|
||||
.to_vec();
|
||||
let public_key = SealingPublicKey::from_bytes(ek_bytes);
|
||||
|
||||
wallet_core.set_sealing_secret_key(secret);
|
||||
wallet_core.store_persistent_data()?;
|
||||
|
||||
@ -285,6 +285,31 @@ pub fn read_password_from_stdin() -> Result<String> {
|
||||
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<u8>, Vec<u8>)> {
|
||||
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<Mnemonic> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
@ -34,13 +34,17 @@ pub enum AuthTransferSubcommand {
|
||||
#[arg(long)]
|
||||
to: Option<CliAccountMention>,
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_npk: Option<String>,
|
||||
/// `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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<u128>,
|
||||
/// 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,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
|
||||
to_npk.copy_from_slice(&to_npk_res);
|
||||
let to_npk = nssa_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_res = hex::decode(&to_vpk)
|
||||
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
|
||||
let to_vpk =
|
||||
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec());
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::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 +440,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
|
||||
to_npk.copy_from_slice(&to_npk_res);
|
||||
let to_npk = nssa_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_res = hex::decode(&to_vpk)
|
||||
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
|
||||
let to_vpk =
|
||||
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec());
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(to_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, _) = NativeTokenTransfer(wallet_core)
|
||||
.send_shielded_transfer_to_outer_account(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
@ -41,13 +41,17 @@ pub enum TokenProgramAgnosticSubcommand {
|
||||
#[arg(long)]
|
||||
to: Option<CliAccountMention>,
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_npk: Option<String>,
|
||||
/// `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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<u128>,
|
||||
/// amount - amount of balance to move.
|
||||
@ -87,13 +91,17 @@ pub enum TokenProgramAgnosticSubcommand {
|
||||
#[arg(long)]
|
||||
holder: Option<CliAccountMention>,
|
||||
/// `holder_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "holder_keys")]
|
||||
holder_npk: Option<String>,
|
||||
/// `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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<u128>,
|
||||
/// 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 = nssa_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 = nssa_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 =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::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,12 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
holder_npk.copy_from_slice(&holder_npk_res);
|
||||
let holder_npk = nssa_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 = nssa_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 =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::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 +1056,12 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
recipient_npk.copy_from_slice(&recipient_npk_res);
|
||||
let recipient_npk = nssa_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 = nssa_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 =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::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 +1187,12 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
holder_npk.copy_from_slice(&holder_npk_res);
|
||||
let holder_npk = nssa_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 = nssa_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 =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(holder_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, _) = Token(wallet_core)
|
||||
.send_mint_transaction_shielded_foreign_account(
|
||||
|
||||
@ -269,7 +269,10 @@ impl WalletCore {
|
||||
}
|
||||
|
||||
/// Set the wallet's dedicated sealing secret key.
|
||||
pub const fn set_sealing_secret_key(&mut self, key: nssa_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);
|
||||
}
|
||||
|
||||
@ -725,7 +728,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,
|
||||
@ -740,10 +743,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)?;
|
||||
|
||||
nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
@ -825,7 +826,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)) = nssa_core::EncryptionScheme::decrypt(
|
||||
|
||||
@ -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 log::{debug, warn};
|
||||
use nssa::{Account, AccountId};
|
||||
@ -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<nssa_core::encryption::Scalar>,
|
||||
sealing_secret_key: Option<ViewingSecretKey>,
|
||||
}
|
||||
|
||||
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<nssa_core::encryption::Scalar> {
|
||||
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: nssa_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(),
|
||||
}
|
||||
|
||||
@ -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<PersistentAccountData>,
|
||||
#[serde(default)]
|
||||
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
|
||||
pub sealing_secret_key: Option<ViewingSecretKey>,
|
||||
#[serde(default)]
|
||||
pub group_key_holders: BTreeMap<Label, GroupKeyHolder>,
|
||||
#[serde(default)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user