feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
299
Cargo.lock
generated
299
Cargo.lock
generated
@@ -106,6 +106,17 @@ dependencies = [
|
|||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -142,6 +153,12 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
@@ -154,6 +171,22 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cast"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -318,6 +351,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -446,6 +485,12 @@ version = "0.2.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -456,6 +501,30 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -510,6 +579,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idfoto-cli"
|
name = "idfoto-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -542,6 +620,20 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idfoto-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"data-encoding",
|
||||||
|
"hmac",
|
||||||
|
"idfoto-core",
|
||||||
|
"js-sys",
|
||||||
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.10"
|
version = "0.25.10"
|
||||||
@@ -579,12 +671,30 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.184"
|
version = "0.2.184"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@@ -621,6 +731,16 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minicov"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -641,6 +761,15 @@ dependencies = [
|
|||||||
"pxfm",
|
"pxfm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -648,6 +777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -723,12 +853,24 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oorandom"
|
||||||
|
version = "11.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -781,6 +923,12 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -936,6 +1084,21 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -991,6 +1154,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -1002,6 +1176,12 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -1017,6 +1197,12 @@ version = "0.3.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1144,12 +1330,116 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.68"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-test"
|
||||||
|
version = "0.3.68"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bb55e2540ad1c56eec35fd63e2aea15f83b11ce487fd2de9ad11578dfc047ea"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"cast",
|
||||||
|
"js-sys",
|
||||||
|
"libm",
|
||||||
|
"minicov",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"num-traits",
|
||||||
|
"oorandom",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-bindgen-test-macro",
|
||||||
|
"wasm-bindgen-test-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-test-macro"
|
||||||
|
version = "0.3.68"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caf0ca1bd612b988616bac1ab34c4e4290ef18f7148a1d8b7f31c150080e9295"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-test-shared"
|
||||||
|
version = "0.2.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -1172,6 +1462,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"crates/idfoto-core",
|
"crates/idfoto-core",
|
||||||
"crates/idfoto-cli",
|
"crates/idfoto-cli",
|
||||||
|
"crates/idfoto-wasm",
|
||||||
]
|
]
|
||||||
|
|||||||
20
crates/idfoto-wasm/Cargo.toml
Normal file
20
crates/idfoto-wasm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "idfoto-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "WASM bindings for idfoto password manager"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
idfoto-core = { path = "../idfoto-core" }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
|
serde_json = "1"
|
||||||
|
hmac = "0.12"
|
||||||
|
sha1 = "0.10"
|
||||||
|
data-encoding = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3"
|
||||||
328
crates/idfoto-wasm/src/lib.rs
Normal file
328
crates/idfoto-wasm/src/lib.rs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
//! WASM bindings for the idfoto password manager.
|
||||||
|
//!
|
||||||
|
//! This crate wraps [`idfoto_core`] for use in a Chrome MV3 browser extension via
|
||||||
|
//! `wasm-bindgen`. Every function marked `#[wasm_bindgen]` is callable from
|
||||||
|
//! JavaScript after loading the compiled `.wasm` module.
|
||||||
|
//!
|
||||||
|
//! All crypto operations run entirely in the browser -- the extension never sends
|
||||||
|
//! secrets to any server. The TOTP function lets the extension generate live 6-digit
|
||||||
|
//! authenticator codes without a separate authenticator app.
|
||||||
|
//!
|
||||||
|
//! ## Design notes
|
||||||
|
//!
|
||||||
|
//! - Functions accept and return `Vec<u8>`, `&[u8]`, and `String` -- wasm-bindgen
|
||||||
|
//! handles the JS ↔ Rust marshalling automatically (typed arrays for bytes, strings
|
||||||
|
//! for JSON).
|
||||||
|
//! - Errors are mapped to `JsValue` strings so they surface as thrown exceptions in JS.
|
||||||
|
//! - `generate_password` and `generate_entry_id` use `js_sys::Math::random()` because
|
||||||
|
//! `OsRng`/`getrandom` requires special WASM configuration. `Math.random()` is
|
||||||
|
//! sufficient for these non-security-critical operations (password character selection
|
||||||
|
//! and identifier generation).
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use idfoto_core::crypto::{self, KdfParams};
|
||||||
|
use idfoto_core::entry::Entry;
|
||||||
|
use idfoto_core::vault;
|
||||||
|
use idfoto_core::imgsecret;
|
||||||
|
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
/// Derive a 256-bit master key from a passphrase, image secret, salt, and KDF parameters.
|
||||||
|
///
|
||||||
|
/// The `params_json` argument is a JSON object with fields `argon2_m`, `argon2_t`,
|
||||||
|
/// and `argon2_p` (matching [`KdfParams`]). Example:
|
||||||
|
///
|
||||||
|
/// ```json
|
||||||
|
/// {"argon2_m": 65536, "argon2_t": 3, "argon2_p": 4}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns a 32-byte `Uint8Array` in JavaScript.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn derive_master_key(
|
||||||
|
passphrase: &str,
|
||||||
|
image_secret: &[u8],
|
||||||
|
salt: &[u8],
|
||||||
|
params_json: &str,
|
||||||
|
) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let params: KdfParams =
|
||||||
|
serde_json::from_str(params_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
let image_secret: &[u8; 32] = image_secret
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("image_secret must be exactly 32 bytes"))?;
|
||||||
|
let salt: &[u8; 32] = salt
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("salt must be exactly 32 bytes"))?;
|
||||||
|
|
||||||
|
let key = crypto::derive_master_key(passphrase.as_bytes(), image_secret, salt, ¶ms)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(key.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt arbitrary plaintext bytes under a 256-bit key using XChaCha20-Poly1305.
|
||||||
|
///
|
||||||
|
/// Returns the ciphertext as a `Uint8Array` in the format:
|
||||||
|
/// `version(1) || nonce(24) || ciphertext+tag`.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
crypto::encrypt(key, plaintext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a ciphertext blob produced by [`encrypt`], returning the original plaintext.
|
||||||
|
///
|
||||||
|
/// Returns the plaintext as a `Uint8Array`. Throws if the key is wrong or the data
|
||||||
|
/// has been tampered with.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decrypt(ciphertext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
crypto::decrypt(key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the 32-byte steganographic secret from a JPEG image.
|
||||||
|
///
|
||||||
|
/// Returns a 32-byte `Uint8Array` containing the embedded secret.
|
||||||
|
/// Throws if the image is not a valid JPEG or the secret cannot be recovered.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let secret =
|
||||||
|
imgsecret::extract(jpeg_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
Ok(secret.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt an [`Entry`] (given as a JSON string) under the master key.
|
||||||
|
///
|
||||||
|
/// The `entry_json` must deserialize into an [`Entry`] struct. Returns the
|
||||||
|
/// ciphertext as a `Uint8Array`.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn encrypt_entry(entry_json: &str, key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
let entry: Entry =
|
||||||
|
serde_json::from_str(entry_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
vault::encrypt_entry(key, &entry).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt an entry ciphertext blob and return the entry as a JSON string.
|
||||||
|
///
|
||||||
|
/// Throws if the key is wrong, the data is tampered, or the decrypted JSON is malformed.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decrypt_entry(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
let entry =
|
||||||
|
vault::decrypt_entry(key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
serde_json::to_string(&entry).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt a [`Manifest`] (given as a JSON string) under the master key.
|
||||||
|
///
|
||||||
|
/// Returns the ciphertext as a `Uint8Array`.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
let manifest: idfoto_core::entry::Manifest =
|
||||||
|
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
vault::encrypt_manifest(key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a manifest ciphertext blob and return the manifest as a JSON string.
|
||||||
|
///
|
||||||
|
/// Throws if the key is wrong, the data is tampered, or the decrypted JSON is malformed.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decrypt_manifest(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue> {
|
||||||
|
let key: &[u8; 32] = key
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
|
let manifest = vault::decrypt_manifest(key, ciphertext)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
serde_json::to_string(&manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a 6-digit TOTP code per RFC 6238.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `secret_base32`: the shared secret encoded in base32 (with or without padding).
|
||||||
|
/// - `timestamp_secs`: the current Unix timestamp in seconds.
|
||||||
|
///
|
||||||
|
/// # Algorithm
|
||||||
|
///
|
||||||
|
/// 1. Decode the base32 secret.
|
||||||
|
/// 2. Compute the time step: `T = timestamp_secs / 30`.
|
||||||
|
/// 3. Compute `HMAC-SHA1(secret, T as big-endian u64)`.
|
||||||
|
/// 4. Dynamic truncation: extract a 4-byte segment from the HMAC output at an
|
||||||
|
/// offset determined by the last nibble.
|
||||||
|
/// 5. Mask the high bit, take modulo 10^6, and zero-pad to 6 digits.
|
||||||
|
///
|
||||||
|
/// Returns a 6-character string like `"287082"`.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn generate_totp(secret_base32: &str, timestamp_secs: u64) -> Result<String, JsValue> {
|
||||||
|
generate_totp_inner(secret_base32, timestamp_secs)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inner TOTP implementation that returns a standard Result for testability
|
||||||
|
/// (avoids depending on JsValue in native tests).
|
||||||
|
fn generate_totp_inner(
|
||||||
|
secret_base32: &str,
|
||||||
|
timestamp_secs: u64,
|
||||||
|
) -> std::result::Result<String, String> {
|
||||||
|
// Normalize: strip whitespace, uppercase, remove padding for lenient decode,
|
||||||
|
// then re-pad to a multiple of 8 for strict base32.
|
||||||
|
let cleaned: String = secret_base32
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !c.is_whitespace())
|
||||||
|
.collect::<String>()
|
||||||
|
.to_uppercase()
|
||||||
|
.trim_end_matches('=')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Re-pad to a multiple of 8 characters (base32 requirement).
|
||||||
|
let padded = {
|
||||||
|
let remainder = cleaned.len() % 8;
|
||||||
|
if remainder == 0 {
|
||||||
|
cleaned
|
||||||
|
} else {
|
||||||
|
let pad_count = 8 - remainder;
|
||||||
|
format!("{}{}", cleaned, "=".repeat(pad_count))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret = data_encoding::BASE32
|
||||||
|
.decode(padded.as_bytes())
|
||||||
|
.map_err(|e| format!("invalid base32 secret: {}", e))?;
|
||||||
|
|
||||||
|
// Time step: T = floor(timestamp / 30)
|
||||||
|
let time_step = timestamp_secs / 30;
|
||||||
|
|
||||||
|
// HMAC-SHA1(secret, time_step as big-endian u64)
|
||||||
|
type HmacSha1 = Hmac<Sha1>;
|
||||||
|
let mut mac =
|
||||||
|
HmacSha1::new_from_slice(&secret).map_err(|e| format!("HMAC init failed: {}", e))?;
|
||||||
|
mac.update(&time_step.to_be_bytes());
|
||||||
|
let result = mac.finalize().into_bytes();
|
||||||
|
|
||||||
|
// Dynamic truncation per RFC 4226 section 5.4
|
||||||
|
let offset = (result[19] & 0x0F) as usize;
|
||||||
|
let code = ((result[offset] as u32 & 0x7F) << 24)
|
||||||
|
| ((result[offset + 1] as u32) << 16)
|
||||||
|
| ((result[offset + 2] as u32) << 8)
|
||||||
|
| (result[offset + 3] as u32);
|
||||||
|
|
||||||
|
// 6-digit code, zero-padded
|
||||||
|
Ok(format!("{:06}", code % 1_000_000))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random password of the given length.
|
||||||
|
///
|
||||||
|
/// Uses `js_sys::Math::random()` for randomness (not cryptographically secure,
|
||||||
|
/// but sufficient for password character selection). The character set includes
|
||||||
|
/// uppercase, lowercase, digits, and common symbols.
|
||||||
|
///
|
||||||
|
/// This function is only available in WASM -- it will panic in native builds
|
||||||
|
/// because `js_sys::Math::random()` requires a JS runtime.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn generate_password(length: u32) -> String {
|
||||||
|
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
||||||
|
|
||||||
|
(0..length)
|
||||||
|
.map(|_| {
|
||||||
|
let idx = (js_sys::Math::random() * CHARSET.len() as f64) as usize;
|
||||||
|
CHARSET[idx % CHARSET.len()] as char
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random 8-character hex string for use as an entry ID.
|
||||||
|
///
|
||||||
|
/// Uses `js_sys::Math::random()` for randomness. Entry IDs are not
|
||||||
|
/// security-sensitive -- they are just opaque identifiers.
|
||||||
|
///
|
||||||
|
/// This function is only available in WASM -- it will panic in native builds
|
||||||
|
/// because `js_sys::Math::random()` requires a JS runtime.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn generate_entry_id() -> String {
|
||||||
|
(0..4)
|
||||||
|
.map(|_| {
|
||||||
|
let byte = (js_sys::Math::random() * 256.0) as u8;
|
||||||
|
format!("{:02x}", byte)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_rfc6238_test_vector() {
|
||||||
|
// secret = "12345678901234567890" ASCII, time = 59, expected = "287082"
|
||||||
|
let secret_b32 = data_encoding::BASE32.encode(b"12345678901234567890");
|
||||||
|
let result = generate_totp_inner(&secret_b32, 59).unwrap();
|
||||||
|
assert_eq!(result, "287082");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_rfc6238_test_vector_2() {
|
||||||
|
// time = 1111111109, expected = "081804"
|
||||||
|
let secret_b32 = data_encoding::BASE32.encode(b"12345678901234567890");
|
||||||
|
let result = generate_totp_inner(&secret_b32, 1111111109).unwrap();
|
||||||
|
assert_eq!(result, "081804");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_rfc6238_test_vector_3() {
|
||||||
|
// time = 1234567890, expected = "005924"
|
||||||
|
let secret_b32 = data_encoding::BASE32.encode(b"12345678901234567890");
|
||||||
|
let result = generate_totp_inner(&secret_b32, 1234567890).unwrap();
|
||||||
|
assert_eq!(result, "005924");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn totp_invalid_base32_fails() {
|
||||||
|
let result = generate_totp_inner("not-valid-base32!!!", 1000);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_key_via_wasm_wrapper() {
|
||||||
|
let params = r#"{"argon2_m":256,"argon2_t":1,"argon2_p":1}"#;
|
||||||
|
let key =
|
||||||
|
derive_master_key("test-passphrase", &[0x42u8; 32], &[0x01u8; 32], params).unwrap();
|
||||||
|
assert_eq!(key.len(), 32);
|
||||||
|
let key2 =
|
||||||
|
derive_master_key("test-passphrase", &[0x42u8; 32], &[0x01u8; 32], params).unwrap();
|
||||||
|
assert_eq!(key, key2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_decrypt_via_wasm_wrapper() {
|
||||||
|
let key = [0xABu8; 32];
|
||||||
|
let ciphertext = encrypt(b"hello wasm", &key).unwrap();
|
||||||
|
let decrypted = decrypt(&ciphertext, &key).unwrap();
|
||||||
|
assert_eq!(decrypted, b"hello wasm");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_entry_decrypt_entry_round_trip() {
|
||||||
|
let key = [0xABu8; 32];
|
||||||
|
let entry_json = r#"{"name":"Test","password":"secret","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"}"#;
|
||||||
|
let ciphertext = encrypt_entry(entry_json, &key).unwrap();
|
||||||
|
let result = decrypt_entry(&ciphertext, &key).unwrap();
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||||
|
assert_eq!(parsed["name"], "Test");
|
||||||
|
assert_eq!(parsed["password"], "secret");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user