Initital Sender state machine logic implemented
Implemented and tested the state machine model for the `Sender`. Tested against the rust bindings for libsodium using the `crypto_box` api.
This commit is contained in:
parent
f3ab842cc2
commit
cc39f8a0a8
4 changed files with 530 additions and 535 deletions
477
Cargo.lock
generated
477
Cargo.lock
generated
|
|
@ -2,12 +2,48 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.53"
|
||||
|
|
@ -25,12 +61,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "1.5.3"
|
||||
name = "core2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
|
||||
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||
dependencies = [
|
||||
"signature",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "dary_heap"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04"
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301"
|
||||
dependencies = [
|
||||
"dtor-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor-proc-macro"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -39,6 +154,23 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
|
|
@ -51,6 +183,43 @@ dependencies = [
|
|||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
|
|
@ -74,17 +243,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libsodium-sys"
|
||||
version = "0.2.7"
|
||||
name = "libflate"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
|
||||
checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"core2",
|
||||
"crc32fast",
|
||||
"dary_heap",
|
||||
"libflate_lz77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libflate_lz77"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"hashbrown",
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsodium-rs"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7a6a6c03c914ffa9e53f76300ab4e81a86230f8b884c7f224eb6617ab685878"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"ctor",
|
||||
"libc",
|
||||
"libsodium-sys-stable",
|
||||
"pkg-config",
|
||||
"thiserror 1.0.69",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsodium-sys-stable"
|
||||
version = "1.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5d23f4a051a13cf1085b2c5a050d4d890d80c754534cc4247eff525fa5283d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libflate",
|
||||
"minisign-verify",
|
||||
"pkg-config",
|
||||
"walkdir",
|
||||
"tar",
|
||||
"ureq",
|
||||
"vcpkg",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
|
|
@ -95,19 +331,41 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
|||
name = "messagebox-protocol"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libsodium-rs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sodiumoxide",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
|
|
@ -138,21 +396,40 @@ version = "5.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
@ -203,22 +480,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.6.4"
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
|
||||
|
||||
[[package]]
|
||||
name = "sodiumoxide"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028"
|
||||
dependencies = [
|
||||
"ed25519",
|
||||
"libc",
|
||||
"libsodium-sys",
|
||||
"serde",
|
||||
]
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
|
|
@ -231,13 +496,33 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -251,12 +536,54 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
|
|
@ -270,14 +597,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
|
|
@ -333,15 +656,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
|
@ -363,8 +677,69 @@ version = "0.51.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "7.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9013f1222db8a6d680f13a7ccdc60a781199cd09c2fa4eff58e728bb181757fc"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sodiumoxide = "0.2"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
uuid = { version = "1.19", features = ["v4", "serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
libsodium-rs = "0.2.2" # The library itself is agnostic to implementation, but we need something to test against
|
||||
|
|
|
|||
|
|
@ -760,19 +760,6 @@ Result:
|
|||
|
||||
---
|
||||
|
||||
## Future Enhancements (Out of Scope)
|
||||
|
||||
- Public key fingerprint verification
|
||||
- Sender reply channel (optional contact info)
|
||||
- Message categories/tags
|
||||
- Read receipts
|
||||
- Storage quotas and auto-cleanup
|
||||
- Multi-device reader support
|
||||
- Message threading
|
||||
- Rate limiting and spam prevention
|
||||
|
||||
---
|
||||
|
||||
## Cryptographic Primitives
|
||||
|
||||
**Sealed Box:** NaCl/libsodium compatible
|
||||
|
|
@ -847,83 +834,6 @@ Result:
|
|||
- Key ID is preserved from PublicKey through MessageRecord
|
||||
- Enables key rotation without sender awareness
|
||||
|
||||
### Testing Considerations
|
||||
|
||||
**Essential Test Coverage:**
|
||||
1. State transitions (valid and invalid)
|
||||
2. Input validation (empty, max, over-max)
|
||||
3. Cryptographic randomness (same input → different output)
|
||||
4. Memory cleanup (plaintext destroyed after sealing)
|
||||
5. Unicode and special character handling
|
||||
6. Multiple messages in sequence
|
||||
7. Timestamp correctness
|
||||
8. UUID uniqueness
|
||||
|
||||
**Error Cases:**
|
||||
- Invalid state transitions
|
||||
- Missing public key
|
||||
- Missing plaintext
|
||||
- Missing sealed message
|
||||
- Invalid sender name (empty, too long)
|
||||
- Invalid message body (empty, too long)
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions (from Implementation)
|
||||
|
||||
### Public Key Structure
|
||||
**Decision:** PublicKey contains both key_id and public_key bytes
|
||||
- **Rationale:** Sender needs key_id to include in MessageRecord
|
||||
- **Alternative considered:** Fetch key_id separately from store
|
||||
- **Chosen approach:** Bundle them together to reduce round trips
|
||||
|
||||
### State Machine Strictness
|
||||
**Decision:** Enforce state transitions with compile-time types would be ideal, but runtime checks are practical
|
||||
- **Rationale:** Makes invalid states unrepresentable
|
||||
- **Trade-off:** More complex type system vs runtime flexibility
|
||||
- **Implementation:** Runtime checks with explicit error types
|
||||
|
||||
### Plaintext Ownership
|
||||
**Decision:** Move (consume) plaintext during seal operation
|
||||
- **Rationale:** Prevents accidental reuse or logging
|
||||
- **Alternative considered:** Clone and clear
|
||||
- **Chosen approach:** Rust's move semantics provide memory safety guarantee
|
||||
|
||||
### Reset from Any State
|
||||
**Decision:** Allow reset from any state, not just SUBMITTED or ERROR
|
||||
- **Rationale:** Provides escape hatch for error recovery
|
||||
- **Trade-off:** Less strict state machine vs operational flexibility
|
||||
- **Chosen approach:** Flexible reset for better UX
|
||||
|
||||
### Timestamp Precision
|
||||
**Decision:** Unix epoch seconds (not milliseconds)
|
||||
- **Rationale:** Second precision sufficient for message ordering
|
||||
- **Trade-off:** Less precision vs simpler arithmetic
|
||||
- **Chosen approach:** Seconds for simplicity
|
||||
|
||||
### Character vs Byte Limits
|
||||
**Decision:** Enforce character limits, not byte limits
|
||||
- **Rationale:** User-facing (WYSIWYG behavior)
|
||||
- **Trade-off:** Unicode overhead vs user expectations
|
||||
- **Chosen approach:** Character counting for better UX
|
||||
|
||||
### Error Type Design
|
||||
**Decision:** Use enum with contextual error messages
|
||||
- **Rationale:** Structured errors enable better error handling
|
||||
- **Alternative considered:** String errors
|
||||
- **Chosen approach:** Typed errors with Display impl for messages
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Authentication mechanism** for Reader → Store operations (key rotation, deletion)
|
||||
2. **Public key verification** - how does sender know public_key is authentic?
|
||||
3. **Rate limiting** - should store impose submission limits?
|
||||
4. **Message expiry** - auto-delete after N days?
|
||||
5. **Multi-device reader** - how to distribute private keys securely?
|
||||
6. **Sealed box size** - current spec says max 50KB, but actual max message is 10KB chars + overhead (~48 bytes). Should store validate based on plaintext limit or total size?
|
||||
7. **Key ID format** - should it be standardized (e.g., hash of public key) or implementation-defined?
|
||||
8. **Concurrent key rotation** - if sender caches public key, how long is cache valid?
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
|
|
|||
492
src/sender.rs
492
src/sender.rs
|
|
@ -1,6 +1,4 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sodiumoxide::crypto::box_::{self, PublicKey as SodiumPublicKey};
|
||||
use sodiumoxide::crypto::sealedbox;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -54,11 +52,8 @@ impl std::fmt::Display for SenderState {
|
|||
}
|
||||
|
||||
/// Public key information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PublicKey {
|
||||
pub key_id: String,
|
||||
pub public_key: [u8; 32],
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PublicKey(pub [u8; 32]);
|
||||
|
||||
/// Plaintext message before encryption
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -68,10 +63,11 @@ struct PlaintextMessage {
|
|||
}
|
||||
|
||||
/// Sealed message ready for transmission
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct SealedMessage {
|
||||
sender_name: String,
|
||||
key_id: String,
|
||||
public_key: PublicKey,
|
||||
sealed_box: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +77,7 @@ pub struct MessageRecord {
|
|||
pub message_id: Uuid,
|
||||
pub sender_name: String,
|
||||
pub created_at: i64, // Unix timestamp
|
||||
pub key_id: String,
|
||||
pub public_key: PublicKey,
|
||||
pub sealed_box: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +167,11 @@ impl SenderClient {
|
|||
}
|
||||
|
||||
/// Transition: COMPOSING → ENCRYPTING
|
||||
/// Seal the message using the loaded public key
|
||||
pub fn seal_message(&mut self) -> Result<(), SenderError> {
|
||||
/// Seal the message using the loaded public key and a provided cryptographic method
|
||||
pub fn seal_message<F>(&mut self, sealer: F) -> Result<(), SenderError>
|
||||
where
|
||||
F: FnOnce(String, &PublicKey) -> Option<Vec<u8>>,
|
||||
{
|
||||
if self.state != SenderState::Composing {
|
||||
return Err(SenderError::InvalidStateTransition {
|
||||
from: self.state.to_string(),
|
||||
|
|
@ -183,16 +182,13 @@ impl SenderClient {
|
|||
let public_key = self.public_key.as_ref().ok_or(SenderError::NoPublicKey)?;
|
||||
let plaintext = self.plaintext.take().ok_or(SenderError::NoPlaintext)?;
|
||||
|
||||
// Convert public key bytes to sodiumoxide type
|
||||
let pk = SodiumPublicKey::from_slice(&public_key.public_key)
|
||||
.ok_or(SenderError::EncryptionFailed)?;
|
||||
|
||||
// Encrypt the message body
|
||||
let sealed_box = sealedbox::seal(plaintext.message_body.as_bytes(), &pk);
|
||||
let sealed_box =
|
||||
sealer(plaintext.message_body, public_key).ok_or(SenderError::EncryptionFailed)?;
|
||||
|
||||
self.sealed_message = Some(SealedMessage {
|
||||
sender_name: plaintext.sender_name,
|
||||
key_id: public_key.key_id.clone(),
|
||||
public_key: public_key.clone(),
|
||||
sealed_box,
|
||||
});
|
||||
|
||||
|
|
@ -216,6 +212,8 @@ impl SenderClient {
|
|||
.take()
|
||||
.ok_or(SenderError::NoSealedMessage)?;
|
||||
|
||||
let public_key = self.public_key.as_ref().ok_or(SenderError::NoPublicKey)?;
|
||||
|
||||
let record = MessageRecord {
|
||||
message_id: Uuid::new_v4(),
|
||||
sender_name: sealed.sender_name,
|
||||
|
|
@ -223,7 +221,7 @@ impl SenderClient {
|
|||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64,
|
||||
key_id: sealed.key_id,
|
||||
public_key: public_key.clone(),
|
||||
sealed_box: sealed.sealed_box,
|
||||
};
|
||||
|
||||
|
|
@ -264,203 +262,93 @@ impl Default for SenderClient {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libsodium_rs as ls;
|
||||
|
||||
fn create_test_public_key() -> super::PublicKey {
|
||||
// Initialize sodiumoxide
|
||||
sodiumoxide::init().unwrap();
|
||||
fn create_test_public_key() -> PublicKey {
|
||||
ls::ensure_init().unwrap();
|
||||
|
||||
// Generate a real keypair for testing
|
||||
let (pk, _sk) = box_::gen_keypair();
|
||||
PublicKey(
|
||||
ls::crypto_box::KeyPair::generate()
|
||||
.public_key
|
||||
.as_bytes()
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
super::PublicKey {
|
||||
key_id: "test-key-1".to_string(),
|
||||
public_key: pk.0,
|
||||
}
|
||||
// Generic message sealing function for testing
|
||||
fn generic_sealer() -> Box<dyn FnOnce(String, &PublicKey) -> Option<Vec<u8>>> {
|
||||
ls::ensure_init().unwrap();
|
||||
|
||||
Box::new(|message, recipient_pk| {
|
||||
match ls::crypto_box::seal_box(message.as_bytes(), &recipient_pk.0.into()) {
|
||||
Ok(sealed) => Some(sealed),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_sender_starts_idle() {
|
||||
let sender = SenderClient::new();
|
||||
assert_eq!(sender.state(), SenderState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_public_key_transitions_to_ready() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
let result = sender.load_public_key(pk);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(sender.state(), SenderState::Ready);
|
||||
assert!(sender.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_load_key_when_not_idle() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
|
||||
let result = sender.load_public_key(pk);
|
||||
assert!(matches!(result, Err(SenderError::InvalidStateTransition { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_message_transitions_to_composing() {
|
||||
fn test_bad_messages_dont_parse() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
"Hello, Bob!".to_string(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(sender.state(), SenderState::Composing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_rejects_empty_sender_name() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let result = sender.compose_message(
|
||||
"".to_string(),
|
||||
"Hello!".to_string(),
|
||||
);
|
||||
// Empty sender name should fail
|
||||
let result = sender.compose_message("".to_string(), "Hello!".to_string());
|
||||
assert!(matches!(result, Err(SenderError::InvalidSenderName(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_rejects_long_sender_name() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
// Sender name too long should fail
|
||||
let long_name = "a".repeat(201);
|
||||
let result = sender.compose_message(
|
||||
long_name,
|
||||
"Hello!".to_string(),
|
||||
);
|
||||
let result = sender.compose_message(long_name, "Hello!".to_string());
|
||||
assert!(matches!(result, Err(SenderError::InvalidSenderName(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_rejects_empty_message_body() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
"".to_string(),
|
||||
);
|
||||
// Empty message body should fail
|
||||
let result = sender.compose_message("Alice".to_string(), "".to_string());
|
||||
assert!(matches!(result, Err(SenderError::InvalidMessageBody(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_rejects_long_message_body() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let long_message = "a".repeat(10_001);
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
long_message,
|
||||
);
|
||||
// Message body too long should fail
|
||||
let long_body = "a".repeat(10_001);
|
||||
let result = sender.compose_message("Alice".to_string(), long_body);
|
||||
assert!(matches!(result, Err(SenderError::InvalidMessageBody(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seal_message_transitions_to_encrypting() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
|
||||
let result = sender.seal_message();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(sender.state(), SenderState::Encrypting);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seal_destroys_plaintext() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
|
||||
sender.seal_message().unwrap();
|
||||
|
||||
// Plaintext should be None after sealing
|
||||
assert!(sender.plaintext.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_creates_message_record() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
|
||||
let result = sender.submit();
|
||||
assert!(result.is_ok());
|
||||
|
||||
let record = result.unwrap();
|
||||
assert_eq!(record.sender_name, "Alice");
|
||||
assert_eq!(record.key_id, pk.key_id);
|
||||
assert!(!record.sealed_box.is_empty());
|
||||
assert_eq!(sender.state(), SenderState::Submitted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_transitions_to_idle() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
sender.submit().unwrap();
|
||||
|
||||
sender.reset();
|
||||
assert_eq!(sender.state(), SenderState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_happy_path() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
let kp = ls::crypto_box::KeyPair::generate();
|
||||
let pk = PublicKey(kp.public_key.as_bytes().clone());
|
||||
|
||||
let message = "Hello, Bob!".to_string();
|
||||
|
||||
// IDLE → READY
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
assert_eq!(sender.state(), SenderState::Ready);
|
||||
|
||||
// READY → COMPOSING
|
||||
sender.compose_message("Alice".to_string(), "Hello, Bob!".to_string()).unwrap();
|
||||
sender
|
||||
.compose_message("Alice".to_string(), message.clone())
|
||||
.unwrap();
|
||||
assert_eq!(sender.state(), SenderState::Composing);
|
||||
|
||||
let sealer = generic_sealer();
|
||||
|
||||
// COMPOSING → ENCRYPTING
|
||||
sender.seal_message().unwrap();
|
||||
sender.seal_message(sealer).unwrap();
|
||||
assert_eq!(sender.state(), SenderState::Encrypting);
|
||||
|
||||
// Ensure the sealed message is decryptable with the corresponding secret
|
||||
let unboxed = {
|
||||
let sealed_box = sender.sealed_message.as_ref().unwrap().sealed_box.clone();
|
||||
ls::crypto_box::open_sealed_box(&sealed_box, &pk.0.into(), &kp.secret_key).unwrap()
|
||||
};
|
||||
assert_eq!(unboxed.as_slice(), message.as_bytes());
|
||||
|
||||
// ENCRYPTING → SUBMITTED
|
||||
let record = sender.submit().unwrap();
|
||||
assert_eq!(sender.state(), SenderState::Submitted);
|
||||
assert_eq!(record.sender_name, "Alice");
|
||||
assert_eq!(record.key_id, pk.key_id);
|
||||
assert_eq!(record.public_key, pk);
|
||||
|
||||
// SUBMITTED → IDLE
|
||||
sender.reset();
|
||||
|
|
@ -473,13 +361,21 @@ mod tests {
|
|||
|
||||
// Cannot compose without loading key
|
||||
let result = sender.compose_message("Alice".to_string(), "Hello!".to_string());
|
||||
assert!(matches!(result, Err(SenderError::InvalidStateTransition { .. })));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(SenderError::InvalidStateTransition { .. })
|
||||
));
|
||||
|
||||
// Cannot seal without composing
|
||||
let pk = create_test_public_key();
|
||||
sender.load_public_key(pk).unwrap();
|
||||
let result = sender.seal_message();
|
||||
assert!(matches!(result, Err(SenderError::InvalidStateTransition { .. })));
|
||||
|
||||
let sealer = generic_sealer();
|
||||
let result = sender.seal_message(sealer);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(SenderError::InvalidStateTransition { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -488,37 +384,16 @@ mod tests {
|
|||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender
|
||||
.compose_message("Alice".to_string(), "Hello!".to_string())
|
||||
.unwrap();
|
||||
|
||||
// Try to submit without sealing
|
||||
let result = sender.submit();
|
||||
assert!(matches!(result, Err(SenderError::InvalidStateTransition { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_at_max_length() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
// Exactly 10,000 characters should be allowed
|
||||
let max_message = "a".repeat(10_000);
|
||||
let result = sender.compose_message("Alice".to_string(), max_message);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sender_name_at_max_length() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
// Exactly 200 characters should be allowed
|
||||
let max_name = "a".repeat(200);
|
||||
let result = sender.compose_message(max_name, "Hello!".to_string());
|
||||
assert!(result.is_ok());
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(SenderError::InvalidStateTransition { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -544,15 +419,22 @@ mod tests {
|
|||
// Test reset from COMPOSING state
|
||||
let mut sender = SenderClient::new();
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender
|
||||
.compose_message("Alice".to_string(), "Hello!".to_string())
|
||||
.unwrap();
|
||||
sender.reset();
|
||||
assert_eq!(sender.state(), SenderState::Idle);
|
||||
|
||||
// Test reset from ENCRYPTING state
|
||||
let mut sender = SenderClient::new();
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
sender
|
||||
.compose_message("Alice".to_string(), "Hello!".to_string())
|
||||
.unwrap();
|
||||
|
||||
let sealer = generic_sealer();
|
||||
|
||||
sender.seal_message(sealer).unwrap();
|
||||
sender.reset();
|
||||
assert_eq!(sender.state(), SenderState::Idle);
|
||||
}
|
||||
|
|
@ -564,15 +446,20 @@ mod tests {
|
|||
|
||||
// First message
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "First message".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
sender
|
||||
.compose_message("Alice".to_string(), "First message".to_string())
|
||||
.unwrap();
|
||||
|
||||
sender.seal_message(generic_sealer()).unwrap();
|
||||
let record1 = sender.submit().unwrap();
|
||||
sender.reset();
|
||||
|
||||
// Second message
|
||||
sender.load_public_key(pk.clone()).unwrap();
|
||||
sender.compose_message("Bob".to_string(), "Second message".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
sender
|
||||
.compose_message("Bob".to_string(), "Second message".to_string())
|
||||
.unwrap();
|
||||
sender.seal_message(generic_sealer()).unwrap();
|
||||
let record2 = sender.submit().unwrap();
|
||||
sender.reset();
|
||||
|
||||
|
|
@ -581,181 +468,4 @@ mod tests {
|
|||
assert_eq!(record1.sender_name, "Alice");
|
||||
assert_eq!(record2.sender_name, "Bob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sealed_box_is_not_empty() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Secret message".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
let record = sender.submit().unwrap();
|
||||
|
||||
// Sealed box should contain encrypted data
|
||||
assert!(!record.sealed_box.is_empty());
|
||||
// Sealed box should be longer than the plaintext due to crypto overhead
|
||||
assert!(record.sealed_box.len() > "Secret message".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_record_has_valid_timestamp() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
let before = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
let record = sender.submit().unwrap();
|
||||
|
||||
let after = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
// Timestamp should be between before and after
|
||||
assert!(record.created_at >= before);
|
||||
assert!(record.created_at <= after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_record_preserves_key_id() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
let expected_key_id = pk.key_id.clone();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
sender.seal_message().unwrap();
|
||||
let record = sender.submit().unwrap();
|
||||
|
||||
assert_eq!(record.key_id, expected_key_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_messages_produce_different_ciphertext() {
|
||||
let pk = create_test_public_key();
|
||||
|
||||
// First message
|
||||
let mut sender1 = SenderClient::new();
|
||||
sender1.load_public_key(pk.clone()).unwrap();
|
||||
sender1.compose_message("Alice".to_string(), "Message A".to_string()).unwrap();
|
||||
sender1.seal_message().unwrap();
|
||||
let record1 = sender1.submit().unwrap();
|
||||
|
||||
// Second message (different content)
|
||||
let mut sender2 = SenderClient::new();
|
||||
sender2.load_public_key(pk).unwrap();
|
||||
sender2.compose_message("Alice".to_string(), "Message B".to_string()).unwrap();
|
||||
sender2.seal_message().unwrap();
|
||||
let record2 = sender2.submit().unwrap();
|
||||
|
||||
// Ciphertexts should be different
|
||||
assert_ne!(record1.sealed_box, record2.sealed_box);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_same_message_produces_different_ciphertext() {
|
||||
// Due to sealed box randomness, same plaintext should produce different ciphertext
|
||||
let pk = create_test_public_key();
|
||||
|
||||
// First encryption
|
||||
let mut sender1 = SenderClient::new();
|
||||
sender1.load_public_key(pk.clone()).unwrap();
|
||||
sender1.compose_message("Alice".to_string(), "Same message".to_string()).unwrap();
|
||||
sender1.seal_message().unwrap();
|
||||
let record1 = sender1.submit().unwrap();
|
||||
|
||||
// Second encryption of same message
|
||||
let mut sender2 = SenderClient::new();
|
||||
sender2.load_public_key(pk).unwrap();
|
||||
sender2.compose_message("Alice".to_string(), "Same message".to_string()).unwrap();
|
||||
sender2.seal_message().unwrap();
|
||||
let record2 = sender2.submit().unwrap();
|
||||
|
||||
// Ciphertexts should be different (sealed box uses randomness)
|
||||
assert_ne!(record1.sealed_box, record2.sealed_box);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_in_sender_name() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let result = sender.compose_message(
|
||||
"Alice 🎉 Müller".to_string(),
|
||||
"Hello!".to_string(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_in_message_body() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
"Hello 世界! 🌍".to_string(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_newlines_in_message_body() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let multiline_message = "Line 1\nLine 2\nLine 3";
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
multiline_message.to_string(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_special_characters_in_message() {
|
||||
let mut sender = SenderClient::new();
|
||||
let pk = create_test_public_key();
|
||||
|
||||
sender.load_public_key(pk).unwrap();
|
||||
|
||||
let special_message = "Special: !@#$%^&*()[]{}|\\;':\"<>?,./`~";
|
||||
let result = sender.compose_message(
|
||||
"Alice".to_string(),
|
||||
special_message.to_string(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_ready_only_when_key_loaded() {
|
||||
let mut sender = SenderClient::new();
|
||||
assert!(!sender.is_ready());
|
||||
|
||||
let pk = create_test_public_key();
|
||||
sender.load_public_key(pk).unwrap();
|
||||
assert!(sender.is_ready());
|
||||
|
||||
sender.compose_message("Alice".to_string(), "Hello!".to_string()).unwrap();
|
||||
assert!(!sender.is_ready()); // Not ready in COMPOSING state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_creates_idle_sender() {
|
||||
let sender = SenderClient::default();
|
||||
assert_eq!(sender.state(), SenderState::Idle);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue