diff --git a/README.md b/README.md index 19ceb60..3205405 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,28 @@ manifest can call the config plugin instead, which adds the same entries: } ``` +## Encrypted export + +`encryptExport` turns an app-defined JSON payload into an +[age](https://age-encryption.org) file encrypted to a passphrase, so a backup can +be moved between devices by any means. `decryptExport` opens files that this +library or `age -p -a` produced. Both run the scrypt key derivation in the Rust +core on a native worker, off the JavaScript thread. + +```ts +const file = await encryptExport(JSON.stringify(backup), passphrase); +await FileSystem.writeAsStringAsync(path, file); + +const backup = JSON.parse(await decryptExport(await readAsStringAsync(path), passphrase)); +``` + +Rejections carry a `code`: + +| `code` | Cause | +| --- | --- | +| `WrongPassphrase` | The passphrase does not open the file. | +| `InvalidExport` | The payload exceeds 16 MiB, the passphrase is shorter than 8 characters when encrypting, or the input is not an intact age file. | + ## License Apache-2.0. See [LICENSE](./LICENSE). diff --git a/android/src/main/java/ai/kilo/reactnativessh/ReactNativeSshModule.kt b/android/src/main/java/ai/kilo/reactnativessh/ReactNativeSshModule.kt index d1acac7..d38b864 100644 --- a/android/src/main/java/ai/kilo/reactnativessh/ReactNativeSshModule.kt +++ b/android/src/main/java/ai/kilo/reactnativessh/ReactNativeSshModule.kt @@ -18,6 +18,8 @@ import ai.kilo.reactnativessh.uniffi.SshSession import ai.kilo.reactnativessh.uniffi.TransferProgress import ai.kilo.reactnativessh.uniffi.categoryIsRetryable import ai.kilo.reactnativessh.uniffi.coreVersion +import ai.kilo.reactnativessh.uniffi.decryptExport +import ai.kilo.reactnativessh.uniffi.encryptExport import ai.kilo.reactnativessh.uniffi.generateEd25519Key import ai.kilo.reactnativessh.uniffi.parsePrivateKey import android.content.Context @@ -92,6 +94,14 @@ class ReactNativeSshModule : Module() { ) } + AsyncFunction("encryptExport") { plaintextJson: String, passphrase: String -> + translateFailures { encryptExport(plaintextJson, passphrase) } + } + + AsyncFunction("decryptExport") { armored: String, passphrase: String -> + translateFailures { decryptExport(armored, passphrase) } + } + AsyncFunction("connect").SuspendBody { config: ConnectConfigRecord -> connectSession(config) { handler -> SshSession.connect(config.toNative(), handler) } } diff --git a/android/src/main/java/ai/kilo/reactnativessh/SshErrors.kt b/android/src/main/java/ai/kilo/reactnativessh/SshErrors.kt index 178224c..07cfea4 100644 --- a/android/src/main/java/ai/kilo/reactnativessh/SshErrors.kt +++ b/android/src/main/java/ai/kilo/reactnativessh/SshErrors.kt @@ -22,6 +22,7 @@ private fun SshException.code(): String = when (this) { is SshException.UnsupportedAlgorithm -> "UnsupportedAlgorithm" is SshException.KeyParse -> "KeyParse" is SshException.WrongPassphrase -> "WrongPassphrase" + is SshException.InvalidExport -> "InvalidExport" is SshException.ChannelClosed -> "ChannelClosed" is SshException.Protocol -> "Protocol" is SshException.KeyGeneration -> "KeyGeneration" diff --git a/ios/ReactNativeSshModule.swift b/ios/ReactNativeSshModule.swift index 7627958..0fa616b 100644 --- a/ios/ReactNativeSshModule.swift +++ b/ios/ReactNativeSshModule.swift @@ -41,6 +41,14 @@ public final class ReactNativeSshModule: Module { ) } + AsyncFunction("encryptExport") { (plaintextJson: String, passphrase: String) throws -> String in + try translate { try encryptExport(plaintextJson: plaintextJson, passphrase: passphrase) } + } + + AsyncFunction("decryptExport") { (armored: String, passphrase: String) throws -> String in + try translate { try decryptExport(armored: armored, passphrase: passphrase) } + } + AsyncFunction("connect") { (config: ConnectConfigRecord) async throws -> String in try await self.connectSession(config) } diff --git a/ios/SshErrors.swift b/ios/SshErrors.swift index 31a061b..2dd9f79 100644 --- a/ios/SshErrors.swift +++ b/ios/SshErrors.swift @@ -25,6 +25,8 @@ extension SshError { return "KeyParse" case .WrongPassphrase: return "WrongPassphrase" + case .InvalidExport: + return "InvalidExport" case .ChannelClosed: return "ChannelClosed" case .Protocol: @@ -51,6 +53,7 @@ extension SshError { .AuthFailed(let message), .UnsupportedAlgorithm(let message), .KeyParse(let message), + .InvalidExport(let message), .ChannelClosed(let message), .Protocol(let message), .KeyGeneration(let message), diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6f1e15b..ea7a9ef 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,14 +2,24 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + [[package]] name = "aead" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.2.2", + "inout 0.2.6", ] [[package]] @@ -18,9 +28,9 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fc36d7440d015b45468ba08a21e80e056ebbd014c611c6298bb7503e04bd1" dependencies = [ - "cipher", + "cipher 0.5.2", "cpubits", - "cpufeatures", + "cpufeatures 0.3.1", "zeroize", ] @@ -30,15 +40,58 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" dependencies = [ - "aead", + "aead 0.6.1", "aes", - "cipher", + "cipher 0.5.2", "ctr", "ctutils", "ghash", "zeroize", ] +[[package]] +name = "age" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "047a482d1843edf1ce76ada63183698144030fe1191bd5ddba6e41e164e0bc43" +dependencies = [ + "age-core", + "base64", + "bech32", + "chacha20poly1305", + "cookie-factory", + "hmac 0.12.1", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand 0.8.8", + "rust-embed", + "scrypt 0.11.0", + "sha2 0.10.9", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64", + "chacha20poly1305", + "cookie-factory", + "hkdf 0.12.4", + "io_tee", + "nom", + "rand 0.8.8", + "secrecy", + "sha2 0.10.9", +] + [[package]] name = "android_system_properties" version = "0.1.6" @@ -60,6 +113,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arc-swap" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.6.0" @@ -68,7 +130,7 @@ checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.3.1", "password-hash", ] @@ -97,7 +159,7 @@ dependencies = [ "memchr", "proc-macro2", "quote", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "syn 3.0.10", @@ -118,7 +180,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c5ae75772275d268b03ab8bdccdd12117b6169ee23256942b34e46c9f476583" dependencies = [ - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "unicode-ident", @@ -148,6 +210,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64ct" version = "1.8.3" @@ -170,10 +238,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" dependencies = [ "blowfish", - "pbkdf2", - "sha2", + "pbkdf2 0.13.0", + "sha2 0.11.1", ] +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bitflags" version = "2.13.2" @@ -189,7 +263,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" dependencies = [ - "digest", + "digest 0.11.3", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", ] [[package]] @@ -218,7 +301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" dependencies = [ "byteorder", - "cipher", + "cipher 0.5.2", ] [[package]] @@ -251,7 +334,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -276,6 +359,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.2" @@ -283,9 +377,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", - "rand_core", + "cipher 0.5.2", + "cpufeatures 0.3.1", + "rand_core 0.10.1", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] @@ -302,15 +409,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", +] + [[package]] name = "cipher" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer", - "crypto-common", - "inout", + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.6", "zeroize", ] @@ -365,6 +483,15 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be95d4a884358a21a199689323c29e9256b4a1b315e22f183d155422041d3a9a" +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -377,6 +504,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.1" @@ -403,12 +539,22 @@ dependencies = [ "getrandom 0.4.3", "hybrid-array", "num-traits", - "rand_core", + "rand_core 0.10.1", "serdect", "subtle", "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.2" @@ -417,7 +563,7 @@ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "getrandom 0.4.3", "hybrid-array", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -427,7 +573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" dependencies = [ "crypto-bigint", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -436,7 +582,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -449,6 +595,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "5.0.0" @@ -456,11 +617,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rand_core", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -525,7 +686,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" dependencies = [ - "cipher", + "cipher 0.5.2", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -534,12 +706,23 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer", + "block-buffer 0.12.1", "const-oid", - "crypto-common", + "crypto-common 0.2.2", "ctutils", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + [[package]] name = "ecdsa" version = "0.17.0" @@ -547,7 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", - "digest", + "digest 0.11.3", "elliptic-curve", "rfc6979", "signature", @@ -571,11 +754,11 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 5.0.0", "ed25519", - "rand_core", + "rand_core 0.10.1", "serde", - "sha2", + "sha2 0.11.1", "signature", "subtle", "zeroize", @@ -589,15 +772,15 @@ checksum = "900f37e7e18d74a2347389af6cd69e3a30d5cc9e697f40004865679aa75e9c7c" dependencies = [ "base16ct", "crypto-bigint", - "crypto-common", - "digest", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", "group", - "hkdf", + "hkdf 0.13.0", "hybrid-array", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -643,22 +826,81 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core", + "rand_core 0.10.1", "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "find-msvc-tools" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fs-err" version = "3.3.2" @@ -756,6 +998,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "generic-array" version = "1.4.5" @@ -787,7 +1039,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -837,7 +1089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core", + "rand_core 0.10.1", "subtle", ] @@ -871,13 +1123,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hkdf" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "hmac", + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", ] [[package]] @@ -886,7 +1156,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -901,6 +1171,72 @@ dependencies = [ "zeroize", ] +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -937,6 +1273,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "inout" version = "0.2.6" @@ -947,6 +1292,31 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + [[package]] name = "itoa" version = "1.0.19" @@ -971,7 +1341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", ] [[package]] @@ -980,10 +1350,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" dependencies = [ - "crypto-common", - "rand_core", + "crypto-common 0.2.2", + "rand_core 0.10.1", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libc" version = "0.2.191" @@ -1023,6 +1399,22 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mime" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5848f5a9e8117cf94f2f90faac24a87ff81600ff8628c1e3deaab572e76bfa1" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1050,7 +1442,7 @@ dependencies = [ "kem", "module-lattice", "pkcs8", - "rand_core", + "rand_core 0.10.1", "sha3", "shake", ] @@ -1096,8 +1488,8 @@ checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" dependencies = [ "num-integer", "num-traits", - "rand", - "rand_core", + "rand 0.10.3", + "rand_core 0.10.1", ] [[package]] @@ -1124,6 +1516,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "p256" version = "0.14.0" @@ -1134,7 +1532,7 @@ dependencies = [ "elliptic-curve", "primefield", "primeorder", - "sha2", + "sha2 0.11.1", ] [[package]] @@ -1145,10 +1543,10 @@ checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ "ecdsa", "elliptic-curve", - "fiat-crypto", + "fiat-crypto 0.3.0", "primefield", "primeorder", - "sha2", + "sha2 0.11.1", ] [[package]] @@ -1162,7 +1560,7 @@ dependencies = [ "elliptic-curve", "primefield", "primeorder", - "sha2", + "sha2 0.11.1", ] [[package]] @@ -1177,14 +1575,24 @@ dependencies = [ "delegate", "futures", "log", - "rand", - "sha2", - "thiserror", + "rand 0.10.3", + "sha2 0.11.1", + "thiserror 2.0.21", "tokio", "windows", "windows-strings", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -1207,14 +1615,24 @@ dependencies = [ "phc", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "pbkdf2" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest", - "hmac", + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -1242,6 +1660,26 @@ dependencies = [ "ctutils", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1268,10 +1706,10 @@ dependencies = [ "aes-gcm", "cbc", "der", - "pbkdf2", - "rand_core", - "scrypt", - "sha2", + "pbkdf2 0.13.0", + "rand_core 0.10.1", + "scrypt 0.12.0", + "sha2 0.11.1", "spki", ] @@ -1283,7 +1721,7 @@ checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ "der", "pkcs5", - "rand_core", + "rand_core 0.10.1", "spki", ] @@ -1293,14 +1731,25 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + [[package]] name = "poly1305" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures", - "universal-hash", + "cpufeatures 0.3.1", + "universal-hash 0.6.1", "zeroize", ] @@ -1311,11 +1760,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", - "cpufeatures", - "universal-hash", + "cpufeatures 0.3.1", + "universal-hash 0.6.1", "zeroize", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "primefield" version = "0.14.0" @@ -1323,9 +1781,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ "crypto-bigint", - "crypto-common", + "crypto-common 0.2.2", "ff", - "rand_core", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -1343,6 +1801,28 @@ dependencies = [ "wnaf", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1367,15 +1847,45 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "chacha20", + "chacha20 0.10.2", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1400,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ "crypto-bigint", - "hmac", + "hmac 0.13.0", ] [[package]] @@ -1426,11 +1936,11 @@ dependencies = [ "const-oid", "crypto-bigint", "crypto-primes", - "digest", + "digest 0.11.3", "pkcs1", "pkcs8", - "rand_core", - "sha2", + "rand_core 0.10.1", + "sha2 0.11.1", "signature", "spki", "zeroize", @@ -1448,25 +1958,25 @@ dependencies = [ "byteorder", "bytes", "cbc", - "cipher", + "cipher 0.5.2", "crypto-bigint", "ctr", - "curve25519-dalek", + "curve25519-dalek 5.0.0", "data-encoding", "delegate", "der", - "digest", + "digest 0.11.3", "ecdsa", "ed25519-dalek", "elliptic-curve", "enum_dispatch", "futures", - "generic-array", + "generic-array 1.4.5", "getrandom 0.4.3", "ghash", "hex-literal", - "hmac", - "inout", + "hmac 0.13.0", + "inout 0.2.6", "keccak", "log", "md5", @@ -1477,32 +1987,32 @@ dependencies = [ "p384", "p521", "pageant", - "pbkdf2", + "pbkdf2 0.13.0", "pkcs1", "pkcs5", "pkcs8", "polyval", - "rand", - "rand_core", + "rand 0.10.3", + "rand_core 0.10.1", "ring", "rsa", "russh-cryptovec", "russh-util", - "salsa20", - "scrypt", + "salsa20 0.11.0", + "scrypt 0.12.0", "sec1", "sha1", - "sha2", + "sha2 0.11.1", "sha3", "signature", "spki", "ssh-encoding", "ssh-key", "subtle", - "thiserror", + "thiserror 2.0.21", "tokio", "typenum", - "universal-hash", + "universal-hash 0.6.1", "zeroize", ] @@ -1532,7 +2042,7 @@ dependencies = [ "log", "serde", "serde_bytes", - "thiserror", + "thiserror 2.0.21", "tokio", "tokio-util", "wasm-bindgen-futures", @@ -1550,6 +2060,47 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "rust-embed" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98f580cf7a2863aeb6827800283f52cbe6fb3f3bb7c1843783f903c16dcb8ad" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93e2f69cf224a6c7df7509aa5ba4a23f56828ee41b040e92ac8762371eb9ba7" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915e33dccbe7deb2b8a342dc4c517503fe85e0230ddbae40a4fb0220493dad9b" +dependencies = [ + "sha2 0.11.1", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1584,6 +2135,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "salsa20" version = "0.11.0" @@ -1591,7 +2151,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher", + "cipher 0.5.2", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", ] [[package]] @@ -1620,6 +2189,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20 0.10.2", + "sha2 0.10.9", +] + [[package]] name = "scrypt" version = "0.12.0" @@ -1627,9 +2207,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ "cfg-if", - "pbkdf2", - "salsa20", - "sha2", + "pbkdf2 0.13.0", + "salsa20 0.11.0", + "sha2 0.11.1", ] [[package]] @@ -1646,6 +2226,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.3.0", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + [[package]] name = "semver" version = "1.0.28" @@ -1731,8 +2335,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -1742,8 +2357,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -1752,7 +2367,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -1763,7 +2378,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ - "digest", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -1780,8 +2395,8 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest", - "rand_core", + "digest 0.11.3", + "rand_core 0.10.1", ] [[package]] @@ -1840,14 +2455,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" dependencies = [ - "aead", + "aead 0.6.1", "aes", "aes-gcm", - "chacha20", - "cipher", + "chacha20 0.10.2", + "cipher 0.5.2", "ctutils", "des", - "poly1305", + "poly1305 0.9.1", "ssh-encoding", "zeroize", ] @@ -1856,12 +2471,15 @@ dependencies = [ name = "ssh-core" version = "0.0.0" dependencies = [ + "age", "async-trait", "getrandom 0.4.3", "russh", "russh-sftp", + "sha2 0.10.9", "tokio", "uniffi", + "zeroize", ] [[package]] @@ -1874,7 +2492,7 @@ dependencies = [ "bytes", "crypto-bigint", "ctutils", - "digest", + "digest 0.11.3", "pem-rfc7468", "zeroize", ] @@ -1890,15 +2508,15 @@ dependencies = [ "ctutils", "ed25519-dalek", "hex", - "hmac", + "hmac 0.13.0", "p256", "p384", "p521", - "rand_core", + "rand_core 0.10.1", "rsa", "sec1", "sha1", - "sha2", + "sha2 0.11.1", "signature", "ssh-cipher", "ssh-encoding", @@ -1968,13 +2586,33 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1988,6 +2626,17 @@ dependencies = [ "syn 3.0.10", ] +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "tokio" version = "1.54.1" @@ -2028,6 +2677,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "1.2.1+spec-1.1.0" @@ -2067,12 +2725,46 @@ version = "1.2.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-ident" version = "1.0.27" @@ -2125,7 +2817,7 @@ dependencies = [ "serde", "tempfile", "textwrap", - "toml", + "toml 1.2.1+spec-1.1.0", "uniffi_internal_macros", "uniffi_meta", "uniffi_pipeline", @@ -2170,7 +2862,7 @@ dependencies = [ "quote", "serde", "syn 2.0.119", - "toml", + "toml 1.2.1+spec-1.1.0", "uniffi_meta", ] @@ -2211,13 +2903,23 @@ dependencies = [ "weedle2", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "universal-hash" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common", + "crypto-common 0.2.2", "ctutils", ] @@ -2227,6 +2929,22 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2297,6 +3015,15 @@ dependencies = [ "nom", ] +[[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]] name = "windows" version = "0.62.2" @@ -2510,11 +3237,73 @@ dependencies = [ "primefield", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" + [[package]] name = "zeroize" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "zerofrom", +] [[package]] name = "zmij" diff --git a/rust/ssh-core/Cargo.toml b/rust/ssh-core/Cargo.toml index 741d851..58fb8ab 100644 --- a/rust/ssh-core/Cargo.toml +++ b/rust/ssh-core/Cargo.toml @@ -11,6 +11,7 @@ name = "ssh_core" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] +age = { version = "0.11.1", default-features = false, features = ["armor"] } async-trait = "0.1" getrandom = { version = "0.4", features = ["sys_rng"] } # ring is the only russh crypto backend that cross-compiles to the Android NDK and @@ -20,6 +21,7 @@ russh = { version = "0.65.1", default-features = false, features = ["ring", "rsa russh-sftp = "3.0.1" tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread", "net", "sync", "time"] } uniffi = { workspace = true } +zeroize = "1.9" [dev-dependencies] sha2 = "0.10" diff --git a/rust/ssh-core/src/error.rs b/rust/ssh-core/src/error.rs index b4807c9..eda32d8 100644 --- a/rust/ssh-core/src/error.rs +++ b/rust/ssh-core/src/error.rs @@ -33,6 +33,8 @@ pub enum SshError { KeyParse { message: String }, /// A passphrase is required, or does not decrypt the key. WrongPassphrase, + /// An export is not a valid age file, or cannot be produced from its inputs. + InvalidExport { message: String }, /// The channel is closed: end of stream, server close, or session loss. ChannelClosed { message: String }, /// The peer broke the protocol or the session ended unexpectedly. @@ -68,6 +70,7 @@ impl SshError { Self::UnsupportedAlgorithm { .. } => "UnsupportedAlgorithm", Self::KeyParse { .. } => "KeyParse", Self::WrongPassphrase => "WrongPassphrase", + Self::InvalidExport { .. } => "InvalidExport", Self::ChannelClosed { .. } => "ChannelClosed", Self::Protocol { .. } => "Protocol", Self::KeyGeneration { .. } => "KeyGeneration", @@ -113,6 +116,7 @@ impl fmt::Display for SshError { } Self::KeyParse { message } => write!(f, "key parse error: {message}"), Self::WrongPassphrase => write!(f, "wrong or missing passphrase"), + Self::InvalidExport { message } => write!(f, "invalid export: {message}"), Self::ChannelClosed { message } => write!(f, "channel closed: {message}"), Self::Protocol { message } => write!(f, "protocol error: {message}"), Self::KeyGeneration { message } => write!(f, "key generation failed: {message}"), @@ -286,6 +290,9 @@ mod tests { message: "garbage".into(), }, SshError::WrongPassphrase, + SshError::InvalidExport { + message: "not an age file".into(), + }, SshError::ChannelClosed { message: "eof".into(), }, @@ -339,6 +346,7 @@ mod tests { "UnsupportedAlgorithm", "KeyParse", "WrongPassphrase", + "InvalidExport", "ChannelClosed", "Protocol", "KeyGeneration", diff --git a/rust/ssh-core/src/export.rs b/rust/ssh-core/src/export.rs new file mode 100644 index 0000000..5494a5b --- /dev/null +++ b/rust/ssh-core/src/export.rs @@ -0,0 +1,126 @@ +//! Passphrase-encrypted export and import of the app's data. +//! +//! The payload is app-defined JSON; the core treats it as opaque bytes. Files are +//! age v1 files encrypted to an scrypt passphrase recipient and ASCII-armored, so +//! this module and `age -p` interoperate in both directions. + +use std::fmt; +use std::io::{BufReader, Read, Write}; +use std::iter; + +use age::armor::{ArmoredReader, ArmoredWriter, Format}; +use age::secrecy::SecretString; +use age::{DecryptError, Encryptor}; +use zeroize::Zeroize; + +use crate::SshError; + +const MAX_EXPORT_BYTES: usize = 16 * 1024 * 1024; +const MIN_PASSPHRASE_CHARS: usize = 8; +// age's scrypt default: 2^18 iterations and a 256 MiB allocation. A fixed factor +// keeps a file created on a fast device openable on a slow one, and bounds the +// memory a supplied file may demand. +const SCRYPT_WORK_FACTOR: u8 = 18; + +/// Encrypts `plaintext_json` into an ASCII-armored age file. +/// +/// Fails with [`SshError::InvalidExport`] when the payload exceeds 16 MiB or the +/// passphrase is shorter than 8 characters. +#[uniffi::export] +pub fn encrypt_export(plaintext_json: String, passphrase: String) -> Result { + if passphrase.chars().count() < MIN_PASSPHRASE_CHARS { + return Err(SshError::InvalidExport { + message: format!("the passphrase must be at least {MIN_PASSPHRASE_CHARS} characters"), + }); + } + + let mut plaintext = plaintext_json.into_bytes(); + let byte_count = plaintext.len(); + + if byte_count > MAX_EXPORT_BYTES { + plaintext.zeroize(); + + return Err(SshError::InvalidExport { + message: format!("the payload is {byte_count} bytes; the limit is {MAX_EXPORT_BYTES}"), + }); + } + + let sealed = seal(&plaintext, passphrase); + plaintext.zeroize(); + + sealed +} + +/// Decrypts an ASCII-armored age file produced by [`encrypt_export`] or by +/// `age -p -a`. +/// +/// Fails with [`SshError::WrongPassphrase`] when the passphrase does not open the +/// file, and with [`SshError::InvalidExport`] when the input is not an age file, +/// is damaged, or decrypts to more than 16 MiB. +#[uniffi::export] +pub fn decrypt_export(armored: String, passphrase: String) -> Result { + let mut identity = age::scrypt::Identity::new(SecretString::from(passphrase)); + identity.set_max_work_factor(SCRYPT_WORK_FACTOR); + + let decryptor = + age::Decryptor::new_buffered(ArmoredReader::new(BufReader::new(armored.as_bytes()))) + .map_err(|error| invalid_export(format!("not an age export: {error}")))?; + + let mut reader = decryptor + .decrypt(iter::once(&identity as &dyn age::Identity)) + .map_err(open_error)?; + + let mut plaintext = Vec::new(); + reader + .by_ref() + .take(MAX_EXPORT_BYTES as u64 + 1) + .read_to_end(&mut plaintext) + .map_err(|error| invalid_export(format!("the export is damaged: {error}")))?; + + if plaintext.len() > MAX_EXPORT_BYTES { + plaintext.zeroize(); + + return Err(SshError::InvalidExport { + message: format!("the payload exceeds the {MAX_EXPORT_BYTES} byte limit"), + }); + } + + String::from_utf8(plaintext).map_err(|_| invalid_export("the payload is not UTF-8".to_string())) +} + +fn seal(plaintext: &[u8], passphrase: String) -> Result { + let mut recipient = age::scrypt::Recipient::new(SecretString::from(passphrase)); + recipient.set_work_factor(SCRYPT_WORK_FACTOR); + + let encryptor = Encryptor::with_recipients(iter::once(&recipient as &dyn age::Recipient)) + .map_err(|error| invalid_export(format!("could not encrypt: {error}")))?; + + let mut armored = Vec::with_capacity(plaintext.len() + plaintext.len() / 3 + 1024); + let mut writer = encryptor + .wrap_output( + ArmoredWriter::wrap_output(&mut armored, Format::AsciiArmor) + .map_err(|error| invalid_export(format!("could not encrypt: {error}")))?, + ) + .map_err(|error| invalid_export(format!("could not encrypt: {error}")))?; + + writer + .write_all(plaintext) + .and_then(|()| writer.finish()?.finish()) + .map_err(|error| invalid_export(format!("could not encrypt: {error}")))?; + + String::from_utf8(armored) + .map_err(|_| invalid_export("the ciphertext is not UTF-8".to_string())) +} + +fn open_error(error: DecryptError) -> SshError { + match error { + DecryptError::DecryptionFailed => SshError::WrongPassphrase, + other => invalid_export(format!("the export cannot be opened: {other}")), + } +} + +fn invalid_export(message: impl fmt::Display) -> SshError { + SshError::InvalidExport { + message: message.to_string(), + } +} diff --git a/rust/ssh-core/src/lib.rs b/rust/ssh-core/src/lib.rs index 5448c57..373cf85 100644 --- a/rust/ssh-core/src/lib.rs +++ b/rust/ssh-core/src/lib.rs @@ -5,6 +5,7 @@ uniffi::setup_scaffolding!(); mod error; +mod export; mod forward; mod key; mod runtime; @@ -14,6 +15,7 @@ mod shell; mod transfer; pub use error::SshError; +pub use export::{decrypt_export, encrypt_export}; pub use forward::{ForwardConfig, ForwardHandle, ForwardSink}; pub use key::{GeneratedKey, ParsedKey, generate_ed25519_key, parse_private_key}; pub use session::{AuthMethod, ConnectConfig, ExecResult, Prompt, SessionHandler, SshSession}; diff --git a/rust/ssh-core/tests/export.rs b/rust/ssh-core/tests/export.rs new file mode 100644 index 0000000..f17ed38 --- /dev/null +++ b/rust/ssh-core/tests/export.rs @@ -0,0 +1,167 @@ +use ssh_core::{SshError, decrypt_export, encrypt_export}; + +const PASSPHRASE: &str = "correct horse battery staple"; +const CLI_FIXTURE: &str = include_str!("fixtures/age-cli-passphrase.age"); +const CLI_FIXTURE_PASSPHRASE: &str = "fixture passphrase 123"; +const CLI_FIXTURE_PLAINTEXT: &str = r#"{"version":1,"hosts":[{"host":"example.com","user":"fixture"}],"note":"made by the age CLI"}"#; + +fn payload() -> String { + r#"{"version":1,"hosts":[{"host":"example.com","port":22,"user":"игорь"}],"note":"round trip"}"# + .to_string() +} + +fn tamper(armored: &str) -> String { + let mut bytes: Vec = armored.as_bytes().to_vec(); + let start = bytes.len() * 3 / 4; + let index = (start..bytes.len()) + .find(|&index| bytes[index].is_ascii_alphanumeric() && bytes[index] != b'-') + .expect("the armor body is not empty"); + + bytes[index] = if bytes[index] == b'A' { b'B' } else { b'A' }; + + String::from_utf8(bytes).expect("the armor is UTF-8") +} + +#[test] +fn round_trip_returns_the_payload() { + let armored = encrypt_export(payload(), PASSPHRASE.to_string()).expect("encrypts"); + + let decrypted = decrypt_export(armored, PASSPHRASE.to_string()).expect("decrypts"); + + assert_eq!(decrypted, payload()); +} + +#[test] +fn output_is_ascii_armor() { + let armored = encrypt_export(payload(), PASSPHRASE.to_string()).expect("encrypts"); + + assert!(armored.starts_with("-----BEGIN AGE ENCRYPTED FILE-----\n")); + assert!(armored.ends_with("-----END AGE ENCRYPTED FILE-----\n")); +} + +#[test] +fn wrong_passphrase_is_reported() { + let armored = encrypt_export(payload(), PASSPHRASE.to_string()).expect("encrypts"); + + let error = + decrypt_export(armored, "wrong horse battery staple".to_string()).expect_err("fails"); + + assert!(matches!(error, SshError::WrongPassphrase), "{error:?}"); + assert_eq!(error.category(), "WrongPassphrase"); +} + +#[test] +fn tampered_file_is_invalid() { + let armored = encrypt_export(payload(), PASSPHRASE.to_string()).expect("encrypts"); + + let error = decrypt_export(tamper(&armored), PASSPHRASE.to_string()).expect_err("fails"); + + assert!(matches!(error, SshError::InvalidExport { .. }), "{error:?}"); + assert_eq!(error.category(), "InvalidExport"); +} + +#[test] +fn truncated_file_is_invalid() { + let armored = encrypt_export(payload(), PASSPHRASE.to_string()).expect("encrypts"); + let truncated = armored[..armored.len() - 40].to_string(); + + let error = decrypt_export(truncated, PASSPHRASE.to_string()).expect_err("fails"); + + assert!(matches!(error, SshError::InvalidExport { .. }), "{error:?}"); +} + +#[test] +fn non_age_input_is_invalid() { + for input in ["", "{\"version\":1}", "not an age file at all"] { + let error = decrypt_export(input.to_string(), PASSPHRASE.to_string()).expect_err("fails"); + + assert!( + matches!(error, SshError::InvalidExport { .. }), + "{input:?}: {error:?}" + ); + } +} + +#[test] +fn short_passphrase_is_rejected() { + let error = encrypt_export(payload(), "1234567".to_string()).expect_err("fails"); + + assert!(matches!(error, SshError::InvalidExport { .. }), "{error:?}"); + assert!(error.to_string().contains('8'), "{error}"); +} + +#[test] +fn oversized_payload_is_rejected() { + let oversized = "a".repeat(16 * 1024 * 1024 + 1); + + let error = encrypt_export(oversized, PASSPHRASE.to_string()).expect_err("fails"); + + assert!(matches!(error, SshError::InvalidExport { .. }), "{error:?}"); + assert!(error.to_string().contains("16777216"), "{error}"); +} + +#[test] +fn cli_made_file_decrypts() { + let decrypted = decrypt_export(CLI_FIXTURE.to_string(), CLI_FIXTURE_PASSPHRASE.to_string()) + .expect("decrypts"); + + assert_eq!(decrypted, CLI_FIXTURE_PLAINTEXT); +} + +#[cfg(target_os = "linux")] +#[test] +fn our_file_decrypts_with_the_age_cli() { + use std::process::Command; + + if Command::new("age").arg("--version").output().is_err() { + eprintln!("skipping: the age CLI is not installed"); + + return; + } + + let armored = encrypt_export(CLI_FIXTURE_PLAINTEXT.to_string(), PASSPHRASE.to_string()) + .expect("encrypts"); + let path = std::env::temp_dir().join(format!("ssh-core-export-{}.age", std::process::id())); + std::fs::write(&path, armored).expect("writes the export"); + + let output = decrypt_with_cli(&path); + + let _ = std::fs::remove_file(&path); + + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!( + output.status.success(), + "age -d failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + stdout.contains(CLI_FIXTURE_PLAINTEXT), + "age -d returned {stdout:?}" + ); +} + +#[cfg(target_os = "linux")] +fn decrypt_with_cli(path: &std::path::Path) -> std::process::Output { + use std::io::Write; + use std::process::{Command, Stdio}; + + let command = format!("age -d {}", path.display()); + + let mut child = Command::new("script") + .args(["-q", "-e", "-c", &command, "/dev/null"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("starts the age CLI on a pty"); + + child + .stdin + .as_mut() + .expect("a piped stdin") + .write_all(format!("{PASSPHRASE}\n").as_bytes()) + .expect("types the passphrase"); + + child.wait_with_output().expect("waits for the age CLI") +} diff --git a/rust/ssh-core/tests/fixtures/age-cli-passphrase.age b/rust/ssh-core/tests/fixtures/age-cli-passphrase.age new file mode 100644 index 0000000..aa4f878 --- /dev/null +++ b/rust/ssh-core/tests/fixtures/age-cli-passphrase.age @@ -0,0 +1,8 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCArKzd4NWoyd3I1Ymw5VXRK +bGxIZHF3IDE4ClZJMTZQdWF6VDAxSHFhSW9tTTdhYUNYM2FTbzIraDNUdVUvbU9H +RTV3MkUKLS0tIDJKRnA5b253LzJNdG5acXlZY2IzSUMzOFc0MkhFQWo3eWg5RURI +R1ZvTXMK+2JrZungT19VpL08jK2MNsN1WJN0aiOePpaVZDtA/J89Q0tSOQx4TCbh +de4uUWY9VCuQjaTVbH8B7BR8DoEAH8TN7K1uDa6hMHGlOp/d300DQRo6WvhHGxzn +74pGa631pRI7KFVclHPAaJaEl4tIYg5bCTK8d4zG1cx1Bg== +-----END AGE ENCRYPTED FILE----- diff --git a/src/ReactNativeSsh.types.ts b/src/ReactNativeSsh.types.ts index 8a753ee..a344ed3 100644 --- a/src/ReactNativeSsh.types.ts +++ b/src/ReactNativeSsh.types.ts @@ -23,6 +23,7 @@ export const SSH_ERROR_CODES = [ 'UnsupportedAlgorithm', 'KeyParse', 'WrongPassphrase', + 'InvalidExport', 'ChannelClosed', 'Protocol', 'KeyGeneration', diff --git a/src/ReactNativeSshModule.ts b/src/ReactNativeSshModule.ts index e04cf4d..8d0aece 100644 --- a/src/ReactNativeSshModule.ts +++ b/src/ReactNativeSshModule.ts @@ -16,6 +16,8 @@ declare class ReactNativeSshModule extends NativeModule { coreVersion(): string; generateEd25519Key(comment: string): Promise; parsePrivateKey(pem: string, passphrase: string | null): ParsedKey; + encryptExport(plaintextJson: string, passphrase: string): Promise; + decryptExport(armored: string, passphrase: string): Promise; connect(config: NativeConnectConfig): Promise; /** Connects through an authenticated session that carries the stream. */ connectVia(jumpSessionId: string, config: NativeConnectConfig): Promise; diff --git a/src/ReactNativeSshModule.web.ts b/src/ReactNativeSshModule.web.ts index e291827..6dba61c 100644 --- a/src/ReactNativeSshModule.web.ts +++ b/src/ReactNativeSshModule.web.ts @@ -31,6 +31,17 @@ class ReactNativeSshModule extends NativeModule { return unsupported(); } + encryptExport( + _plaintextJson: string, + _passphrase: string + ): Promise { + return Promise.reject(new Error(unavailable)); + } + + decryptExport(_armored: string, _passphrase: string): Promise { + return Promise.reject(new Error(unavailable)); + } + connect(_config: NativeConnectConfig): Promise { return Promise.reject(new Error(unavailable)); } diff --git a/src/export.ts b/src/export.ts new file mode 100644 index 0000000..944cc45 --- /dev/null +++ b/src/export.ts @@ -0,0 +1,31 @@ +import ReactNativeSsh from './ReactNativeSshModule'; +import { guard } from './errors'; + +/** + * Encrypts an app-defined JSON payload into an ASCII-armored age file that the + * `age` CLI can decrypt with the same passphrase. + * + * Rejects with `InvalidExport` when the payload exceeds 16 MiB or the passphrase + * is shorter than 8 characters. The key derivation runs natively, off the + * JavaScript thread. + */ +export function encryptExport( + plaintextJson: string, + passphrase: string +): Promise { + return guard(() => ReactNativeSsh.encryptExport(plaintextJson, passphrase)); +} + +/** + * Decrypts a file produced by [`encryptExport`] or by `age -p -a`. + * + * Rejects with `WrongPassphrase` when the passphrase does not open the file, and + * with `InvalidExport` when the input is not an age file, is damaged, or holds + * more than 16 MiB. + */ +export function decryptExport( + armored: string, + passphrase: string +): Promise { + return guard(() => ReactNativeSsh.decryptExport(armored, passphrase)); +} diff --git a/src/index.ts b/src/index.ts index 79aefbb..eb4c17d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export { openShell, parsePrivateKey, } from './ssh'; +export { decryptExport, encryptExport } from './export'; export { startLocalForward, startRemoteForward } from './forward'; export { closeSftp,