diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5ba5885d72..93693f183c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -122,6 +122,9 @@ apps/cli/src/locales/en/messages.json
apps/desktop/src/locales/en/messages.json
apps/web/src/locales/en/messages.json
+## Ssh agent temporary co-codeowner
+apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-platform-dev @bitwarden/wg-ssh-keys
+
## BRE team owns these workflows ##
.github/workflows/brew-bump-desktop.yml @bitwarden/dept-bre
.github/workflows/deploy-web.yml @bitwarden/dept-bre
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 92fabdae3e..a62dac0543 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -152,6 +152,15 @@
"copyLicenseNumber": {
"message": "Copy license number"
},
+ "copyPrivateKey": {
+ "message": "Copy private key"
+ },
+ "copyPublicKey": {
+ "message": "Copy public key"
+ },
+ "copyFingerprint": {
+ "message": "Copy fingerprint"
+ },
"copyCustomField": {
"message": "Copy $FIELD$",
"placeholders": {
@@ -1764,6 +1773,9 @@
"typeIdentity": {
"message": "Identity"
},
+ "typeSshKey": {
+ "message": "SSH key"
+ },
"newItemHeader": {
"message": "New $TYPE$",
"placeholders": {
@@ -4593,6 +4605,30 @@
"enterprisePolicyRequirementsApplied": {
"message": "Enterprise policy requirements have been applied to this setting"
},
+ "sshPrivateKey": {
+ "message": "Private key"
+ },
+ "sshPublicKey": {
+ "message": "Public key"
+ },
+ "sshFingerprint": {
+ "message": "Fingerprint"
+ },
+ "sshKeyAlgorithm": {
+ "message": "Key type"
+ },
+ "sshKeyAlgorithmED25519": {
+ "message": "ED25519"
+ },
+ "sshKeyAlgorithmRSA2048": {
+ "message": "RSA 2048-Bit"
+ },
+ "sshKeyAlgorithmRSA3072": {
+ "message": "RSA 3072-Bit"
+ },
+ "sshKeyAlgorithmRSA4096": {
+ "message": "RSA 4096-Bit"
+ },
"retry": {
"message": "Retry"
},
diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts
index 5febed788e..8d95acbce9 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts
@@ -331,6 +331,8 @@ export class AddEditV2Component implements OnInit {
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase());
case CipherType.SecureNote:
return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase());
+ case CipherType.SshKey:
+ return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLocaleLowerCase());
}
}
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
index f4444a10ae..973b1f9f1a 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
@@ -88,3 +88,27 @@
[cipher]="cipher"
>
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
index a53c4a7c35..00a775024c 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
@@ -48,5 +48,13 @@ export class ItemCopyActionsComponent {
return !!this.cipher.notes;
}
+ get hasSshKeyValues() {
+ return (
+ !!this.cipher.sshKey.privateKey ||
+ !!this.cipher.sshKey.publicKey ||
+ !!this.cipher.sshKey.keyFingerprint
+ );
+ }
+
constructor() {}
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
index cc2c419c04..b1cbe8bc3e 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts
@@ -131,6 +131,8 @@ export class ViewV2Component {
);
case CipherType.SecureNote:
return this.i18nService.t("viewItemHeader", this.i18nService.t("note").toLowerCase());
+ case CipherType.SshKey:
+ return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey").toLowerCase());
}
}
diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html
index 32d7c283b2..fb1efbbbd7 100644
--- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html
+++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html
@@ -529,6 +529,26 @@
/>
+
+
+
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
index 42d76e1dfe..610d48fdc6 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
@@ -202,6 +202,7 @@ describe("VaultPopupItemsService", () => {
[CipherType.Card]: 2,
[CipherType.Identity]: 3,
[CipherType.SecureNote]: 4,
+ [CipherType.SshKey]: 5,
};
// Assume all ciphers are autofill ciphers to test sorting
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
index 09c7d5fb0d..20ac3b3de9 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
@@ -277,6 +277,7 @@ export class VaultPopupItemsService {
[CipherType.Card]: 2,
[CipherType.Identity]: 3,
[CipherType.SecureNote]: 4,
+ [CipherType.SshKey]: 5,
};
// Compare types first
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
index 5d7e690193..02ad7375f6 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
@@ -97,6 +97,7 @@ describe("VaultPopupListFiltersService", () => {
CipherType.Card,
CipherType.Identity,
CipherType.SecureNote,
+ CipherType.SshKey,
]);
});
});
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
index 4059a43b56..590807cff6 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
@@ -163,6 +163,11 @@ export class VaultPopupListFiltersService {
label: this.i18nService.t("note"),
icon: "bwi-sticky-note",
},
+ {
+ value: CipherType.SshKey,
+ label: this.i18nService.t("typeSshKey"),
+ icon: "bwi-key",
+ },
];
/** Resets `filterForm` to the original state */
diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock
index 74c75d38e7..7136384ff2 100644
--- a/apps/desktop/desktop_native/Cargo.lock
+++ b/apps/desktop/desktop_native/Cargo.lock
@@ -17,6 +17,16 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
[[package]]
name = "aes"
version = "0.8.4"
@@ -28,6 +38,20 @@ dependencies = [
"cpufeatures",
]
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -185,6 +209,28 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-task"
version = "4.7.1"
@@ -235,12 +281,51 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
+dependencies = [
+ "blowfish",
+ "pbkdf2",
+ "sha2",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+[[package]]
+name = "bitwarden-russh"
+version = "0.1.0"
+source = "git+https://github.com/bitwarden/bitwarden-russh.git?branch=km/pm-10098/clean-russh-implementation#86ff1bf2f4620a3ae5684adee31abdbee33c6f07"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "futures",
+ "russh-cryptovec",
+ "ssh-encoding",
+ "ssh-key",
+ "thiserror",
+ "tokio",
+ "tokio-util",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -281,6 +366,16 @@ dependencies = [
"piper",
]
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -339,6 +434,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
[[package]]
name = "cipher"
version = "0.4.4"
@@ -377,6 +483,12 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
[[package]]
name = "convert_case"
version = "0.6.0"
@@ -437,6 +549,41 @@ dependencies = [
"syn",
]
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "cxx"
version = "1.0.129"
@@ -481,6 +628,17 @@ dependencies = [
"syn",
]
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
[[package]]
name = "deranged"
version = "0.3.11"
@@ -508,25 +666,38 @@ dependencies = [
"aes",
"anyhow",
"arboard",
+ "async-stream",
"base64",
+ "bitwarden-russh",
+ "byteorder",
"cbc",
"core-foundation",
"dirs",
+ "ed25519",
"futures",
"gio",
+ "homedir",
"interprocess",
"keytar",
"libc",
"libsecret",
"log",
+ "pin-project",
+ "pkcs8",
"rand",
+ "rand_chacha",
"retry",
+ "rsa",
+ "russh-cryptovec",
"scopeguard",
"security-framework",
"security-framework-sys",
"sha2",
+ "ssh-encoding",
+ "ssh-key",
"thiserror",
"tokio",
+ "tokio-stream",
"tokio-util",
"typenum",
"widestring",
@@ -540,11 +711,14 @@ name = "desktop_napi"
version = "0.0.0"
dependencies = [
"anyhow",
+ "base64",
"desktop_core",
+ "hex",
"napi",
"napi-build",
"napi-derive",
"tokio",
+ "tokio-stream",
"tokio-util",
"windows-registry",
]
@@ -570,7 +744,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
+ "subtle",
]
[[package]]
@@ -615,6 +791,28 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "sha2",
+ "subtle",
+]
+
[[package]]
name = "embed_plist"
version = "1.2.2"
@@ -697,6 +895,12 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -842,6 +1046,16 @@ dependencies = [
"wasi",
]
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
[[package]]
name = "gimli"
version = "0.31.1"
@@ -885,7 +1099,7 @@ version = "0.19.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"futures-channel",
"futures-core",
"futures-executor",
@@ -965,6 +1179,27 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "homedir"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bed305c13ce3829a09d627f5d43ff738482a09361ae4eb8039993b55fb10e5e"
+dependencies = [
+ "cfg-if",
+ "nix 0.26.4",
+ "widestring",
+ "windows",
+]
+
[[package]]
name = "indexmap"
version = "2.6.0"
@@ -1027,6 +1262,15 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
[[package]]
name = "libc"
version = "0.2.159"
@@ -1043,13 +1287,19 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"libc",
]
@@ -1116,6 +1366,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+[[package]]
+name = "memoffset"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "memoffset"
version = "0.9.1"
@@ -1158,7 +1417,7 @@ version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"ctor",
"napi-derive",
"napi-sys",
@@ -1210,13 +1469,26 @@ dependencies = [
"libloading",
]
+[[package]]
+name = "nix"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+ "memoffset 0.7.1",
+ "pin-utils",
+]
+
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
@@ -1228,11 +1500,11 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
- "memoffset",
+ "memoffset 0.9.1",
]
[[package]]
@@ -1245,12 +1517,59 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -1282,7 +1601,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"block2",
"libc",
"objc2",
@@ -1298,7 +1617,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"block2",
"objc2",
"objc2-foundation",
@@ -1328,7 +1647,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"block2",
"libc",
"objc2",
@@ -1340,7 +1659,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"block2",
"objc2",
"objc2-foundation",
@@ -1352,7 +1671,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"block2",
"objc2",
"objc2-foundation",
@@ -1374,6 +1693,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -1429,6 +1754,25 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
[[package]]
name = "petgraph"
version = "0.6.5"
@@ -1439,6 +1783,26 @@ dependencies = [
"indexmap",
]
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.15"
@@ -1462,6 +1826,44 @@ dependencies = [
"futures-io",
]
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs5"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
+dependencies = [
+ "aes",
+ "cbc",
+ "der",
+ "pbkdf2",
+ "scrypt",
+ "sha2",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "pkcs5",
+ "rand_core",
+ "spki",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.31"
@@ -1483,6 +1885,29 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -1576,7 +2001,7 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
]
[[package]]
@@ -1628,25 +2053,74 @@ dependencies = [
"rand",
]
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "russh-cryptovec"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fadd2c0ab350e21c66556f94ee06f766d8bdae3213857ba7610bfd8e10e51880"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
[[package]]
name = "rustix"
version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
+[[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher",
+]
+
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -1665,13 +2139,24 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
+[[package]]
+name = "scrypt"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+dependencies = [
+ "pbkdf2",
+ "salsa20",
+ "sha2",
+]
+
[[package]]
name = "security-framework"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -1771,6 +2256,16 @@ dependencies = [
"libc",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
[[package]]
name = "simplelog"
version = "0.12.2"
@@ -1807,12 +2302,81 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "cbc",
+ "chacha20",
+ "cipher",
+ "ctr",
+ "poly1305",
+ "ssh-encoding",
+ "subtle",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+dependencies = [
+ "base64ct",
+ "pem-rfc7468",
+ "sha2",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc"
+dependencies = [
+ "bcrypt-pbkdf",
+ "ed25519-dalek",
+ "num-bigint-dig",
+ "rand_core",
+ "rsa",
+ "sha2",
+ "signature",
+ "ssh-cipher",
+ "ssh-encoding",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "2.0.87"
@@ -1920,9 +2484,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.41.0"
+version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
+checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"bytes",
@@ -1946,10 +2510,21 @@ dependencies = [
]
[[package]]
-name = "tokio-util"
-version = "0.7.12"
+name = "tokio-stream"
+version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
@@ -2048,7 +2623,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
- "memoffset",
+ "memoffset 0.9.1",
"tempfile",
"winapi",
]
@@ -2071,6 +2646,16 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -2109,7 +2694,7 @@ version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"rustix",
"wayland-backend",
"wayland-scanner",
@@ -2121,7 +2706,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -2133,7 +2718,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6"
dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -2581,6 +3166,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
[[package]]
name = "zvariant"
version = "4.2.0"
diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml
index 4196e415e7..cf409ccc29 100644
--- a/apps/desktop/desktop_native/core/Cargo.toml
+++ b/apps/desktop/desktop_native/core/Cargo.toml
@@ -27,21 +27,39 @@ anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control",
] }
+async-stream = "0.3.5"
base64 = "=0.22.1"
+byteorder = "1.5.0"
cbc = { version = "=0.1.2", features = ["alloc"] }
+homedir = "0.3.3"
+libc = "=0.2.159"
+pin-project = "1.1.5"
dirs = "=5.0.1"
futures = "=0.3.31"
interprocess = { version = "=2.2.1", features = ["tokio"] }
-libc = "=0.2.159"
log = "=0.4.22"
rand = "=0.8.5"
retry = "=2.0.0"
+russh-cryptovec = "0.7.3"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
+ssh-encoding = "0.2.0"
+ssh-key = { version = "0.6.6", default-features = false, features = [
+ "encryption",
+ "ed25519",
+ "rsa",
+ "getrandom",
+] }
+bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/pm-10098/clean-russh-implementation" }
+tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] }
+tokio-stream = { version = "=0.1.15", features = ["net"] }
+tokio-util = "=0.7.11"
thiserror = "=1.0.68"
-tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
-tokio-util = "=0.7.12"
typenum = "=1.17.0"
+rand_chacha = "=0.3.1"
+pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
+rsa = "=0.9.6"
+ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
[target.'cfg(windows)'.dependencies]
widestring = { version = "=1.1.0", optional = true }
diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs
index c41ad9dda5..72352cf228 100644
--- a/apps/desktop/desktop_native/core/src/biometric/mod.rs
+++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs
@@ -6,8 +6,8 @@ use anyhow::{anyhow, Result};
#[cfg_attr(target_os = "macos", path = "macos.rs")]
mod biometric;
-pub use biometric::Biometric;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
+pub use biometric::Biometric;
use sha2::{Digest, Sha256};
use crate::crypto::{self, CipherString};
@@ -42,7 +42,6 @@ pub trait BiometricTrait {
) -> Result
;
}
-
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result {
let iv = base64_engine
.decode(iv_b64)?
@@ -77,4 +76,4 @@ impl KeyMaterial {
pub fn derive_key(&self) -> Result> {
Ok(Sha256::digest(self.digest_material()))
}
-}
\ No newline at end of file
+}
diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs
index 742b736e81..563bd1dfe5 100644
--- a/apps/desktop/desktop_native/core/src/biometric/unix.rs
+++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs
@@ -5,13 +5,13 @@ use base64::Engine;
use rand::RngCore;
use sha2::{Digest, Sha256};
-use crate::biometric::{KeyMaterial, OsDerivedKey, base64_engine};
+use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey};
use zbus::Connection;
use zbus_polkit::policykit1::*;
use super::{decrypt, encrypt};
-use anyhow::anyhow;
use crate::crypto::CipherString;
+use anyhow::anyhow;
/// The Unix implementation of the biometric trait.
pub struct Biometric {}
@@ -22,13 +22,15 @@ impl super::BiometricTrait for Biometric {
let proxy = AuthorityProxy::new(&connection).await?;
let subject = Subject::new_for_owner(std::process::id(), None, None)?;
let details = std::collections::HashMap::new();
- let result = proxy.check_authorization(
- &subject,
- "com.bitwarden.Bitwarden.unlock",
- &details,
- CheckAuthorizationFlags::AllowUserInteraction.into(),
- "",
- ).await;
+ let result = proxy
+ .check_authorization(
+ &subject,
+ "com.bitwarden.Bitwarden.unlock",
+ &details,
+ CheckAuthorizationFlags::AllowUserInteraction.into(),
+ "",
+ )
+ .await;
match result {
Ok(result) => {
@@ -106,4 +108,4 @@ fn random_challenge() -> [u8; 16] {
let mut challenge = [0u8; 16];
rand::thread_rng().fill_bytes(&mut challenge);
challenge
-}
\ No newline at end of file
+}
diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs
index c5db9e3277..d5e8b6dc91 100644
--- a/apps/desktop/desktop_native/core/src/biometric/windows.rs
+++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs
@@ -160,7 +160,6 @@ impl super::BiometricTrait for Biometric {
}
}
-
fn random_challenge() -> [u8; 16] {
let mut challenge = [0u8; 16];
rand::thread_rng().fill_bytes(&mut challenge);
diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs
index 3132c56f7f..f38e6ef97b 100644
--- a/apps/desktop/desktop_native/core/src/lib.rs
+++ b/apps/desktop/desktop_native/core/src/lib.rs
@@ -11,3 +11,6 @@ pub mod password;
pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod powermonitor;
+#[cfg(feature = "sys")]
+
+pub mod ssh_agent;
diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs
index 53053ee467..1817a4d62e 100644
--- a/apps/desktop/desktop_native/core/src/password/unix.rs
+++ b/apps/desktop/desktop_native/core/src/password/unix.rs
@@ -41,7 +41,11 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
}
pub fn is_available() -> Result {
- let result = password_clear_sync(Some(&get_schema()), build_attributes("bitwardenSecretsAvailabilityTest", "test"), gio::Cancellable::NONE);
+ let result = password_clear_sync(
+ Some(&get_schema()),
+ build_attributes("bitwardenSecretsAvailabilityTest", "test"),
+ gio::Cancellable::NONE,
+ );
match result {
Ok(_) => Ok(true),
Err(_) => {
diff --git a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs
index fe07ad11ff..7d0fde15ed 100644
--- a/apps/desktop/desktop_native/core/src/powermonitor/linux.rs
+++ b/apps/desktop/desktop_native/core/src/powermonitor/linux.rs
@@ -1,6 +1,6 @@
use std::borrow::Cow;
-use zbus::{Connection, MatchRule, export::futures_util::TryStreamExt};
+use zbus::{export::futures_util::TryStreamExt, Connection, MatchRule};
struct ScreenLock {
interface: Cow<'static, str>,
path: Cow<'static, str>,
@@ -42,7 +42,15 @@ pub async fn on_lock(tx: tokio::sync::mpsc::Sender<()>) -> Result<(), Box bool {
let connection = Connection::session().await.unwrap();
for monitor in SCREEN_LOCK_MONITORS {
- let res = connection.call_method(Some(monitor.interface.clone()), monitor.path.clone(), Some(monitor.interface.clone()), "GetActive", &()).await;
+ let res = connection
+ .call_method(
+ Some(monitor.interface.clone()),
+ monitor.path.clone(),
+ Some(monitor.interface.clone()),
+ "GetActive",
+ &(),
+ )
+ .await;
if res.is_ok() {
return true;
}
diff --git a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs
index ba8734cff7..dc027e0b54 100644
--- a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs
+++ b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs
@@ -1,7 +1,7 @@
use anyhow::Result;
-use libc::{c_int, self};
#[cfg(target_env = "gnu")]
use libc::c_uint;
+use libc::{self, c_int};
// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes
// https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20
@@ -22,7 +22,10 @@ pub fn disable_coredumps() -> Result<()> {
};
if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 {
let e = std::io::Error::last_os_error();
- return Err(anyhow::anyhow!("failed to disable core dumping, memory might be persisted to disk on crashes {}", e))
+ return Err(anyhow::anyhow!(
+ "failed to disable core dumping, memory might be persisted to disk on crashes {}",
+ e
+ ));
}
Ok(())
@@ -35,7 +38,7 @@ pub fn is_core_dumping_disabled() -> Result {
};
if unsafe { libc::getrlimit(RLIMIT_CORE, &mut rlimit) } != 0 {
let e = std::io::Error::last_os_error();
- return Err(anyhow::anyhow!("failed to get core dump limit {}", e))
+ return Err(anyhow::anyhow!("failed to get core dump limit {}", e));
}
Ok(rlimit.rlim_cur == 0 && rlimit.rlim_max == 0)
@@ -44,7 +47,10 @@ pub fn is_core_dumping_disabled() -> Result {
pub fn disable_memory_access() -> Result<()> {
if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 {
let e = std::io::Error::last_os_error();
- return Err(anyhow::anyhow!("failed to disable memory dumping, memory is dumpable by other processes {}", e))
+ return Err(anyhow::anyhow!(
+ "failed to disable memory dumping, memory is dumpable by other processes {}",
+ e
+ ));
}
Ok(())
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs
new file mode 100644
index 0000000000..fe639f20e7
--- /dev/null
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs
@@ -0,0 +1,45 @@
+use rand::SeedableRng;
+use rand_chacha::ChaCha8Rng;
+use ssh_key::{Algorithm, HashAlg, LineEnding};
+
+use super::importer::SshKey;
+
+pub async fn generate_keypair(key_algorithm: String) -> Result {
+ // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom
+ // if it cannot be securely sourced, this will panic instead of leading to a weak key
+ let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy();
+
+ let key = match key_algorithm.as_str() {
+ "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519),
+ "rsa2048" | "rsa3072" | "rsa4096" => {
+ let bits = match key_algorithm.as_str() {
+ "rsa2048" => 2048,
+ "rsa3072" => 3072,
+ "rsa4096" => 4096,
+ _ => return Err(anyhow::anyhow!("Unsupported RSA key size")),
+ };
+ let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits)
+ .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
+
+ let private_key = ssh_key::PrivateKey::new(
+ ssh_key::private::KeypairData::from(rsa_keypair),
+ "".to_string(),
+ )
+ .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
+ Ok(private_key)
+ }
+ _ => {
+ return Err(anyhow::anyhow!("Unsupported key algorithm"));
+ }
+ }
+ .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
+
+ let private_key_openssh = key
+ .to_openssh(LineEnding::LF)
+ .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
+ Ok(SshKey {
+ private_key: private_key_openssh.to_string(),
+ public_key: key.public_key().to_string(),
+ key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(),
+ })
+}
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs
new file mode 100644
index 0000000000..3d643e764c
--- /dev/null
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs
@@ -0,0 +1,395 @@
+use ed25519;
+use pkcs8::{
+ der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument,
+};
+use ssh_key::{
+ private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair},
+ HashAlg, LineEnding,
+};
+
+const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----";
+const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----";
+const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
+const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
+
+pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier =
+ ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
+
+#[derive(Debug)]
+enum KeyType {
+ Ed25519,
+ Rsa,
+ Unknown,
+}
+
+pub fn import_key(
+ encoded_key: String,
+ password: String,
+) -> Result {
+ match encoded_key.lines().next() {
+ Some(PKCS1_HEADER) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::UnsupportedKeyType,
+ ssh_key: None,
+ });
+ }
+ Some(PKCS8_UNENCRYPTED_HEADER) => {
+ return match import_pkcs8_key(encoded_key, None) {
+ Ok(result) => Ok(result),
+ Err(_) => Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ }),
+ };
+ }
+ Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) {
+ Ok(result) => {
+ return Ok(result);
+ }
+ Err(err) => match err {
+ SshKeyImportError::PasswordRequired => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::PasswordRequired,
+ ssh_key: None,
+ });
+ }
+ SshKeyImportError::WrongPassword => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::WrongPassword,
+ ssh_key: None,
+ });
+ }
+ SshKeyImportError::ParsingError => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ },
+ },
+ Some(OPENSSH_HEADER) => {
+ return import_openssh_key(encoded_key, password);
+ }
+ Some(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ None => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ }
+}
+
+fn import_pkcs8_key(
+ encoded_key: String,
+ password: Option,
+) -> Result {
+ let der = match SecretDocument::from_pem(&encoded_key) {
+ Ok((_, doc)) => doc,
+ Err(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ };
+
+ let decrypted_der = match password.clone() {
+ Some(password) => {
+ let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes())
+ {
+ Ok(info) => info,
+ Err(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ };
+ match encrypted_private_key_info.decrypt(password.as_bytes()) {
+ Ok(der) => der,
+ Err(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::WrongPassword,
+ ssh_key: None,
+ });
+ }
+ }
+ }
+ None => der,
+ };
+
+ let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes())
+ .map_err(|_| SshKeyImportError::ParsingError)?
+ .algorithm
+ .oid
+ {
+ ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519,
+ RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa,
+ _ => KeyType::Unknown,
+ };
+
+ match key_type {
+ KeyType::Ed25519 => {
+ let pk: ed25519::KeypairBytes = match password {
+ Some(password) => {
+ pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
+ .map_err(|err| match err {
+ ed25519::pkcs8::Error::EncryptedPrivateKey(_) => {
+ SshKeyImportError::WrongPassword
+ }
+ _ => SshKeyImportError::ParsingError,
+ })?
+ }
+ None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
+ .map_err(|_| SshKeyImportError::ParsingError)?,
+ };
+ let pk: Ed25519Keypair =
+ Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key));
+ let private_key = ssh_key::private::PrivateKey::from(pk);
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::Success,
+ ssh_key: Some(SshKey {
+ private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(),
+ public_key: private_key.public_key().to_string(),
+ key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
+ }),
+ });
+ }
+ KeyType::Rsa => {
+ let pk: rsa::RsaPrivateKey = match password {
+ Some(password) => {
+ pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
+ .map_err(|err| match err {
+ pkcs8::Error::EncryptedPrivateKey(_) => {
+ SshKeyImportError::WrongPassword
+ }
+ _ => SshKeyImportError::ParsingError,
+ })?
+ }
+ None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
+ .map_err(|_| SshKeyImportError::ParsingError)?,
+ };
+ let rsa_keypair: Result = RsaKeypair::try_from(pk);
+ match rsa_keypair {
+ Ok(rsa_keypair) => {
+ let private_key = ssh_key::private::PrivateKey::from(rsa_keypair);
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::Success,
+ ssh_key: Some(SshKey {
+ private_key: private_key
+ .to_openssh(LineEnding::LF)
+ .unwrap()
+ .to_string(),
+ public_key: private_key.public_key().to_string(),
+ key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
+ }),
+ });
+ }
+ Err(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ }
+ }
+ _ => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::UnsupportedKeyType,
+ ssh_key: None,
+ });
+ }
+ }
+}
+
+fn import_openssh_key(
+ encoded_key: String,
+ password: String,
+) -> Result {
+ let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key);
+ let private_key = match private_key {
+ Ok(k) => k,
+ Err(err) => {
+ match err {
+ ssh_key::Error::AlgorithmUnknown
+ | ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::UnsupportedKeyType,
+ ssh_key: None,
+ });
+ }
+ _ => {}
+ }
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ });
+ }
+ };
+
+ if private_key.is_encrypted() && password.is_empty() {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::PasswordRequired,
+ ssh_key: None,
+ });
+ }
+ let private_key = if private_key.is_encrypted() {
+ match private_key.decrypt(password.as_bytes()) {
+ Ok(k) => k,
+ Err(_) => {
+ return Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::WrongPassword,
+ ssh_key: None,
+ });
+ }
+ }
+ } else {
+ private_key
+ };
+
+ match private_key.to_openssh(LineEnding::LF) {
+ Ok(private_key_openssh) => Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::Success,
+ ssh_key: Some(SshKey {
+ private_key: private_key_openssh.to_string(),
+ public_key: private_key.public_key().to_string(),
+ key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
+ }),
+ }),
+ Err(_) => Ok(SshKeyImportResult {
+ status: SshKeyImportStatus::ParsingError,
+ ssh_key: None,
+ }),
+ }
+}
+
+#[derive(PartialEq, Debug)]
+pub enum SshKeyImportStatus {
+ /// ssh key was parsed correctly and will be returned in the result
+ Success,
+ /// ssh key was parsed correctly but is encrypted and requires a password
+ PasswordRequired,
+ /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
+ WrongPassword,
+ /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
+ ParsingError,
+ /// ssh key type is not supported
+ UnsupportedKeyType,
+}
+
+pub enum SshKeyImportError {
+ ParsingError,
+ PasswordRequired,
+ WrongPassword,
+}
+
+pub struct SshKeyImportResult {
+ pub status: SshKeyImportStatus,
+ pub ssh_key: Option,
+}
+
+pub struct SshKey {
+ pub private_key: String,
+ pub public_key: String,
+ pub key_fingerprint: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn import_key_ed25519_openssh_unencrypted() {
+ let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted");
+ let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim();
+ let result = import_key(private_key.to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_ed25519_openssh_encrypted() {
+ let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
+ let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim();
+ let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_rsa_openssh_unencrypted() {
+ let private_key = include_str!("./test_keys/rsa_openssh_unencrypted");
+ let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim();
+ let result = import_key(private_key.to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_rsa_openssh_encrypted() {
+ let private_key = include_str!("./test_keys/rsa_openssh_encrypted");
+ let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim();
+ let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_ed25519_pkcs8_unencrypted() {
+ let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted");
+ let public_key =
+ include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", "");
+ let public_key = public_key.trim();
+ let result = import_key(private_key.to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_rsa_pkcs8_unencrypted() {
+ let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted");
+ // for whatever reason pkcs8 + rsa does not include the comment in the public key
+ let public_key =
+ include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
+ let public_key = public_key.trim();
+ let result = import_key(private_key.to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_rsa_pkcs8_encrypted() {
+ let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted");
+ let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", "");
+ let public_key = public_key.trim();
+ let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::Success);
+ assert_eq!(result.ssh_key.unwrap().public_key, public_key);
+ }
+
+ #[test]
+ fn import_key_ed25519_openssh_encrypted_wrong_password() {
+ let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
+ let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::WrongPassword);
+ }
+
+ #[test]
+ fn import_non_key_error() {
+ let result = import_key("not a key".to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::ParsingError);
+ }
+
+ #[test]
+ fn import_ecdsa_error() {
+ let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted");
+ let result = import_key(private_key.to_string(), "".to_string()).unwrap();
+ assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
+ }
+}
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
new file mode 100644
index 0000000000..ad0ac837af
--- /dev/null
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
@@ -0,0 +1,118 @@
+use std::sync::Arc;
+
+use tokio::sync::Mutex;
+use tokio_util::sync::CancellationToken;
+
+use bitwarden_russh::ssh_agent::{self, Key};
+
+#[cfg_attr(target_os = "windows", path = "windows.rs")]
+#[cfg_attr(target_os = "macos", path = "unix.rs")]
+#[cfg_attr(target_os = "linux", path = "unix.rs")]
+mod platform_ssh_agent;
+
+pub mod generator;
+pub mod importer;
+
+#[derive(Clone)]
+pub struct BitwardenDesktopAgent {
+ keystore: ssh_agent::KeyStore,
+ cancellation_token: CancellationToken,
+ show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>,
+ get_ui_response_rx: Arc>>,
+ request_id: Arc>,
+}
+
+impl BitwardenDesktopAgent {
+ async fn get_request_id(&self) -> u32 {
+ let mut request_id = self.request_id.lock().await;
+ *request_id += 1;
+ *request_id
+ }
+}
+
+impl ssh_agent::Agent for BitwardenDesktopAgent {
+ async fn confirm(&self, ssh_key: Key) -> bool {
+ let request_id = self.get_request_id().await;
+
+ let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
+ self.show_ui_request_tx
+ .send((request_id, ssh_key.cipher_uuid.clone()))
+ .await
+ .expect("Should send request to ui");
+ while let Ok((id, response)) = rx_channel.recv().await {
+ if id == request_id {
+ return response;
+ }
+ }
+ false
+ }
+}
+
+impl BitwardenDesktopAgent {
+ pub fn stop(&self) {
+ self.cancellation_token.cancel();
+ self.keystore
+ .0
+ .write()
+ .expect("RwLock is not poisoned")
+ .clear();
+ }
+
+ pub fn set_keys(
+ &mut self,
+ new_keys: Vec<(String, String, String)>,
+ ) -> Result<(), anyhow::Error> {
+ let keystore = &mut self.keystore;
+ keystore.0.write().expect("RwLock is not poisoned").clear();
+
+ for (key, name, cipher_id) in new_keys.iter() {
+ match parse_key_safe(&key) {
+ Ok(private_key) => {
+ let public_key_bytes = private_key
+ .public_key()
+ .to_bytes()
+ .expect("Cipher private key is always correctly parsed");
+ keystore.0.write().expect("RwLock is not poisoned").insert(
+ public_key_bytes,
+ Key {
+ private_key: Some(private_key),
+ name: name.clone(),
+ cipher_uuid: cipher_id.clone(),
+ },
+ );
+ }
+ Err(e) => {
+ eprintln!("[SSH Agent Native Module] Error while parsing key: {}", e);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn lock(&mut self) -> Result<(), anyhow::Error> {
+ let keystore = &mut self.keystore;
+ keystore
+ .0
+ .write()
+ .expect("RwLock is not poisoned")
+ .iter_mut()
+ .for_each(|(_public_key, key)| {
+ key.private_key = None;
+ });
+ Ok(())
+ }
+}
+
+fn parse_key_safe(pem: &str) -> Result {
+ match ssh_key::private::PrivateKey::from_openssh(pem) {
+ Ok(key) => match key.public_key().to_bytes() {
+ Ok(_) => Ok(key),
+ Err(e) => Err(anyhow::Error::msg(format!(
+ "Failed to parse public key: {}",
+ e
+ ))),
+ },
+ Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {}", e))),
+ }
+}
diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs
new file mode 100644
index 0000000000..69399ae753
--- /dev/null
+++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs
@@ -0,0 +1,60 @@
+use std::{
+ io,
+ pin::Pin,
+ task::{Context, Poll},
+};
+
+use futures::Stream;
+use tokio::{
+ net::windows::named_pipe::{NamedPipeServer, ServerOptions},
+ select,
+};
+use tokio_util::sync::CancellationToken;
+
+const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent";
+
+#[pin_project::pin_project]
+pub struct NamedPipeServerStream {
+ rx: tokio::sync::mpsc::Receiver,
+}
+
+impl NamedPipeServerStream {
+ pub fn new(cancellation_token: CancellationToken) -> Self {
+ let (tx, rx) = tokio::sync::mpsc::channel(16);
+ tokio::spawn(async move {
+ println!(
+ "[SSH Agent Native Module] Creating named pipe server on {}",
+ PIPE_NAME
+ );
+ let mut listener = ServerOptions::new().create(PIPE_NAME).unwrap();
+ loop {
+ println!("[SSH Agent Native Module] Waiting for connection");
+ select! {
+ _ = cancellation_token.cancelled() => {
+ println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server");
+ break;
+ }
+ _ = listener.connect() => {
+ println!("[SSH Agent Native Module] Incoming connection");
+ tx.send(listener).await.unwrap();
+ listener = ServerOptions::new().create(PIPE_NAME).unwrap();
+ }
+ }
+ }
+ });
+ Self { rx }
+ }
+}
+
+impl Stream for NamedPipeServerStream {
+ type Item = io::Result;
+
+ fn poll_next(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ ) -> Poll
+
diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html
index 855b5dac48..01ac60fc7e 100644
--- a/apps/web/src/app/vault/individual-vault/add-edit.component.html
+++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html
@@ -851,6 +851,99 @@
+
+