mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-16227] Move import to sdk and enable it in browser/web (#12479)
* Move import to sdk and enable it in browser/web * Add uncomitted files * Update package lock * Fix prettier formatting * Fix build * Rewrite import logic * Update ssh import logic for cipher form component * Fix build on browser * Break early in retry logic * Fix build * Fix build * Fix build errors * Update paste icons and throw error on wrong import * Fix tests * Fix build for cli * Undo change to jest config * Undo change to feature flag enum * Remove unneeded lifetime * Fix browser build * Refactor control flow * Fix i18n key and improve import behavior * Remove for loop limit * Clean up tests * Remove unused code * Update libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts Co-authored-by: SmithThe4th <gsmith@bitwarden.com> * Move import logic to service and add tests * Fix linting * Remove erroneous includes * Attempt to fix storybook * Fix storybook, explicitly implement ssh-import-prompt service abstraction * Fix eslint * Update libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> * Fix services module * Remove ssh import sdk init code * Add tests for errors * Fix import * Fix import * Fix pkcs8 encrypted key not parsing * Fix import button showing on web --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com>
This commit is contained in:
@@ -5128,6 +5128,33 @@
|
|||||||
"extraWide": {
|
"extraWide": {
|
||||||
"message": "Extra wide"
|
"message": "Extra wide"
|
||||||
},
|
},
|
||||||
|
"sshKeyWrongPassword": {
|
||||||
|
"message": "The password you entered is incorrect."
|
||||||
|
},
|
||||||
|
"importSshKey": {
|
||||||
|
"message": "Import"
|
||||||
|
},
|
||||||
|
"confirmSshKeyPassword": {
|
||||||
|
"message": "Confirm password"
|
||||||
|
},
|
||||||
|
"enterSshKeyPasswordDesc": {
|
||||||
|
"message": "Enter the password for the SSH key."
|
||||||
|
},
|
||||||
|
"enterSshKeyPassword": {
|
||||||
|
"message": "Enter password"
|
||||||
|
},
|
||||||
|
"invalidSshKey": {
|
||||||
|
"message": "The SSH key is invalid"
|
||||||
|
},
|
||||||
|
"sshKeyTypeUnsupported": {
|
||||||
|
"message": "The SSH key type is not supported"
|
||||||
|
},
|
||||||
|
"importSshKeyFromClipboard": {
|
||||||
|
"message": "Import key from clipboard"
|
||||||
|
},
|
||||||
|
"sshKeyImported": {
|
||||||
|
"message": "SSH key imported successfully"
|
||||||
|
},
|
||||||
"cannotRemoveViewOnlyCollections": {
|
"cannotRemoveViewOnlyCollections": {
|
||||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -1012,6 +1012,7 @@ export default class MainBackground {
|
|||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.sdkService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualVaultExportService = new IndividualVaultExportService(
|
this.individualVaultExportService = new IndividualVaultExportService(
|
||||||
|
|||||||
@@ -130,7 +130,11 @@ import {
|
|||||||
KeyService,
|
KeyService,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import {
|
||||||
|
DefaultSshImportPromptService,
|
||||||
|
PasswordRepromptService,
|
||||||
|
SshImportPromptService,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||||
@@ -653,6 +657,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ExtensionLoginDecryptionOptionsService,
|
useClass: ExtensionLoginDecryptionOptionsService,
|
||||||
deps: [MessagingServiceAbstraction, Router],
|
deps: [MessagingServiceAbstraction, Router],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SshImportPromptService,
|
||||||
|
useClass: DefaultSshImportPromptService,
|
||||||
|
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -780,6 +780,7 @@ export class ServiceContainer {
|
|||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.sdkService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualExportService = new IndividualVaultExportService(
|
this.individualExportService = new IndividualVaultExportService(
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
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<SshKeyImportResult, anyhow::Error> {
|
|
||||||
match encoded_key.lines().next() {
|
|
||||||
Some(PKCS1_HEADER) => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
Some(PKCS8_UNENCRYPTED_HEADER) => 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) => Ok(result),
|
|
||||||
Err(err) => match err {
|
|
||||||
SshKeyImportError::PasswordRequired => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::PasswordRequired,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
SshKeyImportError::WrongPassword => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::WrongPassword,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
SshKeyImportError::ParsingError => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::ParsingError,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Some(OPENSSH_HEADER) => import_openssh_key(encoded_key, password),
|
|
||||||
Some(_) => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::ParsingError,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
None => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::ParsingError,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_pkcs8_key(
|
|
||||||
encoded_key: String,
|
|
||||||
password: Option<String>,
|
|
||||||
) -> Result<SshKeyImportResult, SshKeyImportError> {
|
|
||||||
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);
|
|
||||||
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, ssh_key::Error> = RsaKeypair::try_from(pk);
|
|
||||||
match rsa_keypair {
|
|
||||||
Ok(rsa_keypair) => {
|
|
||||||
let private_key = ssh_key::private::PrivateKey::from(rsa_keypair);
|
|
||||||
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(_) => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::ParsingError,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Ok(SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
|
||||||
ssh_key: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_openssh_key(
|
|
||||||
encoded_key: String,
|
|
||||||
password: String,
|
|
||||||
) -> Result<SshKeyImportResult, anyhow::Error> {
|
|
||||||
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<SshKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
|
||||||
// Should this test start failing, please change it to expect a correct key, and
|
|
||||||
// make sure the documentation support for putty-exported keys this is updated.
|
|
||||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
|
||||||
#[test]
|
|
||||||
fn import_key_ed25519_putty() {
|
|
||||||
let private_key = include_str!("./test_keys/ed25519_putty_openssh_unencrypted");
|
|
||||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
|
||||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
|
||||||
// Should this test start failing, please change it to expect a correct key, and
|
|
||||||
// make sure the documentation support for putty-exported keys this is updated.
|
|
||||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
|
||||||
#[test]
|
|
||||||
fn import_key_rsa_openssh_putty() {
|
|
||||||
let private_key = include_str!("./test_keys/rsa_putty_openssh_unencrypted");
|
|
||||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
|
||||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn import_key_rsa_pkcs8_putty() {
|
|
||||||
let private_key = include_str!("./test_keys/rsa_putty_pkcs1_unencrypted");
|
|
||||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
|
||||||
assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ mod platform_ssh_agent;
|
|||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
mod peercred_unix_listener_stream;
|
mod peercred_unix_listener_stream;
|
||||||
|
|
||||||
pub mod importer;
|
|
||||||
pub mod peerinfo;
|
pub mod peerinfo;
|
||||||
mod request_parser;
|
mod request_parser;
|
||||||
|
|
||||||
|
|||||||
17
apps/desktop/desktop_native/napi/index.d.ts
vendored
17
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -51,22 +51,6 @@ export declare namespace sshagent {
|
|||||||
publicKey: string
|
publicKey: string
|
||||||
keyFingerprint: string
|
keyFingerprint: string
|
||||||
}
|
}
|
||||||
export const enum SshKeyImportStatus {
|
|
||||||
/** ssh key was parsed correctly and will be returned in the result */
|
|
||||||
Success = 0,
|
|
||||||
/** ssh key was parsed correctly but is encrypted and requires a password */
|
|
||||||
PasswordRequired = 1,
|
|
||||||
/** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */
|
|
||||||
WrongPassword = 2,
|
|
||||||
/** 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 = 3,
|
|
||||||
/** ssh key type is not supported (e.g. ecdsa) */
|
|
||||||
UnsupportedKeyType = 4
|
|
||||||
}
|
|
||||||
export interface SshKeyImportResult {
|
|
||||||
status: SshKeyImportStatus
|
|
||||||
sshKey?: SshKey
|
|
||||||
}
|
|
||||||
export interface SshUiRequest {
|
export interface SshUiRequest {
|
||||||
cipherId?: string
|
cipherId?: string
|
||||||
isList: boolean
|
isList: boolean
|
||||||
@@ -79,7 +63,6 @@ export declare namespace sshagent {
|
|||||||
export function isRunning(agentState: SshAgentState): boolean
|
export function isRunning(agentState: SshAgentState): boolean
|
||||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||||
export function lock(agentState: SshAgentState): void
|
export function lock(agentState: SshAgentState): void
|
||||||
export function importKey(encodedKey: string, password: string): SshKeyImportResult
|
|
||||||
export function clearKeys(agentState: SshAgentState): void
|
export function clearKeys(agentState: SshAgentState): void
|
||||||
export class SshAgentState { }
|
export class SshAgentState { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,67 +182,6 @@ pub mod sshagent {
|
|||||||
pub key_fingerprint: String,
|
pub key_fingerprint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<desktop_core::ssh_agent::importer::SshKey> for SshKey {
|
|
||||||
fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self {
|
|
||||||
SshKey {
|
|
||||||
private_key: key.private_key,
|
|
||||||
public_key: key.public_key,
|
|
||||||
key_fingerprint: key.key_fingerprint,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
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 (e.g. ecdsa)
|
|
||||||
UnsupportedKeyType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportStatus> for SshKeyImportStatus {
|
|
||||||
fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self {
|
|
||||||
match status {
|
|
||||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => {
|
|
||||||
SshKeyImportStatus::Success
|
|
||||||
}
|
|
||||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => {
|
|
||||||
SshKeyImportStatus::PasswordRequired
|
|
||||||
}
|
|
||||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => {
|
|
||||||
SshKeyImportStatus::WrongPassword
|
|
||||||
}
|
|
||||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => {
|
|
||||||
SshKeyImportStatus::ParsingError
|
|
||||||
}
|
|
||||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => {
|
|
||||||
SshKeyImportStatus::UnsupportedKeyType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(object)]
|
|
||||||
pub struct SshKeyImportResult {
|
|
||||||
pub status: SshKeyImportStatus,
|
|
||||||
pub ssh_key: Option<SshKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportResult> for SshKeyImportResult {
|
|
||||||
fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self {
|
|
||||||
SshKeyImportResult {
|
|
||||||
status: result.status.into(),
|
|
||||||
ssh_key: result.ssh_key.map(|k| k.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(object)]
|
#[napi(object)]
|
||||||
pub struct SshUIRequest {
|
pub struct SshUIRequest {
|
||||||
pub cipher_id: Option<String>,
|
pub cipher_id: Option<String>,
|
||||||
@@ -359,13 +298,6 @@ pub mod sshagent {
|
|||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub fn import_key(encoded_key: String, password: String) -> napi::Result<SshKeyImportResult> {
|
|
||||||
let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password)
|
|
||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
||||||
Ok(result.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||||
let bitwarden_agent_state = &mut agent_state.state;
|
let bitwarden_agent_state = &mut agent_state.state;
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ import {
|
|||||||
BiometricsService,
|
BiometricsService,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||||
|
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||||
@@ -430,6 +431,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DesktopLoginApprovalComponentService,
|
useClass: DesktopLoginApprovalComponentService,
|
||||||
deps: [I18nServiceAbstraction],
|
deps: [I18nServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SshImportPromptService,
|
||||||
|
useClass: DefaultSshImportPromptService,
|
||||||
|
deps: [DialogService, ToastService, PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -25,16 +25,6 @@ export class MainSshAgentService {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
) {
|
) {
|
||||||
ipcMain.handle(
|
|
||||||
"sshagent.importkey",
|
|
||||||
async (
|
|
||||||
event: any,
|
|
||||||
{ privateKey, password }: { privateKey: string; password?: string },
|
|
||||||
): Promise<sshagent.SshKeyImportResult> => {
|
|
||||||
return sshagent.importKey(privateKey, password);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle("sshagent.init", async (event: any, message: any) => {
|
ipcMain.handle("sshagent.init", async (event: any, message: any) => {
|
||||||
this.init();
|
this.init();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3532,9 +3532,6 @@
|
|||||||
"unknownApplication": {
|
"unknownApplication": {
|
||||||
"message": "An application"
|
"message": "An application"
|
||||||
},
|
},
|
||||||
"sshKeyPasswordUnsupported": {
|
|
||||||
"message": "Importing password protected SSH keys is not yet supported"
|
|
||||||
},
|
|
||||||
"invalidSshKey": {
|
"invalidSshKey": {
|
||||||
"message": "The SSH key is invalid"
|
"message": "The SSH key is invalid"
|
||||||
},
|
},
|
||||||
@@ -3544,7 +3541,7 @@
|
|||||||
"importSshKeyFromClipboard": {
|
"importSshKeyFromClipboard": {
|
||||||
"message": "Import key from clipboard"
|
"message": "Import key from clipboard"
|
||||||
},
|
},
|
||||||
"sshKeyPasted": {
|
"sshKeyImported": {
|
||||||
"message": "SSH key imported successfully"
|
"message": "SSH key imported successfully"
|
||||||
},
|
},
|
||||||
"fileSavedToDevice": {
|
"fileSavedToDevice": {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { sshagent as ssh } from "desktop_native/napi";
|
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
@@ -64,13 +63,6 @@ const sshAgent = {
|
|||||||
clearKeys: async () => {
|
clearKeys: async () => {
|
||||||
return await ipcRenderer.invoke("sshagent.clearkeys");
|
return await ipcRenderer.invoke("sshagent.clearkeys");
|
||||||
},
|
},
|
||||||
importKey: async (key: string, password: string): Promise<ssh.SshKeyImportResult> => {
|
|
||||||
const res = await ipcRenderer.invoke("sshagent.importkey", {
|
|
||||||
privateKey: key,
|
|
||||||
password: password,
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
isLoaded(): Promise<boolean> {
|
isLoaded(): Promise<boolean> {
|
||||||
return ipcRenderer.invoke("sshagent.isloaded");
|
return ipcRenderer.invoke("sshagent.isloaded");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -512,6 +512,15 @@
|
|||||||
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
|
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="row-btn"
|
||||||
|
appStopClick
|
||||||
|
appA11yTitle="{{ 'importSshKeyFromClipboard' | i18n }}"
|
||||||
|
(click)="importSshKeyFromClipboard()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-paste" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||||
@@ -559,16 +568,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="row-btn"
|
|
||||||
appStopClick
|
|
||||||
(click)="importSshKeyFromClipboard()"
|
|
||||||
>
|
|
||||||
{{ "importSshKeyFromClipboard" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
import { DatePipe } from "@angular/common";
|
import { DatePipe } from "@angular/common";
|
||||||
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
import { NgForm } from "@angular/forms";
|
import { NgForm } from "@angular/forms";
|
||||||
import { sshagent as sshAgent } from "desktop_native/napi";
|
|
||||||
import { lastValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||||
@@ -25,8 +23,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui";
|
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "AddEditComponent";
|
const BroadcasterSubscriptionId = "AddEditComponent";
|
||||||
|
|
||||||
@@ -60,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
cipherAuthorizationService: CipherAuthorizationService,
|
cipherAuthorizationService: CipherAuthorizationService,
|
||||||
sdkService: SdkService,
|
sdkService: SdkService,
|
||||||
|
sshImportPromptService: SshImportPromptService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -82,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
cipherAuthorizationService,
|
cipherAuthorizationService,
|
||||||
toastService,
|
toastService,
|
||||||
sdkService,
|
sdkService,
|
||||||
|
sshImportPromptService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,69 +158,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
this.cipher.revisionDate = cipher.revisionDate;
|
this.cipher.revisionDate = cipher.revisionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async importSshKeyFromClipboard(password: string = "") {
|
|
||||||
const key = await this.platformUtilsService.readFromClipboard();
|
|
||||||
const parsedKey = await ipc.platform.sshAgent.importKey(key, password);
|
|
||||||
if (parsedKey == null) {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("invalidSshKey"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (parsedKey.status) {
|
|
||||||
case sshAgent.SshKeyImportStatus.ParsingError:
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("invalidSshKey"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
case sshAgent.SshKeyImportStatus.UnsupportedKeyType:
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("sshKeyTypeUnsupported"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
case sshAgent.SshKeyImportStatus.PasswordRequired:
|
|
||||||
case sshAgent.SshKeyImportStatus.WrongPassword:
|
|
||||||
if (password !== "") {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("sshKeyWrongPassword"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
password = await this.getSshKeyPassword();
|
|
||||||
if (password === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.importSshKeyFromClipboard(password);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey;
|
|
||||||
this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey;
|
|
||||||
this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint;
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "success",
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("sshKeyPasted"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSshKeyPassword(): Promise<string> {
|
|
||||||
const dialog = this.dialogService.open<string>(SshKeyPasswordPromptComponent, {
|
|
||||||
ariaModal: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await lastValueFrom(dialog.closed);
|
|
||||||
}
|
|
||||||
|
|
||||||
truncateString(value: string, length: number) {
|
truncateString(value: string, length: number) {
|
||||||
return value.length > length ? value.substring(0, length) + "..." : value;
|
return value.length > length ? value.substring(0, length) + "..." : value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ import {
|
|||||||
DefaultThemeStateService,
|
DefaultThemeStateService,
|
||||||
ThemeStateService,
|
ThemeStateService,
|
||||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import {
|
import {
|
||||||
KdfConfigService,
|
KdfConfigService,
|
||||||
@@ -103,6 +104,7 @@ import {
|
|||||||
BiometricsService,
|
BiometricsService,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||||
|
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { flagEnabled } from "../../utils/flags";
|
import { flagEnabled } from "../../utils/flags";
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
@@ -349,6 +351,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: WebLoginDecryptionOptionsService,
|
useClass: WebLoginDecryptionOptionsService,
|
||||||
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SshImportPromptService,
|
||||||
|
useClass: DefaultSshImportPromptService,
|
||||||
|
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
|||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-add-edit",
|
selector: "app-vault-add-edit",
|
||||||
@@ -76,6 +76,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
cipherAuthorizationService: CipherAuthorizationService,
|
cipherAuthorizationService: CipherAuthorizationService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
sdkService: SdkService,
|
sdkService: SdkService,
|
||||||
|
sshImportPromptService: SshImportPromptService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -98,6 +99,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
cipherAuthorizationService,
|
cipherAuthorizationService,
|
||||||
toastService,
|
toastService,
|
||||||
sdkService,
|
sdkService,
|
||||||
|
sshImportPromptService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
|||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { AddEditComponent as BaseAddEditComponent } from "../individual-vault/add-edit.component";
|
import { AddEditComponent as BaseAddEditComponent } from "../individual-vault/add-edit.component";
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
cipherAuthorizationService: CipherAuthorizationService,
|
cipherAuthorizationService: CipherAuthorizationService,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
sdkService: SdkService,
|
sdkService: SdkService,
|
||||||
|
sshImportPromptService: SshImportPromptService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -88,6 +89,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
cipherAuthorizationService,
|
cipherAuthorizationService,
|
||||||
toastService,
|
toastService,
|
||||||
sdkService,
|
sdkService,
|
||||||
|
sshImportPromptService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"restoreMembers": {
|
"restoreMembers": {
|
||||||
"message": "Restore members"
|
"message": "Restore members"
|
||||||
},
|
},
|
||||||
"cannotRestoreAccessError":{
|
"cannotRestoreAccessError": {
|
||||||
"message": "Cannot restore organization access"
|
"message": "Cannot restore organization access"
|
||||||
},
|
},
|
||||||
"allApplicationsWithCount": {
|
"allApplicationsWithCount": {
|
||||||
@@ -1355,8 +1355,8 @@
|
|||||||
"yourAccountIsLocked": {
|
"yourAccountIsLocked": {
|
||||||
"message": "Your account is locked"
|
"message": "Your account is locked"
|
||||||
},
|
},
|
||||||
"uuid":{
|
"uuid": {
|
||||||
"message" : "UUID"
|
"message": "UUID"
|
||||||
},
|
},
|
||||||
"unlock": {
|
"unlock": {
|
||||||
"message": "Unlock"
|
"message": "Unlock"
|
||||||
@@ -5904,10 +5904,10 @@
|
|||||||
"bulkFilteredMessage": {
|
"bulkFilteredMessage": {
|
||||||
"message": "Excluded, not applicable for this action"
|
"message": "Excluded, not applicable for this action"
|
||||||
},
|
},
|
||||||
"nonCompliantMembersTitle":{
|
"nonCompliantMembersTitle": {
|
||||||
"message": "Non-compliant members"
|
"message": "Non-compliant members"
|
||||||
},
|
},
|
||||||
"nonCompliantMembersError":{
|
"nonCompliantMembersError": {
|
||||||
"message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements"
|
"message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements"
|
||||||
},
|
},
|
||||||
"fingerprint": {
|
"fingerprint": {
|
||||||
@@ -9330,7 +9330,7 @@
|
|||||||
"message": "for Bitwarden using the implementation guide for your Identity Provider.",
|
"message": "for Bitwarden using the implementation guide for your Identity Provider.",
|
||||||
"description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider."
|
"description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider."
|
||||||
},
|
},
|
||||||
"userProvisioning":{
|
"userProvisioning": {
|
||||||
"message": "User provisioning"
|
"message": "User provisioning"
|
||||||
},
|
},
|
||||||
"scimIntegration": {
|
"scimIntegration": {
|
||||||
@@ -9344,22 +9344,22 @@
|
|||||||
"message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.",
|
"message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.",
|
||||||
"description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider"
|
"description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider"
|
||||||
},
|
},
|
||||||
"bwdc":{
|
"bwdc": {
|
||||||
"message": "Bitwarden Directory Connector"
|
"message": "Bitwarden Directory Connector"
|
||||||
},
|
},
|
||||||
"bwdcDesc": {
|
"bwdcDesc": {
|
||||||
"message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider."
|
"message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider."
|
||||||
},
|
},
|
||||||
"eventManagement":{
|
"eventManagement": {
|
||||||
"message": "Event management"
|
"message": "Event management"
|
||||||
},
|
},
|
||||||
"eventManagementDesc":{
|
"eventManagementDesc": {
|
||||||
"message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform."
|
"message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform."
|
||||||
},
|
},
|
||||||
"deviceManagement":{
|
"deviceManagement": {
|
||||||
"message": "Device management"
|
"message": "Device management"
|
||||||
},
|
},
|
||||||
"deviceManagementDesc":{
|
"deviceManagementDesc": {
|
||||||
"message": "Configure device management for Bitwarden using the implementation guide for your platform."
|
"message": "Configure device management for Bitwarden using the implementation guide for your platform."
|
||||||
},
|
},
|
||||||
"desktopRequired": {
|
"desktopRequired": {
|
||||||
@@ -9368,7 +9368,7 @@
|
|||||||
"reopenLinkOnDesktop": {
|
"reopenLinkOnDesktop": {
|
||||||
"message": "Reopen this link from your email on a desktop."
|
"message": "Reopen this link from your email on a desktop."
|
||||||
},
|
},
|
||||||
"integrationCardTooltip":{
|
"integrationCardTooltip": {
|
||||||
"message": "Launch $INTEGRATION$ implementation guide.",
|
"message": "Launch $INTEGRATION$ implementation guide.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"integration": {
|
"integration": {
|
||||||
@@ -9377,7 +9377,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smIntegrationTooltip":{
|
"smIntegrationTooltip": {
|
||||||
"message": "Set up $INTEGRATION$.",
|
"message": "Set up $INTEGRATION$.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"integration": {
|
"integration": {
|
||||||
@@ -9386,7 +9386,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smSdkTooltip":{
|
"smSdkTooltip": {
|
||||||
"message": "View $SDK$ repository",
|
"message": "View $SDK$ repository",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"sdk": {
|
"sdk": {
|
||||||
@@ -9395,7 +9395,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integrationCardAriaLabel":{
|
"integrationCardAriaLabel": {
|
||||||
"message": "open $INTEGRATION$ implementation guide in a new tab.",
|
"message": "open $INTEGRATION$ implementation guide in a new tab.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"integration": {
|
"integration": {
|
||||||
@@ -9404,7 +9404,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smSdkAriaLabel":{
|
"smSdkAriaLabel": {
|
||||||
"message": "view $SDK$ repository in a new tab.",
|
"message": "view $SDK$ repository in a new tab.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"sdk": {
|
"sdk": {
|
||||||
@@ -9413,7 +9413,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smIntegrationCardAriaLabel":{
|
"smIntegrationCardAriaLabel": {
|
||||||
"message": "set up $INTEGRATION$ implementation guide in a new tab.",
|
"message": "set up $INTEGRATION$ implementation guide in a new tab.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"integration": {
|
"integration": {
|
||||||
@@ -9820,7 +9820,7 @@
|
|||||||
"message": "Config"
|
"message": "Config"
|
||||||
},
|
},
|
||||||
"learnMoreAboutEmergencyAccess": {
|
"learnMoreAboutEmergencyAccess": {
|
||||||
"message":"Learn more about emergency access"
|
"message": "Learn more about emergency access"
|
||||||
},
|
},
|
||||||
"learnMoreAboutMatchDetection": {
|
"learnMoreAboutMatchDetection": {
|
||||||
"message": "Learn more about match detection"
|
"message": "Learn more about match detection"
|
||||||
@@ -10122,7 +10122,7 @@
|
|||||||
"selfHostingTitleProper": {
|
"selfHostingTitleProper": {
|
||||||
"message": "Self-Hosting"
|
"message": "Self-Hosting"
|
||||||
},
|
},
|
||||||
"claim-domain-single-org-warning" : {
|
"claim-domain-single-org-warning": {
|
||||||
"message": "Claiming a domain will turn on the single organization policy."
|
"message": "Claiming a domain will turn on the single organization policy."
|
||||||
},
|
},
|
||||||
"single-org-revoked-user-warning": {
|
"single-org-revoked-user-warning": {
|
||||||
@@ -10363,6 +10363,36 @@
|
|||||||
"organizationNameMaxLength": {
|
"organizationNameMaxLength": {
|
||||||
"message": "Organization name cannot exceed 50 characters."
|
"message": "Organization name cannot exceed 50 characters."
|
||||||
},
|
},
|
||||||
|
"sshKeyWrongPassword": {
|
||||||
|
"message": "The password you entered is incorrect."
|
||||||
|
},
|
||||||
|
"importSshKey": {
|
||||||
|
"message": "Import"
|
||||||
|
},
|
||||||
|
"confirmSshKeyPassword": {
|
||||||
|
"message": "Confirm password"
|
||||||
|
},
|
||||||
|
"enterSshKeyPasswordDesc": {
|
||||||
|
"message": "Enter the password for the SSH key."
|
||||||
|
},
|
||||||
|
"enterSshKeyPassword": {
|
||||||
|
"message": "Enter password"
|
||||||
|
},
|
||||||
|
"invalidSshKey": {
|
||||||
|
"message": "The SSH key is invalid"
|
||||||
|
},
|
||||||
|
"sshKeyTypeUnsupported": {
|
||||||
|
"message": "The SSH key type is not supported"
|
||||||
|
},
|
||||||
|
"importSshKeyFromClipboard": {
|
||||||
|
"message": "Import key from clipboard"
|
||||||
|
},
|
||||||
|
"sshKeyImported": {
|
||||||
|
"message": "SSH key imported successfully"
|
||||||
|
},
|
||||||
|
"copySSHPrivateKey": {
|
||||||
|
"message": "Copy private key"
|
||||||
|
},
|
||||||
"openingExtension": {
|
"openingExtension": {
|
||||||
"message": "Opening the Bitwarden browser extension"
|
"message": "Opening the Bitwarden browser extension"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
|||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class AddEditComponent implements OnInit, OnDestroy {
|
export class AddEditComponent implements OnInit, OnDestroy {
|
||||||
@@ -131,7 +131,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
private sdkService: SdkService,
|
protected sdkService: SdkService,
|
||||||
|
private sshImportPromptService: SshImportPromptService,
|
||||||
) {
|
) {
|
||||||
this.typeOptions = [
|
this.typeOptions = [
|
||||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||||
@@ -824,6 +825,15 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importSshKeyFromClipboard() {
|
||||||
|
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
|
||||||
|
if (key != null) {
|
||||||
|
this.cipher.sshKey.privateKey = key.privateKey;
|
||||||
|
this.cipher.sshKey.publicKey = key.publicKey;
|
||||||
|
this.cipher.sshKey.keyFingerprint = key.keyFingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async generateSshKey(showNotification: boolean = true) {
|
private async generateSshKey(showNotification: boolean = true) {
|
||||||
await firstValueFrom(this.sdkService.client$);
|
await firstValueFrom(this.sdkService.client$);
|
||||||
const sshKey = generate_ssh_key("Ed25519");
|
const sshKey = generate_ssh_key("Ed25519");
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
||||||
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
||||||
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
||||||
|
"@bitwarden/importer/core": ["../importer/src"],
|
||||||
|
"@bitwarden/importer-ui": ["../importer/src/components"],
|
||||||
"@bitwarden/key-management": ["../key-management/src"],
|
"@bitwarden/key-management": ["../key-management/src"],
|
||||||
"@bitwarden/platform": ["../platform/src"],
|
"@bitwarden/platform": ["../platform/src"],
|
||||||
"@bitwarden/ui-common": ["../ui/common/src"],
|
"@bitwarden/ui-common": ["../ui/common/src"],
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export class CipherExport {
|
|||||||
break;
|
break;
|
||||||
case CipherType.SshKey:
|
case CipherType.SshKey:
|
||||||
view.sshKey = SshKeyExport.toView(req.sshKey);
|
view.sshKey = SshKeyExport.toView(req.sshKey);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.passwordHistory != null) {
|
if (req.passwordHistory != null) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { import_ssh_key } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key";
|
import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key";
|
||||||
@@ -17,16 +18,18 @@ export class SshKeyExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static toView(req: SshKeyExport, view = new SshKeyView()) {
|
static toView(req: SshKeyExport, view = new SshKeyView()) {
|
||||||
view.privateKey = req.privateKey;
|
const parsedKey = import_ssh_key(req.privateKey);
|
||||||
view.publicKey = req.publicKey;
|
view.privateKey = parsedKey.privateKey;
|
||||||
view.keyFingerprint = req.keyFingerprint;
|
view.publicKey = parsedKey.publicKey;
|
||||||
|
view.keyFingerprint = parsedKey.fingerprint;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
static toDomain(req: SshKeyExport, domain = new SshKeyDomain()) {
|
static toDomain(req: SshKeyExport, domain = new SshKeyDomain()) {
|
||||||
domain.privateKey = req.privateKey != null ? new EncString(req.privateKey) : null;
|
const parsedKey = import_ssh_key(req.privateKey);
|
||||||
domain.publicKey = req.publicKey != null ? new EncString(req.publicKey) : null;
|
domain.privateKey = new EncString(parsedKey.privateKey);
|
||||||
domain.keyFingerprint = req.keyFingerprint != null ? new EncString(req.keyFingerprint) : null;
|
domain.publicKey = new EncString(parsedKey.publicKey);
|
||||||
|
domain.keyFingerprint = new EncString(parsedKey.fingerprint);
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
// TODO: Remove once billing stops depending on components
|
// TODO: Remove once billing stops depending on components
|
||||||
"@bitwarden/components": ["../components/src"],
|
"@bitwarden/components": ["../components/src"],
|
||||||
"@bitwarden/key-management": ["../key-management/src"],
|
"@bitwarden/key-management": ["../key-management/src"],
|
||||||
|
"@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"],
|
||||||
"@bitwarden/platform": ["../platform/src"],
|
"@bitwarden/platform": ["../platform/src"],
|
||||||
// TODO: Remove once billing stops depending on components
|
// TODO: Remove once billing stops depending on components
|
||||||
"@bitwarden/ui-common": ["../ui/common/src"]
|
"@bitwarden/ui-common": ["../ui/common/src"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const sharedConfig = require("../shared/jest.config.ts");
|
|||||||
/** @type {import('jest').Config} */
|
/** @type {import('jest').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
preset: "ts-jest",
|
preset: "jest-preset-angular",
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jsdom",
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
prefix: "<rootDir>/",
|
prefix: "<rootDir>/",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -96,6 +97,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
EncryptService,
|
EncryptService,
|
||||||
PinServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
AccountService,
|
AccountService,
|
||||||
|
SdkService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
@@ -30,6 +33,7 @@ describe("ImportService", () => {
|
|||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
let pinService: MockProxy<PinServiceAbstraction>;
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let sdkService: MockProxy<SdkService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
@@ -40,6 +44,9 @@ describe("ImportService", () => {
|
|||||||
keyService = mock<KeyService>();
|
keyService = mock<KeyService>();
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
pinService = mock<PinServiceAbstraction>();
|
pinService = mock<PinServiceAbstraction>();
|
||||||
|
const mockClient = mock<BitwardenClient>();
|
||||||
|
sdkService = mock<SdkService>();
|
||||||
|
sdkService.client$ = of(mockClient, mockClient, mockClient);
|
||||||
|
|
||||||
importService = new ImportService(
|
importService = new ImportService(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -51,6 +58,7 @@ describe("ImportService", () => {
|
|||||||
encryptService,
|
encryptService,
|
||||||
pinService,
|
pinService,
|
||||||
accountService,
|
accountService,
|
||||||
|
sdkService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/reque
|
|||||||
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -114,6 +115,7 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private pinService: PinServiceAbstraction,
|
private pinService: PinServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private sdkService: SdkService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getImportOptions(): ImportOption[] {
|
getImportOptions(): ImportOption[] {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service
|
|||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests";
|
import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests";
|
||||||
|
|
||||||
|
import { SshImportPromptService } from "../services/ssh-import-prompt.service";
|
||||||
|
|
||||||
import { CipherFormService } from "./abstractions/cipher-form.service";
|
import { CipherFormService } from "./abstractions/cipher-form.service";
|
||||||
import { TotpCaptureService } from "./abstractions/totp-capture.service";
|
import { TotpCaptureService } from "./abstractions/totp-capture.service";
|
||||||
import { CipherFormModule } from "./cipher-form.module";
|
import { CipherFormModule } from "./cipher-form.module";
|
||||||
@@ -146,6 +149,12 @@ export default {
|
|||||||
enabled$: new BehaviorSubject(true),
|
enabled$: new BehaviorSubject(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SshImportPromptService,
|
||||||
|
useValue: {
|
||||||
|
importSshKeyFromClipboard: () => Promise.resolve(new SshKeyData()),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: CipherFormGenerationService,
|
provide: CipherFormGenerationService,
|
||||||
useValue: {
|
useValue: {
|
||||||
|
|||||||
@@ -15,6 +15,14 @@
|
|||||||
data-testid="toggle-privateKey-visibility"
|
data-testid="toggle-privateKey-visibility"
|
||||||
bitPasswordInputToggle
|
bitPasswordInputToggle
|
||||||
></button>
|
></button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-paste"
|
||||||
|
bitSuffix
|
||||||
|
data-testid="import-privateKey"
|
||||||
|
*ngIf="showImport"
|
||||||
|
(click)="importSshKeyFromClipboard()"
|
||||||
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { SshImportPromptService } from "../../../services/ssh-import-prompt.service";
|
||||||
import { CipherFormContainer } from "../../cipher-form-container";
|
import { CipherFormContainer } from "../../cipher-form-container";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -60,11 +62,14 @@ export class SshKeySectionComponent implements OnInit {
|
|||||||
keyFingerprint: [""],
|
keyFingerprint: [""],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
showImport = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherFormContainer: CipherFormContainer,
|
private cipherFormContainer: CipherFormContainer,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private i18nService: I18nService,
|
|
||||||
private sdkService: SdkService,
|
private sdkService: SdkService,
|
||||||
|
private sshImportPromptService: SshImportPromptService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
) {
|
) {
|
||||||
this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm);
|
this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm);
|
||||||
this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||||
@@ -87,6 +92,11 @@ export class SshKeySectionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sshKeyForm.disable();
|
this.sshKeyForm.disable();
|
||||||
|
|
||||||
|
// Web does not support clipboard access
|
||||||
|
if (this.platformUtilsService.getClientType() !== ClientType.Web) {
|
||||||
|
this.showImport = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set form initial form values from the current cipher */
|
/** Set form initial form values from the current cipher */
|
||||||
@@ -100,6 +110,17 @@ export class SshKeySectionComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importSshKeyFromClipboard() {
|
||||||
|
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
|
||||||
|
if (key != null) {
|
||||||
|
this.sshKeyForm.setValue({
|
||||||
|
privateKey: key.privateKey,
|
||||||
|
publicKey: key.publicKey,
|
||||||
|
keyFingerprint: key.keyFingerprint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async generateSshKey() {
|
private async generateSshKey() {
|
||||||
await firstValueFrom(this.sdkService.client$);
|
await firstValueFrom(this.sdkService.client$);
|
||||||
const sshKey = generate_ssh_key("Ed25519");
|
const sshKey = generate_ssh_key("Ed25519");
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon
|
|||||||
export * from "./components/carousel";
|
export * from "./components/carousel";
|
||||||
|
|
||||||
export * as VaultIcons from "./icons";
|
export * as VaultIcons from "./icons";
|
||||||
|
|
||||||
export * from "./tasks";
|
export * from "./tasks";
|
||||||
|
|
||||||
|
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||||
|
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||||
|
|
||||||
export * from "./abstractions/change-login-password.service";
|
export * from "./abstractions/change-login-password.service";
|
||||||
export * from "./services/default-change-login-password.service";
|
export * from "./services/default-change-login-password.service";
|
||||||
|
|||||||
109
libs/vault/src/services/default-ssh-import-prompt.service.ts
Normal file
109
libs/vault/src/services/default-ssh-import-prompt.service.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { SshKeyApi } from "@bitwarden/common/vault/models/api/ssh-key.api";
|
||||||
|
import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data";
|
||||||
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui";
|
||||||
|
import { import_ssh_key, SshKeyImportError, SshKeyView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { SshImportPromptService } from "./ssh-import-prompt.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to import ssh keys and prompt for their password.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DefaultSshImportPromptService implements SshImportPromptService {
|
||||||
|
constructor(
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async importSshKeyFromClipboard(): Promise<SshKeyData | null> {
|
||||||
|
const key = await this.platformUtilsService.readFromClipboard();
|
||||||
|
|
||||||
|
let isPasswordProtectedSshKey = false;
|
||||||
|
|
||||||
|
let parsedKey: SshKeyView | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedKey = import_ssh_key(key);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as SshKeyImportError;
|
||||||
|
if (error.variant === "PasswordRequired" || error.variant === "WrongPassword") {
|
||||||
|
isPasswordProtectedSshKey = true;
|
||||||
|
} else {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordProtectedSshKey) {
|
||||||
|
for (;;) {
|
||||||
|
const password = await this.getSshKeyPassword();
|
||||||
|
if (password === "" || password == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedKey = import_ssh_key(key, password);
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as SshKeyImportError;
|
||||||
|
if (error.variant !== "WrongPassword") {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message: this.i18nService.t("sshKeyImported"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new SshKeyData(
|
||||||
|
new SshKeyApi({
|
||||||
|
privateKey: parsedKey!.privateKey,
|
||||||
|
publicKey: parsedKey!.publicKey,
|
||||||
|
keyFingerprint: parsedKey!.fingerprint,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sshImportErrorVariantToI18nKey(variant: string): string {
|
||||||
|
switch (variant) {
|
||||||
|
case "ParsingError":
|
||||||
|
return "invalidSshKey";
|
||||||
|
case "UnsupportedKeyType":
|
||||||
|
return "sshKeyTypeUnsupported";
|
||||||
|
case "PasswordRequired":
|
||||||
|
case "WrongPassword":
|
||||||
|
return "sshKeyWrongPassword";
|
||||||
|
default:
|
||||||
|
return "errorOccurred";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSshKeyPassword(): Promise<string | undefined> {
|
||||||
|
const dialog = this.dialogService.open<string>(SshKeyPasswordPromptComponent, {
|
||||||
|
ariaModal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await firstValueFrom(dialog.closed);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
libs/vault/src/services/ssh-import-prompt.service.spec.ts
Normal file
111
libs/vault/src/services/ssh-import-prompt.service.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { SshKeyApi } from "@bitwarden/common/vault/models/api/ssh-key.api";
|
||||||
|
import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data";
|
||||||
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import * as sdkInternal from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { DefaultSshImportPromptService } from "./default-ssh-import-prompt.service";
|
||||||
|
|
||||||
|
jest.mock("@bitwarden/sdk-internal");
|
||||||
|
|
||||||
|
const exampleSshKey = {
|
||||||
|
privateKey: "private_key",
|
||||||
|
publicKey: "public_key",
|
||||||
|
fingerprint: "key_fingerprint",
|
||||||
|
} as sdkInternal.SshKeyView;
|
||||||
|
|
||||||
|
const exampleSshKeyData = new SshKeyData(
|
||||||
|
new SshKeyApi({
|
||||||
|
publicKey: exampleSshKey.publicKey,
|
||||||
|
privateKey: exampleSshKey.privateKey,
|
||||||
|
keyFingerprint: exampleSshKey.fingerprint,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("SshImportPromptService", () => {
|
||||||
|
let sshImportPromptService: DefaultSshImportPromptService;
|
||||||
|
|
||||||
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let toastService: MockProxy<ToastService>;
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dialogService = mock<DialogService>();
|
||||||
|
toastService = mock<ToastService>();
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
|
||||||
|
sshImportPromptService = new DefaultSshImportPromptService(
|
||||||
|
dialogService,
|
||||||
|
toastService,
|
||||||
|
platformUtilsService,
|
||||||
|
i18nService,
|
||||||
|
);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("importSshKeyFromClipboard()", () => {
|
||||||
|
it("imports unencrypted ssh key", async () => {
|
||||||
|
jest.spyOn(sdkInternal, "import_ssh_key").mockReturnValue(exampleSshKey);
|
||||||
|
platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key");
|
||||||
|
expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(exampleSshKeyData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests password for encrypted ssh key", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(sdkInternal, "import_ssh_key")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw { variant: "PasswordRequired" };
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => exampleSshKey);
|
||||||
|
dialogService.open.mockReturnValue({ closed: new BehaviorSubject("password") } as any);
|
||||||
|
platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key");
|
||||||
|
|
||||||
|
expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(exampleSshKeyData);
|
||||||
|
expect(dialogService.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels when no password was provided", async () => {
|
||||||
|
jest.spyOn(sdkInternal, "import_ssh_key").mockImplementationOnce(() => {
|
||||||
|
throw { variant: "PasswordRequired" };
|
||||||
|
});
|
||||||
|
dialogService.open.mockReturnValue({ closed: new BehaviorSubject("") } as any);
|
||||||
|
platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key");
|
||||||
|
|
||||||
|
expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null);
|
||||||
|
expect(dialogService.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through error on no password", async () => {
|
||||||
|
jest.spyOn(sdkInternal, "import_ssh_key").mockImplementationOnce(() => {
|
||||||
|
throw { variant: "UnsupportedKeyType" };
|
||||||
|
});
|
||||||
|
platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key");
|
||||||
|
|
||||||
|
expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null);
|
||||||
|
expect(i18nService.t).toHaveBeenCalledWith("sshKeyTypeUnsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through error with password", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(sdkInternal, "import_ssh_key")
|
||||||
|
.mockClear()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw { variant: "PasswordRequired" };
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw { variant: "UnsupportedKeyType" };
|
||||||
|
});
|
||||||
|
platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key");
|
||||||
|
dialogService.open.mockReturnValue({ closed: new BehaviorSubject("password") } as any);
|
||||||
|
|
||||||
|
expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null);
|
||||||
|
expect(i18nService.t).toHaveBeenCalledWith("sshKeyTypeUnsupported");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
5
libs/vault/src/services/ssh-import-prompt.service.ts
Normal file
5
libs/vault/src/services/ssh-import-prompt.service.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data";
|
||||||
|
|
||||||
|
export abstract class SshImportPromptService {
|
||||||
|
abstract importSshKeyFromClipboard: () => Promise<SshKeyData | null>;
|
||||||
|
}
|
||||||
@@ -8,11 +8,13 @@
|
|||||||
"@bitwarden/auth/common": ["../auth/src/common"],
|
"@bitwarden/auth/common": ["../auth/src/common"],
|
||||||
"@bitwarden/common/*": ["../common/src/*"],
|
"@bitwarden/common/*": ["../common/src/*"],
|
||||||
"@bitwarden/components": ["../components/src"],
|
"@bitwarden/components": ["../components/src"],
|
||||||
|
"@bitwarden/importer-ui": ["../importer/src/components"],
|
||||||
"@bitwarden/generator-components": ["../tools/generator/components/src"],
|
"@bitwarden/generator-components": ["../tools/generator/components/src"],
|
||||||
"@bitwarden/generator-core": ["../tools/generator/core/src"],
|
"@bitwarden/generator-core": ["../tools/generator/core/src"],
|
||||||
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
||||||
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
||||||
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
||||||
|
"@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"],
|
||||||
"@bitwarden/key-management": ["../key-management/src"],
|
"@bitwarden/key-management": ["../key-management/src"],
|
||||||
"@bitwarden/platform": ["../platform/src"],
|
"@bitwarden/platform": ["../platform/src"],
|
||||||
"@bitwarden/ui-common": ["../ui/common/src"],
|
"@bitwarden/ui-common": ["../ui/common/src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user