From 01f6fd7ee39911d5e9aca1696f74fdd3b3e46f1c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 10 Mar 2025 18:41:47 +0100 Subject: [PATCH 01/43] [PM-16227] Move import to sdk and enable it in browser/web (#12479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 ✨ * 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 Co-authored-by: ✨ Audrey ✨ --- apps/browser/src/_locales/en/messages.json | 27 ++ .../browser/src/background/main.background.ts | 1 + .../src/popup/services/services.module.ts | 11 +- .../service-container/service-container.ts | 1 + .../core/src/ssh_agent/importer.rs | 402 ------------------ .../desktop_native/core/src/ssh_agent/mod.rs | 1 - apps/desktop/desktop_native/napi/index.d.ts | 17 - apps/desktop/desktop_native/napi/src/lib.rs | 68 --- .../src/app/services/services.module.ts | 6 + .../autofill/main/main-ssh-agent.service.ts | 10 - apps/desktop/src/locales/en/messages.json | 5 +- apps/desktop/src/platform/preload.ts | 8 - .../vault/app/vault/add-edit.component.html | 19 +- .../src/vault/app/vault/add-edit.component.ts | 70 +-- apps/web/src/app/core/core.module.ts | 7 + .../individual-vault/add-edit.component.ts | 4 +- .../app/vault/org-vault/add-edit.component.ts | 4 +- apps/web/src/locales/en/messages.json | 68 ++- .../vault/components/add-edit.component.ts | 14 +- libs/angular/tsconfig.json | 2 + .../common/src/models/export/cipher.export.ts | 1 + .../src/models/export/ssh-key.export.ts | 15 +- libs/common/tsconfig.json | 1 + libs/importer/jest.config.js | 2 +- .../src/components/import.component.ts | 2 + .../src/services/import.service.spec.ts | 8 + libs/importer/src/services/import.service.ts | 2 + .../src/cipher-form/cipher-form.stories.ts | 9 + .../sshkey-section.component.html | 8 + .../sshkey-section.component.ts | 25 +- libs/vault/src/index.ts | 4 +- .../default-ssh-import-prompt.service.ts | 109 +++++ .../ssh-import-prompt.service.spec.ts | 111 +++++ .../src/services/ssh-import-prompt.service.ts | 5 + libs/vault/tsconfig.json | 2 + 35 files changed, 428 insertions(+), 621 deletions(-) delete mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/importer.rs create mode 100644 libs/vault/src/services/default-ssh-import-prompt.service.ts create mode 100644 libs/vault/src/services/ssh-import-prompt.service.spec.ts create mode 100644 libs/vault/src/services/ssh-import-prompt.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 67b770230cd..ae82892f1ed 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5128,6 +5128,33 @@ "extraWide": { "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": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", "placeholders": { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index cd65220936e..a5967b5fe76 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1012,6 +1012,7 @@ export default class MainBackground { this.encryptService, this.pinService, this.accountService, + this.sdkService, ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index d49f48c0c64..5a2adfcf62b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -130,7 +130,11 @@ import { KeyService, } from "@bitwarden/key-management"; 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 { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; @@ -653,6 +657,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction, Router], }), + safeProvider({ + provide: SshImportPromptService, + useClass: DefaultSshImportPromptService, + deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], + }), ]; @NgModule({ diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 0e776375e6a..555337736c7 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -780,6 +780,7 @@ export class ServiceContainer { this.encryptService, this.pinService, this.accountService, + this.sdkService, ); this.individualExportService = new IndividualVaultExportService( diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs deleted file mode 100644 index 52464487ec5..00000000000 --- a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs +++ /dev/null @@ -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 { - 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, -) -> 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); - 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); - 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 { - 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); - } - - // 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); - } -} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 3fe327948f8..5f794b49c73 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -16,7 +16,6 @@ mod platform_ssh_agent; #[cfg(any(target_os = "linux", target_os = "macos"))] mod peercred_unix_listener_stream; -pub mod importer; pub mod peerinfo; mod request_parser; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index c40b7aed487..92f31cf5f89 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -51,22 +51,6 @@ export declare namespace sshagent { publicKey: 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 { cipherId?: string isList: boolean @@ -79,7 +63,6 @@ export declare namespace sshagent { export function isRunning(agentState: SshAgentState): boolean export function setKeys(agentState: SshAgentState, newKeys: Array): void export function lock(agentState: SshAgentState): void - export function importKey(encodedKey: string, password: string): SshKeyImportResult export function clearKeys(agentState: SshAgentState): void export class SshAgentState { } } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 7d20bd50699..d0c859d427c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -182,67 +182,6 @@ pub mod sshagent { pub key_fingerprint: String, } - impl From 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 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, - } - - impl From 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)] pub struct SshUIRequest { pub cipher_id: Option, @@ -359,13 +298,6 @@ pub mod sshagent { .map_err(|e| napi::Error::from_reason(e.to_string())) } - #[napi] - pub fn import_key(encoded_key: String, password: String) -> napi::Result { - 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] pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { let bitwarden_agent_state = &mut agent_state.state; diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index edd07097b54..5f8b9762594 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -102,6 +102,7 @@ import { BiometricsService, } from "@bitwarden/key-management"; import { LockComponentService } from "@bitwarden/key-management-ui"; +import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; @@ -430,6 +431,11 @@ const safeProviders: SafeProvider[] = [ useClass: DesktopLoginApprovalComponentService, deps: [I18nServiceAbstraction], }), + safeProvider({ + provide: SshImportPromptService, + useClass: DefaultSshImportPromptService, + deps: [DialogService, ToastService, PlatformUtilsServiceAbstraction, I18nServiceAbstraction], + }), ]; @NgModule({ diff --git a/apps/desktop/src/autofill/main/main-ssh-agent.service.ts b/apps/desktop/src/autofill/main/main-ssh-agent.service.ts index af79d9d7316..595ef778bcf 100644 --- a/apps/desktop/src/autofill/main/main-ssh-agent.service.ts +++ b/apps/desktop/src/autofill/main/main-ssh-agent.service.ts @@ -25,16 +25,6 @@ export class MainSshAgentService { private logService: LogService, private messagingService: MessagingService, ) { - ipcMain.handle( - "sshagent.importkey", - async ( - event: any, - { privateKey, password }: { privateKey: string; password?: string }, - ): Promise => { - return sshagent.importKey(privateKey, password); - }, - ); - ipcMain.handle("sshagent.init", async (event: any, message: any) => { this.init(); }); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index b485b471ccb..7739ab84577 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3532,9 +3532,6 @@ "unknownApplication": { "message": "An application" }, - "sshKeyPasswordUnsupported": { - "message": "Importing password protected SSH keys is not yet supported" - }, "invalidSshKey": { "message": "The SSH key is invalid" }, @@ -3544,7 +3541,7 @@ "importSshKeyFromClipboard": { "message": "Import key from clipboard" }, - "sshKeyPasted": { + "sshKeyImported": { "message": "SSH key imported successfully" }, "fileSavedToDevice": { diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 05dcd484def..bf81021922f 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -1,4 +1,3 @@ -import { sshagent as ssh } from "desktop_native/napi"; import { ipcRenderer } from "electron"; import { DeviceType } from "@bitwarden/common/enums"; @@ -64,13 +63,6 @@ const sshAgent = { clearKeys: async () => { return await ipcRenderer.invoke("sshagent.clearkeys"); }, - importKey: async (key: string, password: string): Promise => { - const res = await ipcRenderer.invoke("sshagent.importkey", { - privateKey: key, - password: password, - }); - return res; - }, isLoaded(): Promise { return ipcRenderer.invoke("sshagent.isloaded"); }, diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index 6244f585bae..d79e15ebe6a 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -512,6 +512,15 @@ [ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }" > +
@@ -559,16 +568,6 @@
-
- -
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index ae332c9723b..2c8b5a8321a 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -3,8 +3,6 @@ import { DatePipe } from "@angular/common"; import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; 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 { 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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "AddEditComponent"; @@ -60,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, sdkService: SdkService, + sshImportPromptService: SshImportPromptService, ) { super( cipherService, @@ -82,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On cipherAuthorizationService, toastService, sdkService, + sshImportPromptService, ); } @@ -159,69 +158,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On 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 { - const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { - ariaModal: true, - }); - - return await lastValueFrom(dialog.closed); - } - truncateString(value: string, length: number) { return value.length > length ? value.substring(0, length) + "..." : value; } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 2cb1a4ee923..d0e876026d2 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -96,6 +96,7 @@ import { DefaultThemeStateService, ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, @@ -103,6 +104,7 @@ import { BiometricsService, } from "@bitwarden/key-management"; import { LockComponentService } from "@bitwarden/key-management-ui"; +import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -349,6 +351,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebLoginDecryptionOptionsService, deps: [MessagingService, RouterService, AcceptOrganizationInviteService], }), + safeProvider({ + provide: SshImportPromptService, + useClass: DefaultSshImportPromptService, + deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], + }), ]; @NgModule({ diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 3df2b9a83c9..68fcee367f1 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -30,7 +30,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; @Component({ selector: "app-vault-add-edit", @@ -76,6 +76,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On cipherAuthorizationService: CipherAuthorizationService, toastService: ToastService, sdkService: SdkService, + sshImportPromptService: SshImportPromptService, ) { super( cipherService, @@ -98,6 +99,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On cipherAuthorizationService, toastService, sdkService, + sshImportPromptService, ); } diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 8490ec6c9db..89f3b79f1fb 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -28,7 +28,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; 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"; @@ -64,6 +64,7 @@ export class AddEditComponent extends BaseAddEditComponent { cipherAuthorizationService: CipherAuthorizationService, toastService: ToastService, sdkService: SdkService, + sshImportPromptService: SshImportPromptService, ) { super( cipherService, @@ -88,6 +89,7 @@ export class AddEditComponent extends BaseAddEditComponent { cipherAuthorizationService, toastService, sdkService, + sshImportPromptService, ); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 229cca65ae5..3888b42fe76 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -38,7 +38,7 @@ "restoreMembers": { "message": "Restore members" }, - "cannotRestoreAccessError":{ + "cannotRestoreAccessError": { "message": "Cannot restore organization access" }, "allApplicationsWithCount": { @@ -1355,8 +1355,8 @@ "yourAccountIsLocked": { "message": "Your account is locked" }, - "uuid":{ - "message" : "UUID" + "uuid": { + "message": "UUID" }, "unlock": { "message": "Unlock" @@ -5904,10 +5904,10 @@ "bulkFilteredMessage": { "message": "Excluded, not applicable for this action" }, - "nonCompliantMembersTitle":{ + "nonCompliantMembersTitle": { "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" }, "fingerprint": { @@ -9330,7 +9330,7 @@ "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." }, - "userProvisioning":{ + "userProvisioning": { "message": "User provisioning" }, "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.", "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" }, "bwdcDesc": { "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, - "eventManagement":{ + "eventManagement": { "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." }, - "deviceManagement":{ + "deviceManagement": { "message": "Device management" }, - "deviceManagementDesc":{ + "deviceManagementDesc": { "message": "Configure device management for Bitwarden using the implementation guide for your platform." }, "desktopRequired": { @@ -9368,7 +9368,7 @@ "reopenLinkOnDesktop": { "message": "Reopen this link from your email on a desktop." }, - "integrationCardTooltip":{ + "integrationCardTooltip": { "message": "Launch $INTEGRATION$ implementation guide.", "placeholders": { "integration": { @@ -9377,7 +9377,7 @@ } } }, - "smIntegrationTooltip":{ + "smIntegrationTooltip": { "message": "Set up $INTEGRATION$.", "placeholders": { "integration": { @@ -9386,7 +9386,7 @@ } } }, - "smSdkTooltip":{ + "smSdkTooltip": { "message": "View $SDK$ repository", "placeholders": { "sdk": { @@ -9395,7 +9395,7 @@ } } }, - "integrationCardAriaLabel":{ + "integrationCardAriaLabel": { "message": "open $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9404,7 +9404,7 @@ } } }, - "smSdkAriaLabel":{ + "smSdkAriaLabel": { "message": "view $SDK$ repository in a new tab.", "placeholders": { "sdk": { @@ -9413,7 +9413,7 @@ } } }, - "smIntegrationCardAriaLabel":{ + "smIntegrationCardAriaLabel": { "message": "set up $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9820,7 +9820,7 @@ "message": "Config" }, "learnMoreAboutEmergencyAccess": { - "message":"Learn more about emergency access" + "message": "Learn more about emergency access" }, "learnMoreAboutMatchDetection": { "message": "Learn more about match detection" @@ -10122,7 +10122,7 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "claim-domain-single-org-warning" : { + "claim-domain-single-org-warning": { "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { @@ -10363,6 +10363,36 @@ "organizationNameMaxLength": { "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": { "message": "Opening the Bitwarden browser extension" }, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index c309aa9624a..c843d186625 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -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 { DialogService, ToastService } from "@bitwarden/components"; import { generate_ssh_key } from "@bitwarden/sdk-internal"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; @Directive() export class AddEditComponent implements OnInit, OnDestroy { @@ -131,7 +131,8 @@ export class AddEditComponent implements OnInit, OnDestroy { protected configService: ConfigService, protected cipherAuthorizationService: CipherAuthorizationService, protected toastService: ToastService, - private sdkService: SdkService, + protected sdkService: SdkService, + private sshImportPromptService: SshImportPromptService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -824,6 +825,15 @@ export class AddEditComponent implements OnInit, OnDestroy { 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) { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index c603e5cf170..d77e56d778e 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -13,6 +13,8 @@ "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/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/platform": ["../platform/src"], "@bitwarden/ui-common": ["../ui/common/src"], diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index e542d0dfc1f..7d0ef9e9c34 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -73,6 +73,7 @@ export class CipherExport { break; case CipherType.SshKey: view.sshKey = SshKeyExport.toView(req.sshKey); + break; } if (req.passwordHistory != null) { diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index a99ebac34b3..5387daf7dd0 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { import_ssh_key } from "@bitwarden/sdk-internal"; import { EncString } from "../../platform/models/domain/enc-string"; import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key"; @@ -17,16 +18,18 @@ export class SshKeyExport { } static toView(req: SshKeyExport, view = new SshKeyView()) { - view.privateKey = req.privateKey; - view.publicKey = req.publicKey; - view.keyFingerprint = req.keyFingerprint; + const parsedKey = import_ssh_key(req.privateKey); + view.privateKey = parsedKey.privateKey; + view.publicKey = parsedKey.publicKey; + view.keyFingerprint = parsedKey.fingerprint; return view; } static toDomain(req: SshKeyExport, domain = new SshKeyDomain()) { - domain.privateKey = req.privateKey != null ? new EncString(req.privateKey) : null; - domain.publicKey = req.publicKey != null ? new EncString(req.publicKey) : null; - domain.keyFingerprint = req.keyFingerprint != null ? new EncString(req.keyFingerprint) : null; + const parsedKey = import_ssh_key(req.privateKey); + domain.privateKey = new EncString(parsedKey.privateKey); + domain.publicKey = new EncString(parsedKey.publicKey); + domain.keyFingerprint = new EncString(parsedKey.fingerprint); return domain; } diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 2d1379f9c5f..dacc7d65ea5 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -9,6 +9,7 @@ // TODO: Remove once billing stops depending on components "@bitwarden/components": ["../components/src"], "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], "@bitwarden/platform": ["../platform/src"], // TODO: Remove once billing stops depending on components "@bitwarden/ui-common": ["../ui/common/src"] diff --git a/libs/importer/jest.config.js b/libs/importer/jest.config.js index ab449dc7757..ee5ae302b99 100644 --- a/libs/importer/jest.config.js +++ b/libs/importer/jest.config.js @@ -7,7 +7,7 @@ const sharedConfig = require("../shared/jest.config.ts"); /** @type {import('jest').Config} */ module.exports = { ...sharedConfig, - preset: "ts-jest", + preset: "jest-preset-angular", testEnvironment: "jsdom", moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { prefix: "/", diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 6ea58545352..79d194b87bc 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -37,6 +37,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -96,6 +97,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, PinServiceAbstraction, AccountService, + SdkService, ], }), ]; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 4de0bf70b07..908f062ecc1 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -1,16 +1,19 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { KeyService } from "@bitwarden/key-management"; +import { BitwardenClient } from "@bitwarden/sdk-internal"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; import { Importer } from "../importers/importer"; @@ -30,6 +33,7 @@ describe("ImportService", () => { let encryptService: MockProxy; let pinService: MockProxy; let accountService: MockProxy; + let sdkService: MockProxy; beforeEach(() => { cipherService = mock(); @@ -40,6 +44,9 @@ describe("ImportService", () => { keyService = mock(); encryptService = mock(); pinService = mock(); + const mockClient = mock(); + sdkService = mock(); + sdkService.client$ = of(mockClient, mockClient, mockClient); importService = new ImportService( cipherService, @@ -51,6 +58,7 @@ describe("ImportService", () => { encryptService, pinService, accountService, + sdkService, ); }); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 781b4f75e56..cc9cdc39320 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -15,6 +15,7 @@ import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/reque import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -114,6 +115,7 @@ export class ImportService implements ImportServiceAbstraction { private encryptService: EncryptService, private pinService: PinServiceAbstraction, private accountService: AccountService, + private sdkService: SdkService, ) {} getImportOptions(): ImportOption[] { diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index fdcfa200321..5d6464b4c79 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -24,6 +24,7 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -39,6 +40,8 @@ import { // eslint-disable-next-line no-restricted-imports 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 { TotpCaptureService } from "./abstractions/totp-capture.service"; import { CipherFormModule } from "./cipher-form.module"; @@ -146,6 +149,12 @@ export default { enabled$: new BehaviorSubject(true), }, }, + { + provide: SshImportPromptService, + useValue: { + importSshKeyFromClipboard: () => Promise.resolve(new SshKeyData()), + }, + }, { provide: CipherFormGenerationService, useValue: { diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html index 51b07a1cbf3..1b11b37084b 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html @@ -15,6 +15,14 @@ data-testid="toggle-privateKey-visibility" bitPasswordInputToggle > + diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 773ddd4ad66..500bb886f7a 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -7,7 +7,8 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { firstValueFrom } from "rxjs"; 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; @@ -22,6 +23,7 @@ import { } from "@bitwarden/components"; import { generate_ssh_key } from "@bitwarden/sdk-internal"; +import { SshImportPromptService } from "../../../services/ssh-import-prompt.service"; import { CipherFormContainer } from "../../cipher-form-container"; @Component({ @@ -60,11 +62,14 @@ export class SshKeySectionComponent implements OnInit { keyFingerprint: [""], }); + showImport = false; + constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, - private i18nService: I18nService, private sdkService: SdkService, + private sshImportPromptService: SshImportPromptService, + private platformUtilsService: PlatformUtilsService, ) { this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm); this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { @@ -87,6 +92,11 @@ export class SshKeySectionComponent implements OnInit { } 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 */ @@ -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() { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index e4857411d05..d21e430f0a3 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -25,8 +25,10 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon export * from "./components/carousel"; export * as VaultIcons from "./icons"; - 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 "./services/default-change-login-password.service"; diff --git a/libs/vault/src/services/default-ssh-import-prompt.service.ts b/libs/vault/src/services/default-ssh-import-prompt.service.ts new file mode 100644 index 00000000000..c4e51dd3638 --- /dev/null +++ b/libs/vault/src/services/default-ssh-import-prompt.service.ts @@ -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 { + 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 { + const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { + ariaModal: true, + }); + + return await firstValueFrom(dialog.closed); + } +} diff --git a/libs/vault/src/services/ssh-import-prompt.service.spec.ts b/libs/vault/src/services/ssh-import-prompt.service.spec.ts new file mode 100644 index 00000000000..49b2b898d7a --- /dev/null +++ b/libs/vault/src/services/ssh-import-prompt.service.spec.ts @@ -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; + let toastService: MockProxy; + let platformUtilsService: MockProxy; + let i18nService: MockProxy; + + beforeEach(() => { + dialogService = mock(); + toastService = mock(); + platformUtilsService = mock(); + i18nService = mock(); + + 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"); + }); + }); +}); diff --git a/libs/vault/src/services/ssh-import-prompt.service.ts b/libs/vault/src/services/ssh-import-prompt.service.ts new file mode 100644 index 00000000000..aae5159895b --- /dev/null +++ b/libs/vault/src/services/ssh-import-prompt.service.ts @@ -0,0 +1,5 @@ +import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data"; + +export abstract class SshImportPromptService { + abstract importSshKeyFromClipboard: () => Promise; +} diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index e1515183f22..6039dccd811 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -8,11 +8,13 @@ "@bitwarden/auth/common": ["../auth/src/common"], "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], + "@bitwarden/importer-ui": ["../importer/src/components"], "@bitwarden/generator-components": ["../tools/generator/components/src"], "@bitwarden/generator-core": ["../tools/generator/core/src"], "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/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/platform": ["../platform/src"], "@bitwarden/ui-common": ["../ui/common/src"], From e0b77c97bab8124bec6635cdb21d4dd7b7d4d116 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:59:07 -0700 Subject: [PATCH 02/43] [PM-18959] - retain popup view cache on cipher view or edit (#13742) * clear popup view cache on tab navigation but not on view or edit cipher * revert clearing cache on tab change * clean up function --- .../popup/view-cache/popup-view-cache.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index 2c29f1e5763..457198eaa4e 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -64,8 +64,16 @@ export class PopupViewCacheService implements ViewCacheService { filter((e) => e instanceof NavigationEnd), /** Skip the first navigation triggered by `popupRouterCacheGuard` */ skip(1), + filter((e: NavigationEnd) => + // viewing/editing a cipher and navigating back to the vault list should not clear the cache + ["/view-cipher", "/edit-cipher", "/tabs/vault"].every( + (route) => !e.urlAfterRedirects.startsWith(route), + ), + ), ) - .subscribe(() => this.clearState()); + .subscribe((e) => { + return this.clearState(); + }); } /** From beccf1a9d7fa43a9bb74389ee426c6669e85b753 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:59:16 -0700 Subject: [PATCH 03/43] increase size of password history dialog (#13693) --- .../app/vault/individual-vault/password-history.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/apps/web/src/app/vault/individual-vault/password-history.component.html index 4eaca8f736e..ccbe20c5192 100644 --- a/apps/web/src/app/vault/individual-vault/password-history.component.html +++ b/apps/web/src/app/vault/individual-vault/password-history.component.html @@ -1,4 +1,4 @@ - + {{ "passwordHistory" | i18n }} From a30a6ee7fb59718aa8bf9039e45f04f147a3842f Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:04:36 -0700 Subject: [PATCH 04/43] remove margin on autofill when no items present (#13691) --- .../autofill-vault-list-items.component.html | 1 + .../vault-list-items-container.component.html | 7 ++++++- .../vault-list-items-container.component.ts | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 071873b40c9..40f00ab4332 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -6,6 +6,7 @@ (onRefresh)="refreshCurrentTab()" [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" showAutofillButton + [disableDescriptionMargin]="showEmptyAutofillTip$ | async" [primaryActionAutofill]="clickItemsToAutofillVaultView" [groupByType]="groupByType()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index 2272d3fbd6c..cbce4bf2961 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -70,7 +70,12 @@ -
+
{{ description }}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index cb758e7a48d..9d70c0ba236 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -245,6 +245,12 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { @Input({ transform: booleanAttribute }) disableSectionMargin: boolean = false; + /** + * Remove the description margin + */ + @Input({ transform: booleanAttribute }) + disableDescriptionMargin: boolean = false; + /** * The tooltip text for the organization icon for ciphers that belong to an organization. * @param cipher From cda1cdb1090d0ec88dc6e1f608cce3cafab69be3 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:04:53 -0700 Subject: [PATCH 05/43] [PM-12726] - [Defect] [Web] "Secure Note" shows in item filters instead of "Note" (#13707) * use note instead of secure note * allow item history to be selectable * Revert "allow item history to be selectable" This reverts commit 7144a210b5707a3ef6db4ac162cd4ef1273ed8fa. --- .../vault-filter/components/vault-filter.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 9c002a4ed94..a232f8faec5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -243,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { }, { id: "note", - name: this.i18nService.t("typeSecureNote"), + name: this.i18nService.t("note"), type: CipherType.SecureNote, icon: "bwi-sticky-note", }, From 985942ac058de35d2bf5e939cc80a203177c1f0f Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:07:04 -0700 Subject: [PATCH 06/43] collapse collections initially (#13646) --- .../components/collection-filter.component.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts index d104026f2f6..168afbdd72a 100644 --- a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, EventEmitter, Input, Output } from "@angular/core"; +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { CollectionView } from "@bitwarden/admin-console/common"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -10,7 +10,7 @@ import { TopLevelTreeNode } from "../models/top-level-tree-node.model"; import { VaultFilter } from "../models/vault-filter.model"; @Directive() -export class CollectionFilterComponent { +export class CollectionFilterComponent implements OnInit { @Input() hide = false; @Input() collapsedFilterNodes: Set; @Input() collectionNodes: DynamicTreeNode; @@ -51,4 +51,13 @@ export class CollectionFilterComponent { async toggleCollapse(node: ITreeNodeObject) { this.onNodeCollapseStateChange.emit(node); } + + ngOnInit() { + // Populate the set with all node IDs so all nodes are collapsed initially. + if (this.collectionNodes?.fullList) { + this.collectionNodes.fullList.forEach((node) => { + this.collapsedFilterNodes.add(node.id); + }); + } + } } From a19bf1687e9c57375bc60e40c6b04b4ea495e4a8 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:07:22 -0700 Subject: [PATCH 07/43] [PM-12557] - center align custom field buttons (#13670) * center align custom field buttons * add margin --- .../components/custom-fields/custom-fields.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index fab3c8f1ab1..c7c5f4a930e 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -87,7 +87,7 @@ (click)="openAddEditCustomFieldDialog({ index: i, label: field.value.name })" [appA11yTitle]="'editFieldLabel' | i18n: field.value.name" bitIconButton="bwi-pencil-square" - class="tw-self-end" + class="tw-self-center tw-mt-2" data-testid="edit-custom-field-button" *ngIf="!isPartialEdit" > @@ -95,7 +95,7 @@ diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts index f89a72b5d2b..0f949e17146 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts @@ -31,7 +31,7 @@ export type VaultFilterSection = { }; action: (filterNode: TreeNode) => Promise; edit?: { - text: string; + filterName: string; action: (filter: VaultFilterType) => void; }; add?: { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3888b42fe76..1948f589661 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -491,6 +491,19 @@ "editFolder": { "message": "Edit folder" }, + "editWithName": { + "message": "Edit $ITEM$: $NAME$", + "placeholders": { + "item": { + "content": "$1", + "example": "login" + }, + "name": { + "content": "$2", + "example": "Social" + } + } + }, "newFolder": { "message": "New folder" }, From d943f53477613dc4572df981e23b22a99dd38266 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:12:02 -0700 Subject: [PATCH 09/43] refactor(routing): [Auth/PM-18783] Remove Unauth UI route swapping for all components except 2FA (#13645) Removes `unauthUiRefreshSwap()` from all routing modules for all refreshed components except for 2FA. This does not remove the legacy components themselves, just the routing to them. --------- Co-authored-by: Todd Martin --- .../src/auth/popup/environment.component.html | 2 +- .../auth/popup/set-password.component.html | 2 +- .../popup/utils/auth-popout-window.spec.ts | 2 +- .../auth/popup/utils/auth-popout-window.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 330 +++++++---------- apps/browser/src/popup/app.component.ts | 4 +- apps/desktop/src/app/app-routing.module.ts | 269 ++++++-------- apps/web/src/app/oss-routing.module.ts | 333 ++++++------------ .../auth/src/angular/login/login.component.ts | 32 +- 9 files changed, 343 insertions(+), 633 deletions(-) diff --git a/apps/browser/src/auth/popup/environment.component.html b/apps/browser/src/auth/popup/environment.component.html index ff19739548a..21e69fbbc39 100644 --- a/apps/browser/src/auth/popup/environment.component.html +++ b/apps/browser/src/auth/popup/environment.component.html @@ -1,7 +1,7 @@
- +

{{ "appName" | i18n }} diff --git a/apps/browser/src/auth/popup/set-password.component.html b/apps/browser/src/auth/popup/set-password.component.html index 6261608c345..71a2e3ac588 100644 --- a/apps/browser/src/auth/popup/set-password.component.html +++ b/apps/browser/src/auth/popup/set-password.component.html @@ -1,7 +1,7 @@
- +

{{ "setMasterPassword" | i18n }} diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 0b47fa4287e..b2c20ba2849 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -72,7 +72,7 @@ describe("AuthPopoutWindow", () => { it("closes any existing popup window types that are open to the login extension route", async () => { const loginTab = createChromeTabMock({ - url: chrome.runtime.getURL("popup/index.html#/home"), + url: chrome.runtime.getURL("popup/index.html#/login"), }); jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([loginTab]); jest.spyOn(BrowserApi, "removeWindow"); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 2f135038315..0646b684b22 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -13,7 +13,7 @@ const AuthPopoutType = { const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), - chrome.runtime.getURL("popup/index.html#/home"), + chrome.runtime.getURL("popup/index.html#/login"), ]); /** diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8f5d754b554..b33940a68d2 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -7,7 +7,6 @@ import { EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { activeAuthGuard, @@ -58,15 +57,9 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { HintComponent } from "../auth/popup/hint.component"; -import { HomeComponent } from "../auth/popup/home.component"; -import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "../auth/popup/login-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; -import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponentV1 } from "../auth/popup/two-factor-options-v1.component"; import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -131,20 +124,19 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [ popupRouterCacheGuard, - redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }), + redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/login", locked: "/lock" }), ], }, + { + path: "home", + redirectTo: "login", + pathMatch: "full", + }, { path: "vault", redirectTo: "/tabs/vault", pathMatch: "full", }, - { - path: "home", - component: HomeComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")], - data: { elevation: 1 } satisfies RouteDataProperties, - }, { path: "fido2", component: Fido2Component, @@ -206,40 +198,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...unauthUiRefreshSwap( - SsoComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "sso", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "sso", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageIcon: VaultIcon, - pageTitle: { - key: "enterpriseSingleSignOn", - }, - pageSubtitle: { - key: "singleSignOnEnterOrgIdentifierText", - }, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: SsoComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ), { path: "device-verification", component: ExtensionAnonLayoutWrapperComponent, @@ -420,158 +378,7 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login-with-device", - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "login-with-device", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "logInRequestSent", - }, - pageSubtitle: { - key: "aNotificationWasSentToYourDevice", - }, - showLogo: false, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: LoginViaAuthRequestComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "admin-approval-requested", - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "admin-approval-requested", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "adminApprovalRequested", - }, - pageSubtitle: { - key: "adminApprovalRequestSentToAdmins", - }, - showLogo: false, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [{ path: "", component: LoginViaAuthRequestComponent }], - }, - ), - ...unauthUiRefreshSwap( - HintComponent, - ExtensionAnonLayoutWrapperComponent, - { - path: "hint", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - elevation: 1, - } satisfies RouteDataProperties, - }, - { - path: "", - children: [ - { - path: "hint", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageTitle: { - key: "requestPasswordHint", - }, - pageSubtitle: { - key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", - }, - pageIcon: UserLockIcon, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: PasswordHintComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 }, - }, - { - path: "", - children: [ - { - path: "login", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageIcon: VaultIcon, - pageTitle: { - key: "logInToBitwarden", - }, - elevation: 1, - showAcctSwitcher: true, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: LoginComponent }, - { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginDecryptionOptionsComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { - pageIcon: DevicesIcon, - }, - children: [{ path: "", component: LoginDecryptionOptionsComponent }], - }, - ), + { path: "", component: ExtensionAnonLayoutWrapperComponent, @@ -597,7 +404,7 @@ const routes: Routes = [ component: RegistrationStartSecondaryComponent, outlet: "secondary", data: { - loginRoute: "/home", + loginRoute: "/login", } satisfies RegistrationStartSecondaryComponentData, }, ], @@ -617,6 +424,127 @@ const routes: Routes = [ }, ], }, + { + path: "login", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "logInToBitwarden", + }, + elevation: 1, + showAcctSwitcher: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginComponent }, + { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "sso", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: SsoComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "logInRequestSent", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "hint", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, + pageIcon: UserLockIcon, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + showLogo: false, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + { + path: "login-initiated", + canActivate: [tdeDecryptionRequiredGuard()], + data: { + pageIcon: DevicesIcon, + }, + children: [{ path: "", component: LoginDecryptionOptionsComponent }], + }, { path: "lock", canActivate: [lockGuard()], diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 9a3b6429e61..2a7fbdce254 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -113,9 +113,7 @@ export class AppComponent implements OnInit, OnDestroy { }); this.changeDetectorRef.detectChanges(); } else if (msg.command === "authBlocked" || msg.command === "goHome") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["home"]); + await this.router.navigate(["login"]); } else if ( msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 19b92d4762a..3a30629b444 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -51,13 +51,8 @@ import { import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { HintComponent } from "../auth/hint.component"; -import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "../auth/login/login-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; -import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; @@ -167,33 +162,6 @@ const routes: Routes = [ }, { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, - ...unauthUiRefreshSwap( - SsoComponentV1, - AnonLayoutWrapperComponent, - { - path: "sso", - }, - { - path: "sso", - data: { - pageIcon: VaultIcon, - pageTitle: { - key: "enterpriseSingleSignOn", - }, - pageSubtitle: { - key: "singleSignOnEnterOrgIdentifierText", - }, - } satisfies AnonLayoutWrapperData, - children: [ - { path: "", component: SsoComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), { path: "send", component: SendComponent, @@ -209,139 +177,6 @@ const routes: Routes = [ component: RemovePasswordComponent, canActivate: [authGuard], }, - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - AnonLayoutWrapperComponent, - { - path: "login-with-device", - }, - { - path: "login-with-device", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "logInRequestSent", - }, - pageSubtitle: { - key: "aNotificationWasSentToYourDevice", - }, - } satisfies AnonLayoutWrapperData, - children: [ - { path: "", component: LoginViaAuthRequestComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - AnonLayoutWrapperComponent, - { - path: "admin-approval-requested", - }, - { - path: "admin-approval-requested", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "adminApprovalRequested", - }, - pageSubtitle: { - key: "adminApprovalRequestSentToAdmins", - }, - } satisfies AnonLayoutWrapperData, - children: [{ path: "", component: LoginViaAuthRequestComponent }], - }, - ), - ...unauthUiRefreshSwap( - HintComponent, - AnonLayoutWrapperComponent, - { - path: "hint", - canActivate: [unauthGuardFn()], - }, - { - path: "", - children: [ - { - path: "hint", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "requestPasswordHint", - }, - pageSubtitle: { - key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", - }, - pageIcon: UserLockIcon, - } satisfies AnonLayoutWrapperData, - children: [ - { path: "", component: PasswordHintComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginComponentV1, - AnonLayoutWrapperComponent, - { - path: "login", - component: LoginComponentV1, - canActivate: [maxAccountsGuardFn()], - }, - { - path: "", - children: [ - { - path: "login", - canActivate: [maxAccountsGuardFn()], - data: { - pageTitle: { - key: "logInToBitwarden", - }, - pageIcon: VaultIcon, - }, - children: [ - { path: "", component: LoginComponent }, - { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: DesktopDefaultOverlayPosition, - }, - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginDecryptionOptionsComponentV1, - AnonLayoutWrapperComponent, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - }, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { - pageIcon: DevicesIcon, - }, - children: [{ path: "", component: LoginDecryptionOptionsComponent }], - }, - ), { path: "", component: AnonLayoutWrapperComponent, @@ -383,6 +218,110 @@ const routes: Routes = [ }, ], }, + { + path: "login", + canActivate: [maxAccountsGuardFn()], + data: { + pageTitle: { + key: "logInToBitwarden", + }, + pageIcon: VaultIcon, + }, + children: [ + { path: "", component: LoginComponent }, + { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: DesktopDefaultOverlayPosition, + }, + }, + ], + }, + { + path: "login-initiated", + canActivate: [tdeDecryptionRequiredGuard()], + data: { + pageIcon: DevicesIcon, + }, + children: [{ path: "", component: LoginDecryptionOptionsComponent }], + }, + { + path: "sso", + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + } satisfies AnonLayoutWrapperData, + children: [ + { path: "", component: SsoComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "logInRequestSent", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + } satisfies AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + } satisfies AnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, + pageIcon: UserLockIcon, + } satisfies AnonLayoutWrapperData, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: "lock", canActivate: [lockGuard()], diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index adb38ef43f0..c531f358b34 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -55,10 +55,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link.guard"; -import { HintComponent } from "./auth/hint.component"; -import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "./auth/login/login-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component"; import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component"; import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; @@ -69,7 +65,6 @@ import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module"; -import { SsoComponentV1 } from "./auth/sso-v1.component"; import { TwoFactorComponentV1 } from "./auth/two-factor-v1.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component"; @@ -172,172 +167,6 @@ const routes: Routes = [ }, ], }, - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - AnonLayoutWrapperComponent, - { - path: "login-with-device", - data: { titleId: "loginWithDevice" } satisfies RouteDataProperties, - }, - { - path: "login-with-device", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "logInRequestSent", - }, - pageSubtitle: { - key: "aNotificationWasSentToYourDevice", - }, - titleId: "loginInitiated", - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { path: "", component: LoginViaAuthRequestComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - AnonLayoutWrapperComponent, - { - path: "admin-approval-requested", - data: { titleId: "adminApprovalRequested" } satisfies RouteDataProperties, - }, - { - path: "admin-approval-requested", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "adminApprovalRequested", - }, - pageSubtitle: { - key: "adminApprovalRequestSentToAdmins", - }, - titleId: "adminApprovalRequested", - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [{ path: "", component: LoginViaAuthRequestComponent }], - }, - ), - ...unauthUiRefreshSwap( - AnonLayoutWrapperComponent, - AnonLayoutWrapperComponent, - { - path: "login", - canActivate: [unauthGuardFn()], - children: [ - { - path: "", - component: LoginComponentV1, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - data: { - pageTitle: { - key: "logIn", - }, - }, - }, - { - path: "login", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "logInToBitwarden", - }, - pageIcon: VaultIcon, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: LoginComponent, - }, - { - path: "", - component: LoginSecondaryContentComponent, - outlet: "secondary", - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginDecryptionOptionsComponentV1, - AnonLayoutWrapperComponent, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - }, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { - pageIcon: DevicesIcon, - }, - children: [{ path: "", component: LoginDecryptionOptionsComponent }], - }, - ), - ...unauthUiRefreshSwap( - AnonLayoutWrapperComponent, - AnonLayoutWrapperComponent, - { - path: "hint", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "passwordHint", - }, - titleId: "passwordHint", - }, - children: [ - { path: "", component: HintComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - { - path: "", - children: [ - { - path: "hint", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "requestPasswordHint", - }, - pageSubtitle: { - key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", - }, - pageIcon: UserLockIcon, - state: "hint", - }, - children: [ - { path: "", component: PasswordHintComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ], - }, - ), { path: "", component: AnonLayoutWrapperComponent, @@ -381,6 +210,97 @@ const routes: Routes = [ }, ], }, + { + path: "login", + canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "logInToBitwarden", + }, + pageIcon: VaultIcon, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: LoginComponent, + }, + { + path: "", + component: LoginSecondaryContentComponent, + outlet: "secondary", + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "logInRequestSent", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + titleId: "loginInitiated", + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + titleId: "adminApprovalRequested", + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + { + path: "hint", + canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, + pageIcon: UserLockIcon, + state: "hint", + }, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "login-initiated", + canActivate: [tdeDecryptionRequiredGuard()], + data: { + pageIcon: DevicesIcon, + }, + children: [{ path: "", component: LoginDecryptionOptionsComponent }], + }, { path: "send/:sendId/:key", data: { @@ -432,64 +352,24 @@ const routes: Routes = [ }, ], }, - ...unauthUiRefreshSwap( - SsoComponentV1, - SsoComponent, - { - path: "sso", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "enterpriseSingleSignOn", - }, - titleId: "enterpriseSingleSignOn", - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: SsoComponentV1, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - { - path: "sso", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "singleSignOn", - }, - titleId: "enterpriseSingleSignOn", - pageSubtitle: { - key: "singleSignOnEnterOrgIdentifierText", - }, - titleAreaMaxWidth: "md", - pageIcon: SsoKeyIcon, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: SsoComponent, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), { - path: "login", + path: "sso", canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "singleSignOn", + }, + titleId: "enterpriseSingleSignOn", + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + titleAreaMaxWidth: "md", + pageIcon: SsoKeyIcon, + } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ { path: "", - component: LoginComponent, + component: SsoComponent, }, { path: "", @@ -497,11 +377,6 @@ const routes: Routes = [ outlet: "environment-selector", }, ], - data: { - pageTitle: { - key: "logIn", - }, - }, }, ...unauthUiRefreshSwap( TwoFactorComponentV1, diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index c291a64a8c5..cc38ec5dfb3 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; import { ActivatedRoute, Router, RouterModule } from "@angular/router"; -import { firstValueFrom, Subject, take, takeUntil, tap } from "rxjs"; +import { firstValueFrom, Subject, take, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -19,11 +19,9 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -121,7 +119,6 @@ export class LoginComponent implements OnInit, OnDestroy { private toastService: ToastService, private logService: LogService, private validationService: ValidationService, - private configService: ConfigService, private loginSuccessHandlerService: LoginSuccessHandlerService, ) { this.clientType = this.platformUtilsService.getClientType(); @@ -131,9 +128,6 @@ export class LoginComponent implements OnInit, OnDestroy { // Add popstate listener to listen for browser back button clicks window.addEventListener("popstate", this.handlePopState); - // TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed. - this.listenForUnauthUiRefreshFlagChanges(); - await this.defaultOnInit(); if (this.clientType === ClientType.Desktop) { @@ -154,30 +148,6 @@ export class LoginComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private listenForUnauthUiRefreshFlagChanges() { - this.configService - .getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh) - .pipe( - tap(async (flag) => { - // If the flag is turned OFF, we must force a reload to ensure the correct UI is shown - if (!flag) { - const qParams = await firstValueFrom(this.activatedRoute.queryParams); - const uniqueQueryParams = { - ...qParams, - // adding a unique timestamp to the query params to force a reload - t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter - }; - - await this.router.navigate(["/"], { - queryParams: uniqueQueryParams, - }); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } - submit = async (): Promise => { if (this.clientType === ClientType.Desktop) { if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) { From c3c4c9c54c51fab19ac56f3cee94d898139e8606 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:12:24 -0700 Subject: [PATCH 10/43] bold new settings callout link (#13664) --- .../new-settings-callout/new-settings-callout.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html index a6abe8ed3ac..6cc60eed6d5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html @@ -17,6 +17,7 @@ Date: Mon, 10 Mar 2025 14:58:11 -0400 Subject: [PATCH 11/43] add tw class (#13765) --- .../manage/device-approvals/device-approvals.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html index 7723324781b..cafd0744a8f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html @@ -1,7 +1,7 @@ From 0568a09212f56c58435a59935b3284ad93734bbf Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:17:46 -0700 Subject: [PATCH 12/43] refactor(device-trust-toasts): [Auth/PM-11225] Refactor Toasts from Auth Services (#13665) Refactor toast calls out of auth services. Toasts are now triggered by an observable emission that gets picked up by an observable pipeline in a new `DeviceTrustToastService` (libs/angular). That observable pipeline is then subscribed by by consuming the `AppComponent` for each client. --- apps/browser/src/popup/app.component.ts | 7 +- apps/desktop/src/app/app.component.ts | 7 +- apps/web/src/app/app.component.ts | 7 +- .../device-trust-toast.service.abstraction.ts | 9 + ...vice-trust-toast.service.implementation.ts | 44 +++++ .../device-trust-toast.service.spec.ts | 167 ++++++++++++++++++ .../src/services/jslib-services.module.ts | 12 ++ .../auth-request.service.abstraction.ts | 15 ++ .../login-strategies/sso-login.strategy.ts | 3 +- .../auth-request/auth-request.service.ts | 9 + .../device-trust.service.abstraction.ts | 6 + .../device-trust.service.implementation.ts | 9 +- 12 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 libs/angular/src/auth/services/device-trust-toast.service.abstraction.ts create mode 100644 libs/angular/src/auth/services/device-trust-toast.service.implementation.ts create mode 100644 libs/angular/src/auth/services/device-trust-toast.service.spec.ts diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 2a7fbdce254..6a08bf007bb 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,9 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; +import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -68,7 +70,10 @@ export class AppComponent implements OnInit, OnDestroy { private animationControlService: AnimationControlService, private biometricStateService: BiometricStateService, private biometricsService: BiometricsService, - ) {} + private deviceTrustToastService: DeviceTrustToastService, + ) { + this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); + } async ngOnInit() { initPopupClosedListener(); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 85c159f0278..1ef03e5bb71 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -10,10 +10,12 @@ import { ViewChild, ViewContainerRef, } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { filter, firstValueFrom, map, Subject, takeUntil, timeout, withLatestFrom } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular"; @@ -157,7 +159,10 @@ export class AppComponent implements OnInit, OnDestroy { private stateEventRunnerService: StateEventRunnerService, private accountService: AccountService, private organizationService: OrganizationService, - ) {} + private deviceTrustToastService: DeviceTrustToastService, + ) { + this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); + } ngOnInit() { this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 9d2afb22688..f6e038f85d9 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,11 +2,13 @@ // @ts-strict-ignore import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -95,7 +97,10 @@ export class AppComponent implements OnDestroy, OnInit { private apiService: ApiService, private appIdService: AppIdService, private processReloadService: ProcessReloadServiceAbstraction, - ) {} + private deviceTrustToastService: DeviceTrustToastService, + ) { + this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); + } ngOnInit() { this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => { diff --git a/libs/angular/src/auth/services/device-trust-toast.service.abstraction.ts b/libs/angular/src/auth/services/device-trust-toast.service.abstraction.ts new file mode 100644 index 00000000000..3de288168b1 --- /dev/null +++ b/libs/angular/src/auth/services/device-trust-toast.service.abstraction.ts @@ -0,0 +1,9 @@ +import { Observable } from "rxjs"; + +export abstract class DeviceTrustToastService { + /** + * An observable pipeline that observes any cross-application toast messages + * that need to be shown as part of the trusted device encryption (TDE) process. + */ + abstract setupListeners$: Observable; +} diff --git a/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts b/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts new file mode 100644 index 00000000000..330519683f3 --- /dev/null +++ b/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts @@ -0,0 +1,44 @@ +import { merge, Observable, tap } from "rxjs"; + +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; + +import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction"; + +export class DeviceTrustToastService implements DeviceTrustToastServiceAbstraction { + private adminLoginApproved$: Observable; + private deviceTrusted$: Observable; + + setupListeners$: Observable; + + constructor( + private authRequestService: AuthRequestServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, + private i18nService: I18nService, + private toastService: ToastService, + ) { + this.adminLoginApproved$ = this.authRequestService.adminLoginApproved$.pipe( + tap(() => { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("loginApproved"), + }); + }), + ); + + this.deviceTrusted$ = this.deviceTrustService.deviceTrusted$.pipe( + tap(() => { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("deviceTrusted"), + }); + }), + ); + + this.setupListeners$ = merge(this.adminLoginApproved$, this.deviceTrusted$); + } +} diff --git a/libs/angular/src/auth/services/device-trust-toast.service.spec.ts b/libs/angular/src/auth/services/device-trust-toast.service.spec.ts new file mode 100644 index 00000000000..cd9c6b0acf5 --- /dev/null +++ b/libs/angular/src/auth/services/device-trust-toast.service.spec.ts @@ -0,0 +1,167 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { EMPTY, of } from "rxjs"; + +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; + +import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction"; +import { DeviceTrustToastService } from "./device-trust-toast.service.implementation"; + +describe("DeviceTrustToastService", () => { + let authRequestService: MockProxy; + let deviceTrustService: MockProxy; + let i18nService: MockProxy; + let toastService: MockProxy; + + let sut: DeviceTrustToastServiceAbstraction; + + beforeEach(() => { + authRequestService = mock(); + deviceTrustService = mock(); + i18nService = mock(); + toastService = mock(); + + i18nService.t.mockImplementation((key: string) => key); // just return the key that was given + }); + + const initService = () => { + return new DeviceTrustToastService( + authRequestService, + deviceTrustService, + i18nService, + toastService, + ); + }; + + const loginApprovalToastOptions = { + variant: "success", + title: "", + message: "loginApproved", + }; + + const deviceTrustedToastOptions = { + variant: "success", + title: "", + message: "deviceTrusted", + }; + + describe("setupListeners$", () => { + describe("given adminLoginApproved$ emits and deviceTrusted$ emits", () => { + beforeEach(() => { + // Arrange + authRequestService.adminLoginApproved$ = of(undefined); + deviceTrustService.deviceTrusted$ = of(undefined); + sut = initService(); + }); + + it("should trigger a toast for login approval", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert + done(); + }, + }); + }); + + it("should trigger a toast for device trust", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert + done(); + }, + }); + }); + }); + + describe("given adminLoginApproved$ emits and deviceTrusted$ does not emit", () => { + beforeEach(() => { + // Arrange + authRequestService.adminLoginApproved$ = of(undefined); + deviceTrustService.deviceTrusted$ = EMPTY; + sut = initService(); + }); + + it("should trigger a toast for login approval", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert + done(); + }, + }); + }); + + it("should NOT trigger a toast for device trust", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert + done(); + }, + }); + }); + }); + + describe("given adminLoginApproved$ does not emit and deviceTrusted$ emits", () => { + beforeEach(() => { + // Arrange + authRequestService.adminLoginApproved$ = EMPTY; + deviceTrustService.deviceTrusted$ = of(undefined); + sut = initService(); + }); + + it("should NOT trigger a toast for login approval", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert + done(); + }, + }); + }); + + it("should trigger a toast for device trust", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert + done(); + }, + }); + }); + }); + + describe("given adminLoginApproved$ does not emit and deviceTrusted$ does not emit", () => { + beforeEach(() => { + // Arrange + authRequestService.adminLoginApproved$ = EMPTY; + deviceTrustService.deviceTrusted$ = EMPTY; + sut = initService(); + }); + + it("should NOT trigger a toast for login approval", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert + done(); + }, + }); + }); + + it("should NOT trigger a toast for device trust", (done) => { + // Act + sut.setupListeners$.subscribe({ + complete: () => { + expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert + done(); + }, + }); + }); + }); + }); +}); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9ee49a30689..93e29846e69 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -317,6 +317,8 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; +import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -1463,6 +1465,16 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultTaskService, deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService], }), + safeProvider({ + provide: DeviceTrustToastServiceAbstraction, + useClass: DeviceTrustToastService, + deps: [ + AuthRequestServiceAbstraction, + DeviceTrustServiceAbstraction, + I18nServiceAbstraction, + ToastService, + ], + }), ]; @NgModule({ diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 1829fe6b0c9..75bb8686163 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -12,6 +12,12 @@ export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ authRequestPushNotification$: Observable; + /** + * Emits when a login has been approved by an admin. This emission is specifically for the + * purpose of notifying the consuming component to display a toast informing the user. + */ + adminLoginApproved$: Observable; + /** * Returns an admin auth request for the given user if it exists. * @param userId The user id. @@ -106,4 +112,13 @@ export abstract class AuthRequestServiceAbstraction { * @returns The dash-delimited fingerprint phrase. */ abstract getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise; + + /** + * Passes a value to the adminLoginApprovedSubject via next(), which causes the + * adminLoginApproved$ observable to emit. + * + * The purpose is to notify consuming components (of adminLoginApproved$) to display + * a toast informing the user that a login has been approved by an admin. + */ + abstract emitAdminLoginApproved(): void; } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index e9d1e0a6c88..ac00ff69a4d 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -278,7 +278,8 @@ export class SsoLoginStrategy extends LoginStrategy { // TODO: eventually we post and clean up DB as well once consumed on client await this.authRequestService.clearAdminAuthRequest(userId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); + // This notification will be picked up by the SsoComponent to handle displaying a toast to the user + this.authRequestService.emitAdminLoginApproved(); } } } diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index 5bc200ae1e8..a6841afe0ff 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -43,6 +43,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private authRequestPushNotificationSubject = new Subject(); authRequestPushNotification$: Observable; + // Observable emission is used to trigger a toast in consuming components + private adminLoginApprovedSubject = new Subject(); + adminLoginApproved$: Observable; + constructor( private appIdService: AppIdService, private accountService: AccountService, @@ -53,6 +57,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private stateProvider: StateProvider, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); + this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable(); } async getAdminAuthRequest(userId: UserId): Promise { @@ -207,4 +212,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { async getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise { return (await this.keyService.getFingerprint(email.toLowerCase(), publicKey)).join("-"); } + + emitAdminLoginApproved(): void { + this.adminLoginApprovedSubject.next(); + } } diff --git a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 24a5d4e8413..2de63b36cc7 100644 --- a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -16,6 +16,12 @@ export abstract class DeviceTrustServiceAbstraction { */ supportsDeviceTrust$: Observable; + /** + * Emits when a device has been trusted. This emission is specifically for the purpose of notifying + * the consuming component to display a toast informing the user the device has been trusted. + */ + deviceTrusted$: Observable; + /** * @description Checks if the device trust feature is supported for the given user. */ diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index fe43df53c48..4a1b901d5ef 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map, Observable, Subject } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { KeyService } from "@bitwarden/key-management"; @@ -63,6 +63,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { supportsDeviceTrust$: Observable; + // Observable emission is used to trigger a toast in consuming components + private deviceTrustedSubject = new Subject(); + deviceTrusted$ = this.deviceTrustedSubject.asObservable(); + constructor( private keyGenerationService: KeyGenerationService, private cryptoFunctionService: CryptoFunctionService, @@ -177,7 +181,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { // store device key in local/secure storage if enc keys posted to server successfully await this.setDeviceKey(userId, deviceKey); - this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted")); + // This emission will be picked up by consuming components to handle displaying a toast to the user + this.deviceTrustedSubject.next(); return deviceResponse; } From 337597cf818712f872ac1dd64fece2b2f5c9aa6e Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:23:42 -0500 Subject: [PATCH 13/43] fix(auth): [PM-10775] Fix spacing of horizontal rules in SSO component - Remove horizontal rule above "Member decryption options" section - Add 1rem margin below horizontal rule before "type" section Resolves PM-10775 --- bitwarden_license/bit-web/src/app/auth/sso/sso.component.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 22887cb7094..9cead9d21f3 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -38,8 +38,6 @@ -
- {{ "memberDecryptionOption" | i18n }} @@ -156,7 +154,7 @@
-
+
{{ "type" | i18n }} From f682870e4117aea3fd8cb2973bdb8a8e36d09ff4 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 10 Mar 2025 15:36:21 -0400 Subject: [PATCH 14/43] remove class, add tw class (#13768) --- .../domain-verification/domain-verification.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index c292d51ebda..adf9fcd2dcf 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -24,7 +24,7 @@ @@ -74,7 +74,7 @@ {{ orgDomain.lastCheckedDate | date: "medium" }} - +

diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index af336c94854..ded456ff963 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -33,73 +33,69 @@ - + - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - {{ "timesReused" | i18n }} - + + {{ "name" | i18n }} + {{ "owner" | i18n }} + {{ "timesReused" | i18n }} - - - - - - - - {{ r.name }} - - - {{ r.name }} - - - - {{ "shared" | i18n }} - - - - {{ "attachments" | i18n }} - -
- {{ r.subTitle }} - - - + + + + + + {{ row.name }} - - - - - {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }} - - - + + + {{ row.name }} + + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ row.subTitle }} + + + + + + + + {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} + +
-
+ diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index 9c5b587e60a..3ef1d11f9b2 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -31,77 +31,73 @@ - + - - - {{ "name" | i18n }} - - {{ "owner" | i18n }} - - - {{ "weakness" | i18n }} - - + + {{ "name" | i18n }} + + {{ "owner" | i18n }} + + + {{ "weakness" | i18n }} + - - - - - - - - {{ r.name }} - - - {{ r.name }} - - - - {{ "shared" | i18n }} - - - - {{ "attachments" | i18n }} - -
- {{ r.subTitle }} - - - + + + + + + {{ row.name }} - - - - - {{ r.reportValue.label | i18n }} - - - + + + {{ row.name }} + + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ row.subTitle }} + + + + + + + + {{ row.reportValue.label | i18n }} + +
-
+ diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html index 26b06ee0e5c..8f2c88ba3ad 100644 --- a/libs/components/src/table/table-scroll.component.html +++ b/libs/components/src/table/table-scroll.component.html @@ -12,7 +12,7 @@ - + From 992be1d054e30cb6b1b2b9c357f80fcb27b9e0cb Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:57:02 -0700 Subject: [PATCH 16/43] [PM-13991] - Edit login - reorder website URIs (#13595) * WIP - sortable website uri * add specs * fix type errors in tests --- apps/browser/src/_locales/en/messages.json | 6 + apps/web/src/locales/en/messages.json | 37 ++++++ .../autofill-options.component.html | 5 +- .../autofill-options.component.spec.ts | 116 ++++++++++++++++++ .../autofill-options.component.ts | 56 +++++++++ .../uri-option.component.html | 81 +++++++----- .../autofill-options/uri-option.component.ts | 15 +++ 7 files changed, 282 insertions(+), 34 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cb3d8c12ef4..127e07f25e8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1679,6 +1679,9 @@ "dragToSort": { "message": "Drag to sort" }, + "dragToReorder": { + "message": "Drag to reorder" + }, "cfTypeText": { "message": "Text" }, @@ -4706,6 +4709,9 @@ } } }, + "reorderWebsiteUriButton": { + "message": "Reorder website URI. Use arrow key to move item up or down." + }, "reorderFieldUp": { "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", "placeholders": { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1948f589661..22d2869b4b5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -449,6 +449,9 @@ "dragToSort": { "message": "Drag to sort" }, + "dragToReorder": { + "message": "Drag to reorder" + }, "cfTypeText": { "message": "Text" }, @@ -4564,6 +4567,40 @@ } } }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, "keyUpdateFoldersFailed": { "message": "When updating your encryption key, your folders could not be decrypted. To continue with the update, your folders must be deleted. No vault items will be deleted if you proceed." }, diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html index 6f7dd35be9e..42164ae6854 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.html @@ -5,12 +5,15 @@ - + { + const actual = jest.requireActual("@angular/cdk/drag-drop"); + return { + ...actual, + moveItemInArray: jest.fn(actual.moveItemInArray), + }; +}); + describe("AutofillOptionsComponent", () => { let component: AutofillOptionsComponent; let fixture: ComponentFixture; @@ -255,4 +264,111 @@ describe("AutofillOptionsComponent", () => { expect(component.autofillOptionsForm.value.uris.length).toEqual(1); }); + + describe("Drag & Drop Functionality", () => { + beforeEach(() => { + // Prevent auto‑adding an empty URI by setting a non‑null initial value. + // This overrides the call to initNewCipher. + + // Now clear any existing URIs (including the auto‑added one) + component.autofillOptionsForm.controls.uris.clear(); + + // Add exactly three URIs that we want to test reordering on. + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + component.addUri({ uri: "https://third.com", matchDetection: null }); + fixture.detectChanges(); + }); + + it("should reorder URI inputs on drop event", () => { + // Simulate a drop event that moves the first URI (index 0) to the last position (index 2). + const dropEvent: CdkDragDrop = { + previousIndex: 0, + currentIndex: 2, + container: null, + previousContainer: null, + isPointerOverContainer: true, + item: null, + distance: { x: 0, y: 0 }, + } as any; + + component.onUriItemDrop(dropEvent); + fixture.detectChanges(); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 0, + 2, + ); + }); + + it("should reorder URI input via keyboard ArrowUp", async () => { + // Clear and add exactly two URIs. + component.autofillOptionsForm.controls.uris.clear(); + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + fixture.detectChanges(); + + // Simulate pressing ArrowUp on the second URI (index 1) + const keyEvent = { + key: "ArrowUp", + preventDefault: jest.fn(), + target: document.createElement("button"), + } as unknown as KeyboardEvent; + + // Force requestAnimationFrame to run synchronously + jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => { + cb(new Date().getTime()); + return 0; + }); + (liveAnnouncer.announce as jest.Mock).mockResolvedValue(null); + + await component.onUriItemKeydown(keyEvent, 1); + fixture.detectChanges(); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 1, + 0, + ); + expect(liveAnnouncer.announce).toHaveBeenCalledWith( + "reorderFieldUp websiteUri 1 2", + "assertive", + ); + }); + + it("should reorder URI input via keyboard ArrowDown", async () => { + // Clear and add exactly three URIs. + component.autofillOptionsForm.controls.uris.clear(); + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + component.addUri({ uri: "https://third.com", matchDetection: null }); + fixture.detectChanges(); + + // Simulate pressing ArrowDown on the second URI (index 1) + const keyEvent = { + key: "ArrowDown", + preventDefault: jest.fn(), + target: document.createElement("button"), + } as unknown as KeyboardEvent; + + jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => { + cb(new Date().getTime()); + return 0; + }); + (liveAnnouncer.announce as jest.Mock).mockResolvedValue(null); + + await component.onUriItemKeydown(keyEvent, 1); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 1, + 2, + ); + expect(liveAnnouncer.announce).toHaveBeenCalledWith( + "reorderFieldDown websiteUri 3 3", + "assertive", + ); + }); + }); }); diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index c3b2a0fb9f9..5b1e4eca103 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { CdkDragDrop, DragDropModule, moveItemInArray } from "@angular/cdk/drag-drop"; import { AsyncPipe, NgForOf, NgIf } from "@angular/common"; import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -41,6 +42,7 @@ interface UriField { templateUrl: "./autofill-options.component.html", standalone: true, imports: [ + DragDropModule, SectionComponent, SectionHeaderComponent, TypographyModule, @@ -229,4 +231,58 @@ export class AutofillOptionsComponent implements OnInit { removeUri(i: number) { this.autofillOptionsForm.controls.uris.removeAt(i); } + + /** Create a new list of LoginUriViews from the form objects and update the cipher */ + private updateUriFields() { + this.cipherFormContainer.patchCipher((cipher) => { + cipher.login.uris = this.uriControls.map( + (control) => + Object.assign(new LoginUriView(), { + uri: control.value.uri, + matchDetection: control.value.matchDetection ?? null, + }) as LoginUriView, + ); + return cipher; + }); + } + + /** Reorder the controls to match the new order after a "drop" event */ + onUriItemDrop(event: CdkDragDrop) { + moveItemInArray(this.uriControls, event.previousIndex, event.currentIndex); + this.updateUriFields(); + } + + /** Handles a uri item keyboard up or down event */ + async onUriItemKeydown(event: KeyboardEvent, index: number) { + if (event.key === "ArrowUp" && index !== 0) { + await this.reorderUriItems(event, index, "Up"); + } + + if (event.key === "ArrowDown" && index !== this.uriControls.length - 1) { + await this.reorderUriItems(event, index, "Down"); + } + } + + /** Reorders the uri items from a keyboard up or down event */ + async reorderUriItems(event: KeyboardEvent, previousIndex: number, direction: "Up" | "Down") { + const currentIndex = previousIndex + (direction === "Up" ? -1 : 1); + event.preventDefault(); + await this.liveAnnouncer.announce( + this.i18nService.t( + `reorderField${direction}`, + this.i18nService.t("websiteUri"), + currentIndex + 1, + this.uriControls.length, + ), + "assertive", + ); + moveItemInArray(this.uriControls, previousIndex, currentIndex); + this.updateUriFields(); + // Refocus the button after the reorder + // Angular re-renders the list when moving an item up which causes the focus to be lost + // Wait for the next tick to ensure the button is rendered before focusing + requestAnimationFrame(() => { + (event.target as HTMLButtonElement).focus(); + }); + } } diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html index a55716083de..5301e4f32b9 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html @@ -1,35 +1,50 @@ - - {{ uriLabel }} - - - - - - - {{ "matchDetection" | i18n }} - - - - +
+
+ + {{ uriLabel }} + + + + +
+ +
+
+ + {{ "matchDetection" | i18n }} + + + + +
diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts index f712e3114e0..07bf7bef775 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DragDropModule } from "@angular/cdk/drag-drop"; import { NgForOf, NgIf } from "@angular/common"; import { Component, @@ -43,6 +44,7 @@ import { }, ], imports: [ + DragDropModule, FormFieldModule, ReactiveFormsModule, IconButtonModule, @@ -74,6 +76,12 @@ export class UriOptionComponent implements ControlValueAccessor { { label: this.i18nService.t("never"), value: UriMatchStrategy.Never }, ]; + /** + * Whether the option can be reordered. If false, the reorder button will be hidden. + */ + @Input({ required: true }) + canReorder: boolean; + /** * Whether the URI can be removed from the form. If false, the remove button will be hidden. */ @@ -101,6 +109,9 @@ export class UriOptionComponent implements ControlValueAccessor { */ @Input({ required: true }) index: number; + @Output() + onKeydown = new EventEmitter(); + /** * Emits when the remove button is clicked and URI should be removed from the form. */ @@ -132,6 +143,10 @@ export class UriOptionComponent implements ControlValueAccessor { private onChange: any = () => {}; private onTouched: any = () => {}; + protected handleKeydown(event: KeyboardEvent) { + this.onKeydown.emit(event); + } + constructor( private formBuilder: FormBuilder, private i18nService: I18nService, From 3b9be21fd7e37c9a42b7de722c974ed21f5f777b Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Mon, 10 Mar 2025 21:20:11 -0400 Subject: [PATCH 17/43] fix(auth-routing): [PM-19018] SSO TDE Routing Fix - Fixed routing logic. (#13778) * fix(auth-routing): [PM-19018] SSO TDE Routing Fix - Fixed routing logic. * PM-19018 - TwoFactorAuthTests - remove tests that are no longer applicable as 2FA comp isn't responsible for setting admin account recovery flag into state. * PM-19018 - LoginStrategyTests - add test for processing forcePasswordReset response --------- Co-authored-by: Jared Snider --- .../two-factor-auth.component.spec.ts | 59 +------------------ .../two-factor-auth.component.ts | 14 +---- .../login-strategies/login.strategy.spec.ts | 25 ++++++++ .../common/login-strategies/login.strategy.ts | 25 +++++--- .../login-strategies/sso-login.strategy.ts | 9 --- 5 files changed, 44 insertions(+), 88 deletions(-) diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index 46b27a5aa42..6b7fca47ad5 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -226,20 +226,6 @@ describe("TwoFactorAuthComponent", () => { }); }; - const testForceResetOnSuccessfulLogin = (reasonString: string) => { - it(`navigates to the component's defined forcePasswordResetRoute route when response.forcePasswordReset is ${reasonString}`, async () => { - // Act - await component.submit("testToken"); - - // expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith(["update-temp-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, - }); - }); - }; - describe("Standard 2FA scenarios", () => { describe("submit", () => { const token = "testToken"; @@ -311,26 +297,6 @@ describe("TwoFactorAuthComponent", () => { }); }); - describe("Force Master Password Reset scenarios", () => { - [ - ForceSetPasswordReason.AdminForcePasswordReset, - ForceSetPasswordReason.WeakMasterPassword, - ].forEach((forceResetPasswordReason) => { - const reasonString = ForceSetPasswordReason[forceResetPasswordReason]; - - beforeEach(() => { - // use standard user with MP because this test is not concerned with password reset. - selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); - - const authResult = new AuthResult(); - authResult.forcePasswordReset = forceResetPasswordReason; - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); - }); - - testForceResetOnSuccessfulLogin(reasonString); - }); - }); - it("navigates to the component's defined success route (vault is default) when the login is successful", async () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); @@ -407,29 +373,7 @@ describe("TwoFactorAuthComponent", () => { }); }); - describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => { - [ - ForceSetPasswordReason.AdminForcePasswordReset, - ForceSetPasswordReason.WeakMasterPassword, - ].forEach((forceResetPasswordReason) => { - const reasonString = ForceSetPasswordReason[forceResetPasswordReason]; - - beforeEach(() => { - // use standard user with MP because this test is not concerned with password reset. - selectedUserDecryptionOptions.next( - mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice, - ); - - const authResult = new AuthResult(); - authResult.forcePasswordReset = forceResetPasswordReason; - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); - }); - - testForceResetOnSuccessfulLogin(reasonString); - }); - }); - - describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => { + describe("Given Trusted Device Encryption is enabled and user doesn't need to set a MP", () => { let authResult; beforeEach(() => { selectedUserDecryptionOptions.next( @@ -437,7 +381,6 @@ describe("TwoFactorAuthComponent", () => { ); authResult = new AuthResult(); - authResult.forcePasswordReset = ForceSetPasswordReason.None; mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult); }); diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 296316198b0..c5e174484b0 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -396,11 +396,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { ); } - // note: this flow affects both TDE & standard users - if (this.isForcePasswordResetRequired(authResult)) { - return await this.handleForcePasswordReset(this.orgSsoIdentifier); - } - const userDecryptionOpts = await firstValueFrom( this.userDecryptionOptionsService.userDecryptionOptions$, ); @@ -415,6 +410,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { const requireSetPassword = !userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined; + // New users without a master password must set a master password before advancing. if (requireSetPassword || authResult.resetMasterPassword) { // Change implies going no password -> password in this case return await this.handleChangePasswordRequired(this.orgSsoIdentifier); @@ -524,14 +520,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { return forceResetReasons.includes(authResult.forcePasswordReset); } - private async handleForcePasswordReset(orgIdentifier: string | undefined) { - await this.router.navigate(["update-temp-password"], { - queryParams: { - identifier: orgIdentifier, - }, - }); - } - showContinueButton() { return ( this.selectedProviderType != null && diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index b4a1e6a77d9..290345a90c7 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -306,6 +306,31 @@ describe("LoginStrategy", () => { expect(result).toEqual(expected); }); + it("processes a forcePasswordReset response properly", async () => { + const tokenResponse = identityTokenResponseFactory(); + tokenResponse.forcePasswordReset = true; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + const result = await passwordLoginStrategy.logIn(credentials); + + const expected = new AuthResult(); + expected.userId = userId; + expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + expected.resetMasterPassword = false; + expected.twoFactorProviders = {} as Partial< + Record> + >; + expected.captchaSiteKey = ""; + expected.twoFactorProviders = null; + expect(result).toEqual(expected); + + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.AdminForcePasswordReset, + userId, + ); + }); + it("rejects login if CAPTCHA is required", async () => { // Sample CAPTCHA response const tokenResponse = new IdentityCaptchaResponse({ diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 89802c609c0..1d4c23d3bab 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -271,17 +271,24 @@ export abstract class LoginStrategy { } } - result.resetMasterPassword = response.resetMasterPassword; - - // Convert boolean to enum - if (response.forcePasswordReset) { - result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; - } - - // Must come before setting keys, user key needs email to update additional keys + // Must come before setting keys, user key needs email to update additional keys. const userId = await this.saveAccountInformation(response); result.userId = userId; + result.resetMasterPassword = response.resetMasterPassword; + + // Convert boolean to enum and set the state for the master password service to + // so we know when we reach the auth guard that we need to guide them properly to admin + // password reset. + if (response.forcePasswordReset) { + result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.AdminForcePasswordReset, + userId, + ); + } + if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation const userEmail = await this.tokenService.getEmail(); @@ -300,7 +307,9 @@ export abstract class LoginStrategy { // The keys comes from different sources depending on the login strategy protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise; + protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; + protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise; // Old accounts used master key for encryption. We are forcing migrations but only need to diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index ac00ff69a4d..f4eaa10c319 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -6,7 +6,6 @@ import { Jsonify } from "type-fest"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -108,14 +107,6 @@ export class SsoLoginStrategy extends LoginStrategy { const email = ssoAuthResult.email; const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken; - // Auth guard currently handles redirects for this. - if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - await this.masterPasswordService.setForceSetPasswordReason( - ssoAuthResult.forcePasswordReset, - ssoAuthResult.userId, - ); - } - this.cache.next({ ...this.cache.value, email, From 7e6f2fa79803bfd8231c44b609133d4f31b5eb89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Tue, 11 Mar 2025 09:03:28 +0100 Subject: [PATCH 18/43] Enable Basic Desktop Modal Support (#11484) Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Colton Hurst Co-authored-by: Andreas Coroiu --- .github/CODEOWNERS | 1 + apps/desktop/src/app/app-routing.module.ts | 5 + .../components/fido2placeholder.component.ts | 36 ++++++ apps/desktop/src/main.ts | 2 + apps/desktop/src/main/tray.main.ts | 43 +++++++- apps/desktop/src/main/window.main.ts | 103 +++++++++++++++--- .../src/platform/popup-modal-styles.ts | 52 +++++++++ .../services/desktop-settings.service.ts | 24 ++++ 8 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/app/components/fido2placeholder.component.ts create mode 100644 apps/desktop/src/platform/popup-modal-styles.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d23cfa58283..5ba84c1f195 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -119,6 +119,7 @@ apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev +apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 3a30629b444..36e267c9355 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -57,6 +57,7 @@ import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; +import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; import { SendComponent } from "./tools/send/send.component"; /** @@ -177,6 +178,10 @@ const routes: Routes = [ component: RemovePasswordComponent, canActivate: [authGuard], }, + { + path: "passkeys", + component: Fido2PlaceholderComponent, + }, { path: "", component: AnonLayoutWrapperComponent, diff --git a/apps/desktop/src/app/components/fido2placeholder.component.ts b/apps/desktop/src/app/components/fido2placeholder.component.ts new file mode 100644 index 00000000000..b3302d63241 --- /dev/null +++ b/apps/desktop/src/app/components/fido2placeholder.component.ts @@ -0,0 +1,36 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +@Component({ + standalone: true, + template: ` +
+

Select your passkey

+
+ +
+ `, +}) +export class Fido2PlaceholderComponent { + constructor( + private readonly desktopSettingsService: DesktopSettingsService, + private readonly router: Router, + ) {} + + async closeModal() { + await this.router.navigate(["/"]); + await this.desktopSettingsService.setInModalMode(false); + } +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 4e167f30ec8..e4894b159fe 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -284,6 +284,8 @@ export class Main { this.migrationRunner.run().then( async () => { await this.toggleHardwareAcceleration(); + // Reset modal mode to make sure main window is displayed correctly + await this.desktopSettingsService.resetInModalMode(); await this.windowMain.init(); await this.i18nService.init(); await this.messagingMain.init(); diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index 9fa7fe6143f..e63e2a00c85 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as path from "path"; +import * as url from "url"; import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron"; import { firstValueFrom } from "rxjs"; @@ -9,6 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BiometricStateService, BiometricsService } from "@bitwarden/key-management"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; +import { cleanUserAgent, isDev } from "../utils"; import { WindowMain } from "./window.main"; @@ -49,6 +51,11 @@ export class TrayMain { label: this.i18nService.t("showHide"), click: () => this.toggleWindow(), }, + { + visible: isDev(), + label: "Fake Popup", + click: () => this.fakePopup(), + }, { type: "separator" }, { label: this.i18nService.t("exit"), @@ -190,7 +197,7 @@ export class TrayMain { this.hideDock(); } } else { - this.windowMain.win.show(); + this.windowMain.show(); if (this.isDarwin()) { this.showDock(); } @@ -203,4 +210,38 @@ export class TrayMain { this.windowMain.win.close(); } } + + /** + * This method is used to test modal behavior during development and could be removed in the future. + * @returns + */ + private async fakePopup() { + if (this.windowMain.win == null || this.windowMain.win.isDestroyed()) { + await this.windowMain.createWindow("modal-app"); + return; + } + + // Restyle existing + const existingWin = this.windowMain.win; + + await this.desktopSettingsService.setInModalMode(true); + await existingWin.loadURL( + url.format({ + protocol: "file:", + //pathname: `${__dirname}/index.html`, + pathname: path.join(__dirname, "/index.html"), + slashes: true, + hash: "/passkeys", + query: { + redirectUrl: "/passkeys", + }, + }), + { + userAgent: cleanUserAgent(existingWin.webContents.userAgent), + }, + ); + existingWin.once("ready-to-show", () => { + existingWin.show(); + }); + } } diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 17f74b78d4c..ca154400ff5 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -5,7 +5,7 @@ import * as path from "path"; import * as url from "url"; import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; -import { firstValueFrom } from "rxjs"; +import { concatMap, firstValueFrom, pairwise } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -14,6 +14,7 @@ import { processisolations } from "@bitwarden/desktop-napi"; import { BiometricStateService } from "@bitwarden/key-management"; import { WindowState } from "../platform/models/domain/window-state"; +import { applyMainWindowStyles, applyPopupModalStyles } from "../platform/popup-modal-styles"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils"; @@ -77,6 +78,24 @@ export class WindowMain { } }); + this.desktopSettingsService.inModalMode$ + .pipe( + pairwise(), + concatMap(async ([lastValue, newValue]) => { + if (lastValue && !newValue) { + // Reset the window state to the main window state + applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); + // Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode. + this.win.hide(); + } else if (!lastValue && newValue) { + // Apply the popup modal styles + applyPopupModalStyles(this.win); + this.win.show(); + } + }), + ) + .subscribe(); + this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => { if (this.win == null) { return; @@ -182,7 +201,20 @@ export class WindowMain { }); } - async createWindow(): Promise { + /// Show the window with main window styles + show() { + if (this.win != null) { + applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); + this.win.show(); + } + } + + /** + * Creates the main window. The template argument is used to determine the styling of the window and what url will be loaded. + * When the template is "modal-app", the window will be styled as a modal and the passkeys page will be loaded. + * TODO: We might want to refactor the template argument to accomodate more target pages, e.g. ssh-agent. + */ + async createWindow(template: "full-app" | "modal-app" = "full-app"): Promise { this.windowStates[mainWindowSizeKey] = await this.getWindowState( this.defaultWidth, this.defaultHeight, @@ -216,6 +248,12 @@ export class WindowMain { }, }); + if (template === "modal-app") { + applyPopupModalStyles(this.win); + } else { + applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]); + } + this.win.webContents.on("dom-ready", () => { this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0; }); @@ -225,21 +263,41 @@ export class WindowMain { } // Show it later since it might need to be maximized. - this.win.show(); + // use once event to avoid flash on unstyled content. + this.win.once("ready-to-show", () => { + this.win.show(); + }); - // and load the index.html of the app. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.win.loadURL( - url.format({ - protocol: "file:", - pathname: path.join(__dirname, "/index.html"), - slashes: true, - }), - { - userAgent: cleanUserAgent(this.win.webContents.userAgent), - }, - ); + if (template === "full-app") { + // and load the index.html of the app. + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + void this.win.loadURL( + url.format({ + protocol: "file:", + pathname: path.join(__dirname, "/index.html"), + slashes: true, + }), + { + userAgent: cleanUserAgent(this.win.webContents.userAgent), + }, + ); + } else { + // we're in modal mode - load the passkeys page + await this.win.loadURL( + url.format({ + protocol: "file:", + pathname: path.join(__dirname, "/index.html"), + slashes: true, + hash: "/passkeys", + query: { + redirectUrl: "/passkeys", + }, + }), + { + userAgent: cleanUserAgent(this.win.webContents.userAgent), + }, + ); + } // Open the DevTools. if (isDev()) { @@ -336,6 +394,12 @@ export class WindowMain { return; } + const inModalMode = await firstValueFrom(this.desktopSettingsService.inModalMode$); + + if (inModalMode) { + return; + } + try { const bounds = win.getBounds(); @@ -346,9 +410,14 @@ export class WindowMain { } } - this.windowStates[configKey].isMaximized = win.isMaximized(); + // We treat fullscreen as maximized (would be even better to store isFullscreen as its own flag). + this.windowStates[configKey].isMaximized = win.isMaximized() || win.isFullScreen(); this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds; + // Maybe store these as well? + // win.isFocused(); + // win.isVisible(); + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { this.windowStates[configKey].x = bounds.x; this.windowStates[configKey].y = bounds.y; diff --git a/apps/desktop/src/platform/popup-modal-styles.ts b/apps/desktop/src/platform/popup-modal-styles.ts new file mode 100644 index 00000000000..9c3f06b34bf --- /dev/null +++ b/apps/desktop/src/platform/popup-modal-styles.ts @@ -0,0 +1,52 @@ +import { BrowserWindow } from "electron"; + +import { WindowState } from "./models/domain/window-state"; + +// change as needed, however limited by mainwindow minimum size +const popupWidth = 680; +const popupHeight = 500; + +export function applyPopupModalStyles(window: BrowserWindow) { + window.unmaximize(); + window.setSize(popupWidth, popupHeight); + window.center(); + window.setWindowButtonVisibility?.(false); + window.setMenuBarVisibility?.(false); + window.setResizable(false); + window.setAlwaysOnTop(true); + + // Adjusting from full screen is a bit more hassle + if (window.isFullScreen()) { + window.setFullScreen(false); + window.once("leave-full-screen", () => { + window.setSize(popupWidth, popupHeight); + window.center(); + }); + } +} + +export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) { + window.setMinimumSize(680, 500); + + // need to guard against null/undefined values + + if (existingWindowState?.width && existingWindowState?.height) { + window.setSize(existingWindowState.width, existingWindowState.height); + } + + if (existingWindowState?.x && existingWindowState?.y) { + window.setPosition(existingWindowState.x, existingWindowState.y); + } + + window.setWindowButtonVisibility?.(true); + window.setMenuBarVisibility?.(true); + window.setResizable(true); + window.setAlwaysOnTop(false); + + // We're currently not recovering the maximized state, mostly due to conflicts with hiding the window. + // window.setFullScreen(existingWindowState.isMaximized); + + // if (existingWindowState.isMaximized) { + // window.maximize(); + // } +} diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index f0d5d124de2..efac0cda252 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -75,6 +75,10 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition(DESKTOP_SETTINGS_DISK, " clearOn: [], // User setting, no need to clear }); +const IN_MODAL_MODE = new KeyDefinition(DESKTOP_SETTINGS_DISK, "inModalMode", { + deserializer: (b) => b, +}); + const PREVENT_SCREENSHOTS = new KeyDefinition( DESKTOP_SETTINGS_DISK, "preventScreenshots", @@ -170,6 +174,10 @@ export class DesktopSettingsService { */ minimizeOnCopy$ = this.minimizeOnCopyState.state$.pipe(map(Boolean)); + private readonly inModalModeState = this.stateProvider.getGlobal(IN_MODAL_MODE); + + inModalMode$ = this.inModalModeState.state$.pipe(map(Boolean)); + constructor(private stateProvider: StateProvider) { this.window$ = this.windowState.state$.pipe( map((window) => @@ -178,6 +186,14 @@ export class DesktopSettingsService { ); } + /** + * This is used to clear the setting on application start to make sure we don't end up + * stuck in modal mode if the application is force-closed in modal mode. + */ + async resetInModalMode() { + await this.inModalModeState.update(() => false); + } + async setHardwareAcceleration(enabled: boolean) { await this.hwState.update(() => enabled); } @@ -286,6 +302,14 @@ export class DesktopSettingsService { await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value); } + /** + * Sets the modal mode of the application. Setting this changes the windows-size and other properties. + * @param value `true` if the application is in modal mode, `false` if it is not. + */ + async setInModalMode(value: boolean) { + await this.inModalModeState.update(() => value); + } + /** * Sets the setting for whether or not the screenshot protection is enabled. * @param value `true` if the screenshot protection is enabled, `false` if it is not. From 5cd47ac90717a40346c057487a4e764f73a8fe7b Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:06:44 +0100 Subject: [PATCH 19/43] [PM-18243] Improve type safety in decryption (#12885) * Improve decrypt failure logging * Rename decryptcontext to decrypttrace * Improve docs * PM-16984: Improving type safety of decryption * Improving type safety of decryption --------- Co-authored-by: Bernd Schoolmann --- .../common/collections/models/collection.ts | 7 +-- .../src/platform/models/domain/domain-base.ts | 57 ++++++++++--------- .../src/platform/models/domain/enc-string.ts | 2 +- .../tools/send/models/domain/send-access.ts | 9 +-- .../src/tools/send/models/domain/send-file.ts | 8 +-- .../src/tools/send/models/domain/send-text.ts | 7 +-- .../src/tools/send/models/domain/send.ts | 10 +--- .../src/vault/models/domain/attachment.ts | 7 +-- libs/common/src/vault/models/domain/card.ts | 12 +--- libs/common/src/vault/models/domain/cipher.ts | 8 +-- .../vault/models/domain/fido2-credential.ts | 47 +++++++-------- libs/common/src/vault/models/domain/field.ts | 8 +-- libs/common/src/vault/models/domain/folder.ts | 8 +-- .../src/vault/models/domain/identity.ts | 43 +++++++------- .../src/vault/models/domain/login-uri.ts | 7 +-- libs/common/src/vault/models/domain/login.ts | 9 +-- .../src/vault/models/domain/password.ts | 7 +-- .../common/src/vault/models/domain/ssh-key.ts | 9 +-- 18 files changed, 111 insertions(+), 154 deletions(-) diff --git a/libs/admin-console/src/common/collections/models/collection.ts b/libs/admin-console/src/common/collections/models/collection.ts index f14ccb20141..5b6f1a6fb7a 100644 --- a/libs/admin-console/src/common/collections/models/collection.ts +++ b/libs/admin-console/src/common/collections/models/collection.ts @@ -39,11 +39,10 @@ export class Collection extends Domain { } decrypt(orgKey: OrgKey): Promise { - return this.decryptObj( + return this.decryptObj( + this, new CollectionView(this), - { - name: null, - }, + ["name"], this.organizationId, orgKey, ); diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index 5aa79946653..11d0193accc 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; @@ -15,6 +13,19 @@ export type DecryptedObject< TDecryptedKeys extends EncStringKeys, > = Record & Omit; +// extracts shared keys from the domain and view types +type EncryptableKeys = (keyof D & + ConditionalKeys) & + (keyof V & ConditionalKeys); + +type DomainEncryptableKeys = { + [key in ConditionalKeys]: EncString | null; +}; + +type ViewEncryptableKeys = { + [key in ConditionalKeys]: string | null; +}; + // https://contributing.bitwarden.com/architecture/clients/data-model#domain export default class Domain { protected buildDomainModel( @@ -37,6 +48,7 @@ export default class Domain { } } } + protected buildDataModel( domain: D, dataObj: any, @@ -58,31 +70,24 @@ export default class Domain { } } - protected async decryptObj( - viewModel: T, - map: any, - orgId: string, - key: SymmetricCryptoKey = null, + protected async decryptObj( + domain: DomainEncryptableKeys, + viewModel: ViewEncryptableKeys, + props: EncryptableKeys[], + orgId: string | null, + key: SymmetricCryptoKey | null = null, objectContext: string = "No Domain Context", - ): Promise { - const self: any = this; - - for (const prop in map) { - // eslint-disable-next-line - if (!map.hasOwnProperty(prop)) { - continue; - } - - const mapProp = map[prop] || prop; - if (self[mapProp]) { - (viewModel as any)[prop] = await self[mapProp].decrypt( + ): Promise { + for (const prop of props) { + viewModel[prop] = + (await domain[prop]?.decrypt( orgId, key, - `Property: ${prop}; ObjectContext: ${objectContext}`, - ); - } + `Property: ${prop as string}; ObjectContext: ${objectContext}`, + )) ?? null; } - return viewModel; + + return viewModel as V; } /** @@ -111,7 +116,7 @@ export default class Domain { const decryptedObjects = []; for (const prop of encryptedProperties) { - const value = (this as any)[prop] as EncString; + const value = this[prop] as EncString; const decrypted = await this.decryptProperty( prop, value, @@ -138,11 +143,9 @@ export default class Domain { encryptService: EncryptService, decryptTrace: string, ) { - let decrypted: string = null; + let decrypted: string | null = null; if (value) { decrypted = await value.decryptWithKey(key, encryptService, decryptTrace); - } else { - decrypted = null; } return { [propertyKey]: decrypted, diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index 360cb9bab46..4ea58a6809e 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -160,7 +160,7 @@ export class EncString implements Encrypted { async decrypt( orgId: string | null, - key: SymmetricCryptoKey = null, + key: SymmetricCryptoKey | null = null, context?: string, ): Promise { if (this.decryptedValue != null) { diff --git a/libs/common/src/tools/send/models/domain/send-access.ts b/libs/common/src/tools/send/models/domain/send-access.ts index dcc2d3ef426..588c4e84aa1 100644 --- a/libs/common/src/tools/send/models/domain/send-access.ts +++ b/libs/common/src/tools/send/models/domain/send-access.ts @@ -54,14 +54,7 @@ export class SendAccess extends Domain { async decrypt(key: SymmetricCryptoKey): Promise { const model = new SendAccessView(this); - await this.decryptObj( - model, - { - name: null, - }, - null, - key, - ); + await this.decryptObj(this, model, ["name"], null, key); switch (this.type) { case SendType.File: diff --git a/libs/common/src/tools/send/models/domain/send-file.ts b/libs/common/src/tools/send/models/domain/send-file.ts index 90e40f3959a..b8d0a265081 100644 --- a/libs/common/src/tools/send/models/domain/send-file.ts +++ b/libs/common/src/tools/send/models/domain/send-file.ts @@ -34,15 +34,13 @@ export class SendFile extends Domain { } async decrypt(key: SymmetricCryptoKey): Promise { - const view = await this.decryptObj( + return await this.decryptObj( + this, new SendFileView(this), - { - fileName: null, - }, + ["fileName"], null, key, ); - return view; } static fromJSON(obj: Jsonify) { diff --git a/libs/common/src/tools/send/models/domain/send-text.ts b/libs/common/src/tools/send/models/domain/send-text.ts index b17e3f769fb..df33e555896 100644 --- a/libs/common/src/tools/send/models/domain/send-text.ts +++ b/libs/common/src/tools/send/models/domain/send-text.ts @@ -30,11 +30,10 @@ export class SendText extends Domain { } decrypt(key: SymmetricCryptoKey): Promise { - return this.decryptObj( + return this.decryptObj( + this, new SendTextView(this), - { - text: null, - }, + ["text"], null, key, ); diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index c2390d439e7..f12a0010fab 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -87,15 +87,7 @@ export class Send extends Domain { // TODO: error? } - await this.decryptObj( - model, - { - name: null, - notes: null, - }, - null, - model.cryptoKey, - ); + await this.decryptObj(this, model, ["name", "notes"], null, model.cryptoKey); switch (this.type) { case SendType.File: diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 4eee0307746..16f3adbe307 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -43,11 +43,10 @@ export class Attachment extends Domain { context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - const view = await this.decryptObj( + const view = await this.decryptObj( + this, new AttachmentView(this), - { - fileName: null, - }, + ["fileName"], orgId, encKey, "DomainType: Attachment; " + context, diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index fccfe3f595b..3d73a8f527c 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -42,16 +42,10 @@ export class Card extends Domain { context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - return this.decryptObj( + return this.decryptObj( + this, new CardView(), - { - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, - }, + ["cardholderName", "brand", "number", "expMonth", "expYear", "code"], orgId, encKey, "DomainType: Card; " + context, diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 21538b87788..c08ec8a4ebc 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -154,12 +154,10 @@ export class Cipher extends Domain implements Decryptable { bypassValidation = false; } - await this.decryptObj( + await this.decryptObj( + this, model, - { - name: null, - notes: null, - }, + ["name", "notes"], this.organizationId, encKey, ); diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 9aa2c753d7c..8b0082892e4 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -52,41 +52,38 @@ export class Fido2Credential extends Domain { } async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - const view = await this.decryptObj( + const view = await this.decryptObj( + this, new Fido2CredentialView(), - { - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - userHandle: null, - userName: null, - rpName: null, - userDisplayName: null, - discoverable: null, - }, + [ + "credentialId", + "keyType", + "keyAlgorithm", + "keyCurve", + "keyValue", + "rpId", + "userHandle", + "userName", + "rpName", + "userDisplayName", + ], orgId, encKey, ); - const { counter } = await this.decryptObj( - { counter: "" }, + const { counter } = await this.decryptObj< + Fido2Credential, { - counter: null, - }, - orgId, - encKey, - ); + counter: string; + } + >(this, { counter: "" }, ["counter"], orgId, encKey); // Counter will end up as NaN if this fails view.counter = parseInt(counter); - const { discoverable } = await this.decryptObj( + const { discoverable } = await this.decryptObj( + this, { discoverable: "" }, - { - discoverable: null, - }, + ["discoverable"], orgId, encKey, ); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index f836184da6a..c0f08a38bcc 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -35,12 +35,10 @@ export class Field extends Domain { } decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - return this.decryptObj( + return this.decryptObj( + this, new FieldView(this), - { - name: null, - value: null, - }, + ["name", "value"], orgId, encKey, ); diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index 65018e3cf08..8749a92fb65 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -40,13 +40,7 @@ export class Folder extends Domain { } decrypt(): Promise { - return this.decryptObj( - new FolderView(this), - { - name: null, - }, - null, - ); + return this.decryptObj(this, new FolderView(this), ["name"], null); } async decryptWithKey( diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index 570e6c0b4d5..5d8c20ef2b3 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -66,28 +66,29 @@ export class Identity extends Domain { context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - return this.decryptObj( + return this.decryptObj( + this, new IdentityView(), - { - title: null, - firstName: null, - middleName: null, - lastName: null, - address1: null, - address2: null, - address3: null, - city: null, - state: null, - postalCode: null, - country: null, - company: null, - email: null, - phone: null, - ssn: null, - username: null, - passportNumber: null, - licenseNumber: null, - }, + [ + "title", + "firstName", + "middleName", + "lastName", + "address1", + "address2", + "address3", + "city", + "state", + "postalCode", + "country", + "company", + "email", + "phone", + "ssn", + "username", + "passportNumber", + "licenseNumber", + ], orgId, encKey, "DomainType: Identity; " + context, diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 36782a81502..883f8c9a616 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -38,11 +38,10 @@ export class LoginUri extends Domain { context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - return this.decryptObj( + return this.decryptObj( + this, new LoginUriView(this), - { - uri: null, - }, + ["uri"], orgId, encKey, context, diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index f9a85cd818e..b29b42bf3de 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -58,13 +58,10 @@ export class Login extends Domain { context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - const view = await this.decryptObj( + const view = await this.decryptObj( + this, new LoginView(this), - { - username: null, - password: null, - totp: null, - }, + ["username", "password", "totp"], orgId, encKey, `DomainType: Login; ${context}`, diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 48063f495f0..8573c224416 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -25,11 +25,10 @@ export class Password extends Domain { } decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - return this.decryptObj( + return this.decryptObj( + this, new PasswordHistoryView(this), - { - password: null, - }, + ["password"], orgId, encKey, "DomainType: PasswordHistory", diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index b4df172e543..f32a1a913ca 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -36,13 +36,10 @@ export class SshKey extends Domain { context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { - return this.decryptObj( + return this.decryptObj( + this, new SshKeyView(), - { - privateKey: null, - publicKey: null, - keyFingerprint: null, - }, + ["privateKey", "publicKey", "keyFingerprint"], orgId, encKey, "DomainType: SshKey; " + context, From 9683779dbf6f3ffde6860286f96c92abe63b9bd2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 11 Mar 2025 14:20:02 +0100 Subject: [PATCH 20/43] [PM-17984] Remove AES128CBC-HMAC encryption (#13304) * Remove AES128CBC-HMAC encryption * Increase test coverage --- .../crypto/abstractions/encrypt.service.ts | 1 - .../encrypt.service.implementation.ts | 19 ------ .../crypto/services/encrypt.service.spec.ts | 62 +++++++++---------- .../platform/enums/encryption-type.enum.ts | 7 +-- .../models/domain/enc-array-buffer.spec.ts | 39 ++++++------ .../models/domain/enc-array-buffer.ts | 1 - .../platform/models/domain/enc-string.spec.ts | 2 - .../src/platform/models/domain/enc-string.ts | 6 +- .../domain/symmetric-crypto-key.spec.ts | 15 ----- .../models/domain/symmetric-crypto-key.ts | 3 - 10 files changed, 50 insertions(+), 105 deletions(-) diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index 484327bcd27..f7f064f5251 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -36,7 +36,6 @@ export abstract class EncryptService { ): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; - abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; /** * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed * @param items The items to decrypt diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 8a001886837..d426340c277 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -78,8 +78,6 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No key provided for decryption."); } - key = this.resolveLegacyKey(key, encString); - // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality. if (key.macKey != null && encString?.mac == null) { this.logService.error( @@ -145,8 +143,6 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("Nothing provided for decryption."); } - key = this.resolveLegacyKey(key, encThing); - // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality. if (key.macKey != null && encThing.macBytes == null) { this.logService.error( @@ -298,19 +294,4 @@ export class EncryptServiceImplementation implements EncryptService { this.logService.error(msg); } } - - /** - * Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged - * @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt - */ - resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey { - if ( - encThing.encryptionType === EncryptionType.AesCbc128_HmacSha256_B64 && - key.encType === EncryptionType.AesCbc256_B64 - ) { - return new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); - } - - return key; - } } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index cff695f4829..3ce0d5883d2 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -325,6 +325,25 @@ describe("EncryptService", () => { }); }); + describe("decryptToUtf8", () => { + it("throws if no key is provided", () => { + return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow( + "No key provided for decryption.", + ); + }); + it("returns null if key is mac key but encstring has no mac", async () => { + const key = new SymmetricCryptoKey( + makeStaticByteArray(64, 0), + EncryptionType.AesCbc256_HmacSha256_B64, + ); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + + const actual = await encryptService.decryptToUtf8(encString, key); + expect(actual).toBeNull(); + expect(logService.error).toHaveBeenCalled(); + }); + }); + describe("rsa", () => { const data = makeStaticByteArray(10, 100); const encryptedData = makeStaticByteArray(10, 150); @@ -370,17 +389,16 @@ describe("EncryptService", () => { return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key"); }); - it.each([ - EncryptionType.AesCbc256_B64, - EncryptionType.AesCbc128_HmacSha256_B64, - EncryptionType.AesCbc256_HmacSha256_B64, - ])("throws if encryption type is %s", async (encType) => { - encString.encryptionType = encType; + it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])( + "throws if encryption type is %s", + async (encType) => { + encString.encryptionType = encType; - await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow( - "Invalid encryption type", - ); - }); + await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow( + "Invalid encryption type", + ); + }, + ); it("decrypts data with provided key", async () => { cryptoFunctionService.rsaDecrypt.mockResolvedValue(data); @@ -398,30 +416,6 @@ describe("EncryptService", () => { }); }); - describe("resolveLegacyKey", () => { - it("creates a legacy key if required", async () => { - const key = new SymmetricCryptoKey(makeStaticByteArray(32), EncryptionType.AesCbc256_B64); - const encString = mock(); - encString.encryptionType = EncryptionType.AesCbc128_HmacSha256_B64; - - const actual = encryptService.resolveLegacyKey(key, encString); - - const expected = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); - expect(actual).toEqual(expected); - }); - - it("does not create a legacy key if not required", async () => { - const encType = EncryptionType.AesCbc256_HmacSha256_B64; - const key = new SymmetricCryptoKey(makeStaticByteArray(64), encType); - const encString = mock(); - encString.encryptionType = encType; - - const actual = encryptService.resolveLegacyKey(key, encString); - - expect(actual).toEqual(key); - }); - }); - describe("hash", () => { it("hashes a string and returns b64", async () => { cryptoFunctionService.hash.mockResolvedValue(Uint8Array.from([1, 2, 3])); diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index a0ffe679279..fd484dc2fdf 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -1,6 +1,6 @@ export enum EncryptionType { AesCbc256_B64 = 0, - AesCbc128_HmacSha256_B64 = 1, + // Type 1 was the unused and removed AesCbc128_HmacSha256_B64 AesCbc256_HmacSha256_B64 = 2, Rsa2048_OaepSha256_B64 = 3, Rsa2048_OaepSha1_B64 = 4, @@ -17,12 +17,10 @@ export function encryptionTypeToString(encryptionType: EncryptionType): string { } /** The expected number of parts to a serialized EncString of the given encryption type. - * For example, an EncString of type AesCbc256_B64 will have 2 parts, and an EncString of type - * AesCbc128_HmacSha256_B64 will have 3 parts. + * For example, an EncString of type AesCbc256_B64 will have 2 parts * * Example of annotated serialized EncStrings: * 0.iv|data - * 1.iv|data|mac * 2.iv|data|mac * 3.data * 4.data @@ -33,7 +31,6 @@ export function encryptionTypeToString(encryptionType: EncryptionType): string { */ export const EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE = { [EncryptionType.AesCbc256_B64]: 2, - [EncryptionType.AesCbc128_HmacSha256_B64]: 3, [EncryptionType.AesCbc256_HmacSha256_B64]: 3, [EncryptionType.Rsa2048_OaepSha256_B64]: 1, [EncryptionType.Rsa2048_OaepSha1_B64]: 1, diff --git a/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts b/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts index 45a45ffe087..de0fea58e36 100644 --- a/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts +++ b/libs/common/src/platform/models/domain/enc-array-buffer.spec.ts @@ -5,28 +5,28 @@ import { EncArrayBuffer } from "./enc-array-buffer"; describe("encArrayBuffer", () => { describe("parses the buffer", () => { - test.each([ - [EncryptionType.AesCbc128_HmacSha256_B64, "AesCbc128_HmacSha256_B64"], - [EncryptionType.AesCbc256_HmacSha256_B64, "AesCbc256_HmacSha256_B64"], - ])("with %c%s", (encType: EncryptionType) => { - const iv = makeStaticByteArray(16, 10); - const mac = makeStaticByteArray(32, 20); - // We use the minimum data length of 1 to test the boundary of valid lengths - const data = makeStaticByteArray(1, 100); + test.each([[EncryptionType.AesCbc256_HmacSha256_B64, "AesCbc256_HmacSha256_B64"]])( + "with %c%s", + (encType: EncryptionType) => { + const iv = makeStaticByteArray(16, 10); + const mac = makeStaticByteArray(32, 20); + // We use the minimum data length of 1 to test the boundary of valid lengths + const data = makeStaticByteArray(1, 100); - const array = new Uint8Array(1 + iv.byteLength + mac.byteLength + data.byteLength); - array.set([encType]); - array.set(iv, 1); - array.set(mac, 1 + iv.byteLength); - array.set(data, 1 + iv.byteLength + mac.byteLength); + const array = new Uint8Array(1 + iv.byteLength + mac.byteLength + data.byteLength); + array.set([encType]); + array.set(iv, 1); + array.set(mac, 1 + iv.byteLength); + array.set(data, 1 + iv.byteLength + mac.byteLength); - const actual = new EncArrayBuffer(array); + const actual = new EncArrayBuffer(array); - expect(actual.encryptionType).toEqual(encType); - expect(actual.ivBytes).toEqualBuffer(iv); - expect(actual.macBytes).toEqualBuffer(mac); - expect(actual.dataBytes).toEqualBuffer(data); - }); + expect(actual.encryptionType).toEqual(encType); + expect(actual.ivBytes).toEqualBuffer(iv); + expect(actual.macBytes).toEqualBuffer(mac); + expect(actual.dataBytes).toEqualBuffer(data); + }, + ); it("with AesCbc256_B64", () => { const encType = EncryptionType.AesCbc256_B64; @@ -50,7 +50,6 @@ describe("encArrayBuffer", () => { describe("throws if the buffer has an invalid length", () => { test.each([ - [EncryptionType.AesCbc128_HmacSha256_B64, 50, "AesCbc128_HmacSha256_B64"], [EncryptionType.AesCbc256_HmacSha256_B64, 50, "AesCbc256_HmacSha256_B64"], [EncryptionType.AesCbc256_B64, 18, "AesCbc256_B64"], ])("with %c%c%s", (encType: EncryptionType, minLength: number) => { diff --git a/libs/common/src/platform/models/domain/enc-array-buffer.ts b/libs/common/src/platform/models/domain/enc-array-buffer.ts index 305504f57b7..8b69cb347ba 100644 --- a/libs/common/src/platform/models/domain/enc-array-buffer.ts +++ b/libs/common/src/platform/models/domain/enc-array-buffer.ts @@ -20,7 +20,6 @@ export class EncArrayBuffer implements Encrypted { const encType = encBytes[0]; switch (encType) { - case EncryptionType.AesCbc128_HmacSha256_B64: case EncryptionType.AesCbc256_HmacSha256_B64: { const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH + MIN_DATA_LENGTH; if (encBytes.length < minimumLength) { diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 3b2586fc22f..c3f257d442a 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -60,9 +60,7 @@ describe("EncString", () => { const cases = [ "aXY=|Y3Q=", // AesCbc256_B64 w/out header - "aXY=|Y3Q=|cnNhQ3Q=", // AesCbc128_HmacSha256_B64 w/out header "0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc256_B64 with header - "1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc128_HmacSha256_B64 "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc256_HmacSha256_B64 "3.QmFzZTY0UGFydA==", // Rsa2048_OaepSha256_B64 "4.QmFzZTY0UGFydA==", // Rsa2048_OaepSha1_B64 diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index 4ea58a6809e..b0b03e0fb3c 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -89,7 +89,6 @@ export class EncString implements Encrypted { } switch (encType) { - case EncryptionType.AesCbc128_HmacSha256_B64: case EncryptionType.AesCbc256_HmacSha256_B64: this.iv = encPieces[0]; this.data = encPieces[1]; @@ -132,10 +131,7 @@ export class EncString implements Encrypted { } } else { encPieces = encryptedString.split("|"); - encType = - encPieces.length === 3 - ? EncryptionType.AesCbc128_HmacSha256_B64 - : EncryptionType.AesCbc256_B64; + encType = EncryptionType.AesCbc256_B64; } return { diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts index e4c43264eaf..58c902ebab6 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts @@ -27,21 +27,6 @@ describe("SymmetricCryptoKey", () => { }); }); - it("AesCbc128_HmacSha256_B64", () => { - const key = makeStaticByteArray(32); - const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64); - - expect(cryptoKey).toEqual({ - encKey: key.slice(0, 16), - encKeyB64: "AAECAwQFBgcICQoLDA0ODw==", - encType: 1, - key: key, - keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - macKey: key.slice(16, 32), - macKeyB64: "EBESExQVFhcYGRobHB0eHw==", - }); - }); - it("AesCbc256_HmacSha256_B64", () => { const key = makeStaticByteArray(64); const cryptoKey = new SymmetricCryptoKey(key); diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index eab4c7b2114..372b869fd9c 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -38,9 +38,6 @@ export class SymmetricCryptoKey { if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) { this.encKey = key; this.macKey = null; - } else if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && key.byteLength === 32) { - this.encKey = key.slice(0, 16); - this.macKey = key.slice(16, 32); } else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) { this.encKey = key.slice(0, 32); this.macKey = key.slice(32, 64); From ef06e9f03c292c7c4aaead7a9777fc9f5d7f1777 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:42:10 +0100 Subject: [PATCH 21/43] [PM-15442]Upgrade modal additional instances (#13557) * display inline information error message * Add collection service * Refactor the code * Add a feature flag to the change * Add the modal pop for free org * Use custom error messages passed from the validator * Add the js document * Merge changes in main * Add the changes after file movement * remove these floating promises * Adding unit test and seprating the validation * fix the unit test request * Remove the conditional statment in test --- .../collection-dialog.component.html | 2 +- .../collection-dialog.component.ts | 62 +++++++++++++++ ...ree-org-collection-limit.validator.spec.ts | 78 +++++++++++++++++++ .../free-org-collection-limit.validator.ts | 44 +++++++++++ .../vault-header/vault-header.component.ts | 76 +++++++++++++++++- apps/web/src/locales/en/messages.json | 3 + 6 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html index 61fc290f6fe..9188ba5ab96 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html @@ -124,7 +124,7 @@ buttonType="primary" [disabled]="loading || dialogReadonly" > - {{ "save" | i18n }} + {{ buttonDisplayName | i18n }} - - - - - diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts deleted file mode 100644 index 0c4510204d1..00000000000 --- a/apps/browser/src/auth/popup/home.component.ts +++ /dev/null @@ -1,130 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; - -import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; - -import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; - -@Component({ - selector: "app-home", - templateUrl: "home.component.html", -}) -export class HomeComponent implements OnInit, OnDestroy { - @ViewChild(EnvironmentSelectorComponent, { static: true }) - environmentSelector!: EnvironmentSelectorComponent; - private destroyed$: Subject = new Subject(); - - loginInitiated = false; - formGroup = this.formBuilder.group({ - email: ["", [Validators.required, Validators.email]], - rememberEmail: [false], - }); - - constructor( - protected platformUtilsService: PlatformUtilsService, - private formBuilder: FormBuilder, - private router: Router, - private i18nService: I18nService, - private loginEmailService: LoginEmailServiceAbstraction, - private accountSwitcherService: AccountSwitcherService, - private toastService: ToastService, - private configService: ConfigService, - private route: ActivatedRoute, - ) {} - - async ngOnInit(): Promise { - this.listenForUnauthUiRefreshFlagChanges(); - - const email = await firstValueFrom(this.loginEmailService.loginEmail$); - const rememberEmail = this.loginEmailService.getRememberEmail(); - - if (email != null) { - this.formGroup.patchValue({ email, rememberEmail }); - } else { - const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); - - if (storedEmail != null) { - this.formGroup.patchValue({ email: storedEmail, rememberEmail: true }); - } - } - - this.environmentSelector.onOpenSelfHostedSettings - .pipe( - switchMap(async () => { - await this.setLoginEmailValues(); - await this.router.navigate(["environment"]); - }), - takeUntil(this.destroyed$), - ) - .subscribe(); - } - - ngOnDestroy(): void { - this.destroyed$.next(); - this.destroyed$.complete(); - } - - private listenForUnauthUiRefreshFlagChanges() { - this.configService - .getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh) - .pipe( - tap(async (flag) => { - // If the flag is turned ON, we must force a reload to ensure the correct UI is shown - if (flag) { - const qParams = await firstValueFrom(this.route.queryParams); - - const uniqueQueryParams = { - ...qParams, - // adding a unique timestamp to the query params to force a reload - t: new Date().getTime().toString(), - }; - - await this.router.navigate(["/login"], { - queryParams: uniqueQueryParams, - }); - } - }), - takeUntil(this.destroyed$), - ) - .subscribe(); - } - - get availableAccounts$() { - return this.accountSwitcherService.availableAccounts$; - } - - async submit() { - this.formGroup.markAllAsTouched(); - - if (this.formGroup.invalid) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccured"), - message: this.i18nService.t("invalidEmail"), - }); - return; - } - - await this.setLoginEmailValues(); - await this.router.navigate(["login"], { - queryParams: { email: this.formGroup.controls.email.value }, - }); - } - - async setLoginEmailValues() { - // Note: Browser saves email settings here instead of the login component - this.loginEmailService.setRememberEmail(this.formGroup.controls.rememberEmail.value); - await this.loginEmailService.setLoginEmail(this.formGroup.controls.email.value); - await this.loginEmailService.saveEmailSettings(); - } -} diff --git a/apps/browser/src/auth/popup/login-v1.component.html b/apps/browser/src/auth/popup/login-v1.component.html deleted file mode 100644 index 145a9cbc754..00000000000 --- a/apps/browser/src/auth/popup/login-v1.component.html +++ /dev/null @@ -1,81 +0,0 @@ -
-
-

- {{ "logIn" | i18n }} -

-
-
-
-
-
-
- - - - -
-
- -
-
-
- -
-
- -
- -
-
diff --git a/apps/browser/src/auth/popup/login-v1.component.ts b/apps/browser/src/auth/popup/login-v1.component.ts deleted file mode 100644 index b2c52f248c6..00000000000 --- a/apps/browser/src/auth/popup/login-v1.component.ts +++ /dev/null @@ -1,142 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; -import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -@Component({ - selector: "app-login", - templateUrl: "login-v1.component.html", -}) -export class LoginComponentV1 extends BaseLoginComponent implements OnInit { - constructor( - devicesApiService: DevicesApiServiceAbstraction, - appIdService: AppIdService, - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, - protected stateService: StateService, - protected environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected cryptoFunctionService: CryptoFunctionService, - syncService: SyncService, - logService: LogService, - ngZone: NgZone, - formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService, - route: ActivatedRoute, - loginEmailService: LoginEmailServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - toastService: ToastService, - ) { - super( - devicesApiService, - appIdService, - loginStrategyService, - router, - platformUtilsService, - i18nService, - stateService, - environmentService, - passwordGenerationService, - cryptoFunctionService, - logService, - ngZone, - formBuilder, - formValidationErrorService, - route, - loginEmailService, - ssoLoginService, - toastService, - ); - this.onSuccessfulLogin = async () => { - await syncService.fullSync(true); - }; - this.successRoute = "/tabs/vault"; - } - - async ngOnInit(): Promise { - await super.ngOnInit(); - await this.validateEmail(); - } - - settings() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["environment"]); - } - - async launchSsoBrowser() { - // Save off email for SSO - await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - - // Generate necessary sso params - const passwordOptions: any = { - type: "password", - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - - const state = - (await this.passwordGenerationService.generatePassword(passwordOptions)) + - ":clientId=browser"; - const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256"); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - - await this.ssoLoginService.setCodeVerifier(codeVerifier); - await this.ssoLoginService.setSsoState(state); - - const env = await firstValueFrom(this.environmentService.environment$); - let url = env.getWebVaultUrl(); - if (url == null) { - url = "https://vault.bitwarden.com"; - } - - const redirectUri = url + "/sso-connector.html"; - - // Launch browser - this.platformUtilsService.launchUri( - url + - "/#/sso?clientId=browser" + - "&redirectUri=" + - encodeURIComponent(redirectUri) + - "&state=" + - state + - "&codeChallenge=" + - codeChallenge + - "&email=" + - encodeURIComponent(this.formGroup.controls.email.value), - ); - } - - async saveEmailSettings() { - // values should be saved on home component - return; - } -} diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index a190cb134ac..13aeb25b998 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -22,9 +22,7 @@ import { CurrentAccountComponent } from "../auth/popup/account-switching/current import { EnvironmentComponent } from "../auth/popup/environment.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { HintComponent } from "../auth/popup/hint.component"; -import { HomeComponent } from "../auth/popup/home.component"; import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "../auth/popup/login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; @@ -98,9 +96,7 @@ import "../platform/popup/locales"; ColorPasswordCountPipe, EnvironmentComponent, HintComponent, - HomeComponent, LoginViaAuthRequestComponentV1, - LoginComponentV1, LoginDecryptionOptionsComponentV1, SetPasswordComponent, SsoComponentV1, diff --git a/apps/desktop/src/auth/login/login-v1.component.html b/apps/desktop/src/auth/login/login-v1.component.html deleted file mode 100644 index aae4cec239d..00000000000 --- a/apps/desktop/src/auth/login/login-v1.component.html +++ /dev/null @@ -1,157 +0,0 @@ -
-
-
- - Bitwarden - -

{{ "loginOrCreateNewAccount" | i18n }}

- - -
-
-
- - -
-
- - -
-
- -
-
-
- -
-
-
-

{{ "newAroundHere" | i18n }}

- -
-
- - -
-
-
-
- - -
-
- -
-
-
-
-
-
- -
- -
-
-
-
-
- -
-
- -
-
- -
-
-
- -
-

{{ "loggingInAs" | i18n }} {{ loggedEmail }}

- {{ "notYou" | i18n }} -
-
-
-
-
-
- diff --git a/apps/desktop/src/auth/login/login-v1.component.ts b/apps/desktop/src/auth/login/login-v1.component.ts deleted file mode 100644 index ff8688353be..00000000000 --- a/apps/desktop/src/auth/login/login-v1.component.ts +++ /dev/null @@ -1,266 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil, tap } from "rxjs"; - -import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; -import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -import { EnvironmentComponent } from "../environment.component"; - -const BroadcasterSubscriptionId = "LoginComponent"; - -@Component({ - selector: "app-login", - templateUrl: "login-v1.component.html", -}) -export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDestroy { - @ViewChild("environment", { read: ViewContainerRef, static: true }) - environmentModal: ViewContainerRef; - - protected componentDestroyed$: Subject = new Subject(); - webVaultHostname = ""; - - showingModal = false; - - private deferFocus: boolean = null; - - get loggedEmail() { - return this.formGroup.value.email; - } - - constructor( - devicesApiService: DevicesApiServiceAbstraction, - appIdService: AppIdService, - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - i18nService: I18nService, - syncService: SyncService, - private modalService: ModalService, - platformUtilsService: PlatformUtilsService, - stateService: StateService, - environmentService: EnvironmentService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - cryptoFunctionService: CryptoFunctionService, - private broadcasterService: BroadcasterService, - ngZone: NgZone, - private messagingService: MessagingService, - logService: LogService, - formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService, - route: ActivatedRoute, - loginEmailService: LoginEmailServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - toastService: ToastService, - private configService: ConfigService, - ) { - super( - devicesApiService, - appIdService, - loginStrategyService, - router, - platformUtilsService, - i18nService, - stateService, - environmentService, - passwordGenerationService, - cryptoFunctionService, - logService, - ngZone, - formBuilder, - formValidationErrorService, - route, - loginEmailService, - ssoLoginService, - toastService, - ); - this.onSuccessfulLogin = () => { - return syncService.fullSync(true); - }; - } - - async ngOnInit() { - this.listenForUnauthUiRefreshFlagChanges(); - - await super.ngOnInit(); - await this.getLoginWithDevice(this.loggedEmail); - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(() => { - switch (message.command) { - case "windowHidden": - this.onWindowHidden(); - break; - case "windowIsFocused": - if (this.deferFocus === null) { - this.deferFocus = !message.windowIsFocused; - if (!this.deferFocus) { - this.focusInput(); - } - } else if (this.deferFocus && message.windowIsFocused) { - this.focusInput(); - this.deferFocus = false; - } - break; - default: - } - }); - }); - this.messagingService.send("getWindowIsFocused"); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.componentDestroyed$.next(); - this.componentDestroyed$.complete(); - } - - private listenForUnauthUiRefreshFlagChanges() { - this.configService - .getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh) - .pipe( - tap(async (flag) => { - if (flag) { - const qParams = await firstValueFrom(this.route.queryParams); - - const uniqueQueryParams = { - ...qParams, - // adding a unique timestamp to the query params to force a reload - t: new Date().getTime().toString(), - }; - - await this.router.navigate(["/"], { - queryParams: uniqueQueryParams, - }); - } - }), - takeUntil(this.componentDestroyed$), - ) - .subscribe(); - } - - async settings() { - const [modal, childComponent] = await this.modalService.openViewRef( - EnvironmentComponent, - this.environmentModal, - ); - - modal.onShown.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => { - this.showingModal = true; - }); - - modal.onClosed.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => { - this.showingModal = false; - }); - - // eslint-disable-next-line rxjs/no-async-subscribe - childComponent.onSaved.pipe(takeUntil(this.componentDestroyed$)).subscribe(async () => { - modal.close(); - await this.getLoginWithDevice(this.loggedEmail); - }); - } - - onWindowHidden() { - this.showPassword = false; - } - - async continue() { - await super.validateEmail(); - if (!this.formGroup.controls.email.valid) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccured"), - message: this.i18nService.t("invalidEmail"), - }); - return; - } - this.focusInput(); - } - - async submit() { - if (!this.validatedEmail) { - return; - } - - await super.submit(); - if (this.captchaSiteKey) { - const content = document.getElementById("content") as HTMLDivElement; - content.setAttribute("style", "width:335px"); - } - } - - private focusInput() { - const email = this.loggedEmail; - document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus(); - } - - async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { - if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) { - return super.launchSsoBrowser(clientId, ssoRedirectUri); - } - const email = this.formGroup.controls.email.value; - - // Save off email for SSO - await this.ssoLoginService.setSsoEmail(email); - - // Generate necessary sso params - const passwordOptions: any = { - type: "password", - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - const state = await this.passwordGenerationService.generatePassword(passwordOptions); - const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - - // Save sso params - await this.ssoLoginService.setSsoState(state); - await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier); - - try { - await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state, email); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (err) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("ssoError"), - ); - } - } - - /** - * Force the validatedEmail flag to false, which will show the login page. - */ - invalidateEmail() { - this.validatedEmail = false; - } -} diff --git a/apps/desktop/src/auth/login/login.module.ts b/apps/desktop/src/auth/login/login.module.ts index 427cbcb2069..8cd1bdb9212 100644 --- a/apps/desktop/src/auth/login/login.module.ts +++ b/apps/desktop/src/auth/login/login.module.ts @@ -6,17 +6,15 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { SharedModule } from "../../app/shared/shared.module"; import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "./login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component"; @NgModule({ imports: [SharedModule, RouterModule], declarations: [ - LoginComponentV1, LoginViaAuthRequestComponentV1, EnvironmentSelectorComponent, LoginDecryptionOptionsComponentV1, ], - exports: [LoginComponentV1, LoginViaAuthRequestComponentV1], + exports: [LoginViaAuthRequestComponentV1], }) export class LoginModule {} diff --git a/apps/web/src/app/auth/login/login-v1.component.html b/apps/web/src/app/auth/login/login-v1.component.html deleted file mode 100644 index b41e55a03b0..00000000000 --- a/apps/web/src/app/auth/login/login-v1.component.html +++ /dev/null @@ -1,129 +0,0 @@ -
- -
- - {{ "emailAddress" | i18n }} - - -
- -
- - - {{ "rememberEmail" | i18n }} - -
- -
- -
- -
-

{{ "or" | i18n }}

- - - {{ "logInWithPasskey" | i18n }} - -
- -
- -

- {{ "newAroundHere" | i18n }} - - - {{ "createAccount" | i18n }} - -

-
- -
-
- - {{ "masterPass" | i18n }} - - - - {{ "getMasterPasswordHint" | i18n }} -
- -
- -
- -
- -
- -
- -
- - - -
- -
-

{{ "loggingInAs" | i18n }} {{ loggedEmail }}

- {{ "notYou" | i18n }} -
-
-
diff --git a/apps/web/src/app/auth/login/login-v1.component.ts b/apps/web/src/app/auth/login/login-v1.component.ts deleted file mode 100644 index 247aee4828c..00000000000 --- a/apps/web/src/app/auth/login/login-v1.component.ts +++ /dev/null @@ -1,224 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { takeUntil } from "rxjs"; -import { first } from "rxjs/operators"; - -import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; -import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -import { RouterService } from "../../core"; -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../organization-invite/organization-invite"; - -@Component({ - selector: "app-login", - templateUrl: "login-v1.component.html", -}) -export class LoginComponentV1 extends BaseLoginComponent implements OnInit { - showResetPasswordAutoEnrollWarning = false; - enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; - policies: Policy[]; - - constructor( - private acceptOrganizationInviteService: AcceptOrganizationInviteService, - devicesApiService: DevicesApiServiceAbstraction, - appIdService: AppIdService, - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - i18nService: I18nService, - route: ActivatedRoute, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - private passwordStrengthService: PasswordStrengthServiceAbstraction, - cryptoFunctionService: CryptoFunctionService, - private policyApiService: PolicyApiServiceAbstraction, - private policyService: InternalPolicyService, - logService: LogService, - ngZone: NgZone, - protected stateService: StateService, - private routerService: RouterService, - formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService, - loginEmailService: LoginEmailServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - toastService: ToastService, - ) { - super( - devicesApiService, - appIdService, - loginStrategyService, - router, - platformUtilsService, - i18nService, - stateService, - environmentService, - passwordGenerationService, - cryptoFunctionService, - logService, - ngZone, - formBuilder, - formValidationErrorService, - route, - loginEmailService, - ssoLoginService, - toastService, - ); - this.onSuccessfulLoginNavigate = this.goAfterLogIn; - } - - submitForm = async (showToast = true) => { - return await this.submitFormHelper(showToast); - }; - - private async submitFormHelper(showToast: boolean) { - await super.submit(showToast); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - // If there is a query parameter called 'org', set previousUrl to `/create-organization?org=paramValue` - if (qParams.org != null) { - const route = this.router.createUrlTree(["create-organization"], { - queryParams: { plan: qParams.org }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - - /** - * If there is a query parameter called 'sponsorshipToken', that means they are coming - * from an email for sponsoring a families organization. If so, then set the prevousUrl - * to `/setup/families-for-enterprise?token=paramValue` - */ - if (qParams.sponsorshipToken != null) { - const route = this.router.createUrlTree(["setup/families-for-enterprise"], { - queryParams: { token: qParams.sponsorshipToken }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - - await super.ngOnInit(); - }); - - // If there's an existing org invite, use it to get the password policies - const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite(); - if (orgInvite != null) { - await this.initPasswordPolicies(orgInvite); - } - } - - async goAfterLogIn(userId: UserId) { - const masterPassword = this.formGroup.value.masterPassword; - - // Check master password against policy - if (this.enforcedPasswordPolicyOptions != null) { - const strengthResult = this.passwordStrengthService.getPasswordStrength( - masterPassword, - this.formGroup.value.email, - ); - const masterPasswordScore = strengthResult == null ? null : strengthResult.score; - - // If invalid, save policies and require update - if ( - !this.policyService.evaluateMasterPassword( - masterPasswordScore, - masterPassword, - this.enforcedPasswordPolicyOptions, - ) - ) { - const policiesData: { [id: string]: PolicyData } = {}; - this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p))); - await this.policyService.replace(policiesData, userId); - await this.router.navigate(["update-password"]); - return; - } - } - - this.loginEmailService.clearValues(); - await this.router.navigate([this.successRoute]); - } - - async goToHint() { - await this.saveEmailSettings(); - await this.router.navigateByUrl("/hint"); - } - - async goToRegister() { - if (this.emailFormControl.valid) { - await this.router.navigate(["/signup"], { - queryParams: { email: this.emailFormControl.value }, - }); - return; - } - - await this.router.navigate(["/signup"]); - } - - protected override async handleMigrateEncryptionKey(result: AuthResult): Promise { - if (!result.requiresEncryptionKeyMigration) { - return false; - } - await this.router.navigate(["migrate-legacy-encryption"]); - return true; - } - - private async initPasswordPolicies(invite: OrganizationInvite): Promise { - try { - this.policies = await this.policyApiService.getPoliciesByToken( - invite.organizationId, - invite.token, - invite.email, - invite.organizationUserId, - ); - } catch (e) { - this.logService.error(e); - } - - if (this.policies == null) { - return; - } - - const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions( - this.policies, - invite.organizationId, - ); - - // Set to true if policy enabled and auto-enroll enabled - this.showResetPasswordAutoEnrollWarning = - resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; - - this.policyService - .masterPasswordPolicyOptions$(this.policies) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPasswordPolicyOptions) => { - this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions; - }); - } -} diff --git a/apps/web/src/app/auth/login/login.module.ts b/apps/web/src/app/auth/login/login.module.ts index a33a6b8a5a8..60c20eb4b7b 100644 --- a/apps/web/src/app/auth/login/login.module.ts +++ b/apps/web/src/app/auth/login/login.module.ts @@ -5,20 +5,17 @@ import { CheckboxModule } from "@bitwarden/components"; import { SharedModule } from "../../../app/shared"; import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "./login-v1.component"; import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component"; import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component"; @NgModule({ imports: [SharedModule, CheckboxModule], declarations: [ - LoginComponentV1, LoginViaAuthRequestComponentV1, LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent, ], exports: [ - LoginComponentV1, LoginViaAuthRequestComponentV1, LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent, diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts index c984b7f0cba..16a249dda97 100644 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ b/libs/angular/src/auth/components/environment-selector.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core"; @@ -7,8 +5,6 @@ import { ActivatedRoute } from "@angular/router"; import { Observable, map, Subject, takeUntil } from "rxjs"; import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, Region, @@ -88,7 +84,6 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { protected environmentService: EnvironmentService, private route: ActivatedRoute, private dialogService: DialogService, - private configService: ConfigService, private toastService: ToastService, private i18nService: I18nService, ) {} @@ -113,24 +108,18 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { } /** - * Opens the self-hosted settings dialog. - * - * If the `UnauthenticatedExtensionUIRefresh` feature flag is enabled, - * the self-hosted settings dialog is opened directly. Otherwise, the - * `onOpenSelfHostedSettings` event is emitted. + * Opens the self-hosted settings dialog when the self-hosted option is selected. */ - if (option === Region.SelfHosted) { - if (await this.configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh)) { - if (await SelfHostedEnvConfigDialogComponent.open(this.dialogService)) { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("environmentSaved"), - }); - } - } else { - this.onOpenSelfHostedSettings.emit(); - } + if ( + option === Region.SelfHosted && + (await SelfHostedEnvConfigDialogComponent.open(this.dialogService)) + ) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("environmentSaved"), + }); + return; } diff --git a/libs/angular/src/auth/components/login-v1.component.ts b/libs/angular/src/auth/components/login-v1.component.ts deleted file mode 100644 index 26903716edf..00000000000 --- a/libs/angular/src/auth/components/login-v1.component.ts +++ /dev/null @@ -1,401 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router"; -import { Subject, firstValueFrom, of } from "rxjs"; -import { switchMap, take, takeUntil } from "rxjs/operators"; - -import { - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, - PasswordLoginCredentials, -} from "@bitwarden/auth/common"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -import { - AllValidationErrors, - FormValidationErrorsService, -} from "../../platform/abstractions/form-validation-errors.service"; - -import { CaptchaProtectedComponent } from "./captcha-protected.component"; - -@Directive() -export class LoginComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy { - @ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef; - - showPassword = false; - formPromise: Promise; - - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: (userId: UserId) => Promise; - onSuccessfulLoginTwoFactorNavigate: () => Promise; - onSuccessfulLoginForceResetNavigate: () => Promise; - - showLoginWithDevice: boolean; - validatedEmail = false; - paramEmailSet = false; - - get emailFormControl() { - return this.formGroup.controls.email; - } - - formGroup = this.formBuilder.nonNullable.group({ - email: ["", [Validators.required, Validators.email]], - masterPassword: [ - "", - [Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)], - ], - rememberEmail: [false], - }); - - protected twoFactorRoute = "2fa"; - protected successRoute = "vault"; - protected forcePasswordResetRoute = "update-temp-password"; - - protected destroy$ = new Subject(); - - get loggedEmail() { - return this.formGroup.controls.email.value; - } - - constructor( - protected devicesApiService: DevicesApiServiceAbstraction, - protected appIdService: AppIdService, - protected loginStrategyService: LoginStrategyServiceAbstraction, - protected router: Router, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - protected stateService: StateService, - environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected cryptoFunctionService: CryptoFunctionService, - protected logService: LogService, - protected ngZone: NgZone, - protected formBuilder: FormBuilder, - protected formValidationErrorService: FormValidationErrorsService, - protected route: ActivatedRoute, - protected loginEmailService: LoginEmailServiceAbstraction, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected toastService: ToastService, - ) { - super(environmentService, i18nService, platformUtilsService, toastService); - } - - async ngOnInit() { - this.route?.queryParams - .pipe( - switchMap((params) => { - if (!params) { - // If no params,loadEmailSettings from state - return this.loadEmailSettings(); - } - - const queryParamsEmail = params.email; - - if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { - this.formGroup.controls.email.setValue(queryParamsEmail); - this.paramEmailSet = true; - } - - // If paramEmailSet is false, loadEmailSettings from state - return this.paramEmailSet ? of(null) : this.loadEmailSettings(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - // If the user navigates to /login from /login, reset the validatedEmail flag - // This should bring the user back to the login screen with the email field - this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => { - if (event instanceof NavigationSkipped && event.url === "/login") { - this.validatedEmail = false; - } - }); - - // Backup check to handle unknown case where activatedRoute is not available - // This shouldn't happen under normal circumstances - if (!this.route) { - await this.loadEmailSettings(); - } - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - async submit(showToast = true) { - await this.setupCaptcha(); - - this.formGroup.markAllAsTouched(); - - //web - if (this.formGroup.invalid && !showToast) { - return; - } - - //desktop, browser; This should be removed once all clients use reactive forms - if (this.formGroup.invalid && showToast) { - const errorText = this.getErrorToastMessage(); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: errorText, - }); - return; - } - - try { - const credentials = new PasswordLoginCredentials( - this.formGroup.controls.email.value, - this.formGroup.controls.masterPassword.value, - this.captchaToken, - undefined, - ); - - this.formPromise = this.loginStrategyService.logIn(credentials); - const response = await this.formPromise; - - await this.saveEmailSettings(); - - if (this.handleCaptchaRequired(response)) { - return; - } else if (await this.handleMigrateEncryptionKey(response)) { - return; - } else if (response.requiresTwoFactor) { - if (this.onSuccessfulLoginTwoFactorNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginTwoFactorNavigate(); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.twoFactorRoute]); - } - } else if (response.forcePasswordReset != ForceSetPasswordReason.None) { - if (this.onSuccessfulLoginForceResetNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginForceResetNavigate(); - } else { - this.loginEmailService.clearValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.forcePasswordResetRoute]); - } - } else { - if (this.onSuccessfulLogin != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLogin(); - } - - if (this.onSuccessfulLoginNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginNavigate(response.userId); - } else { - this.loginEmailService.clearValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); - } - } - } catch (e) { - this.logService.error(e); - } - } - - togglePassword() { - this.showPassword = !this.showPassword; - if (this.ngZone.isStable) { - document.getElementById("masterPassword").focus(); - } else { - this.ngZone.onStable - .pipe(take(1)) - .subscribe(() => document.getElementById("masterPassword").focus()); - } - } - - async startAuthRequestLogin() { - this.formGroup.get("masterPassword")?.clearValidators(); - this.formGroup.get("masterPassword")?.updateValueAndValidity(); - - if (!this.formGroup.valid) { - return; - } - - await this.saveEmailSettings(); - await this.router.navigate(["/login-with-device"]); - } - - async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { - // Save off email for SSO - await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - - // Generate necessary sso params - const passwordOptions: any = { - type: "password", - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - const state = await this.passwordGenerationService.generatePassword(passwordOptions); - const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - - // Save sso params - await this.ssoLoginService.setSsoState(state); - await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier); - - // Build URI - const env = await firstValueFrom(this.environmentService.environment$); - const webUrl = env.getWebVaultUrl(); - - // Launch browser - this.platformUtilsService.launchUri( - webUrl + - "/#/sso?clientId=" + - clientId + - "&redirectUri=" + - encodeURIComponent(ssoRedirectUri) + - "&state=" + - state + - "&codeChallenge=" + - codeChallenge + - "&email=" + - encodeURIComponent(this.formGroup.controls.email.value), - ); - } - - async validateEmail() { - this.formGroup.controls.email.markAsTouched(); - const emailValid = this.formGroup.get("email").valid; - - if (emailValid) { - this.toggleValidateEmail(true); - await this.getLoginWithDevice(this.loggedEmail); - } - } - - toggleValidateEmail(value: boolean) { - this.validatedEmail = value; - if (!this.validatedEmail) { - // Reset master password only when going from validated to not validated - // so that autofill can work properly - this.formGroup.controls.masterPassword.reset(); - } else { - // Mark MP as untouched so that, when users enter email and hit enter, - // the MP field doesn't load with validation errors - this.formGroup.controls.masterPassword.markAsUntouched(); - - // When email is validated, focus on master password after - // waiting for input to be rendered - if (this.ngZone.isStable) { - this.masterPasswordInput?.nativeElement?.focus(); - } else { - this.ngZone.onStable.pipe(take(1)).subscribe(() => { - this.masterPasswordInput?.nativeElement?.focus(); - }); - } - } - } - - private async loadEmailSettings() { - // Try to load from memory first - const email = await firstValueFrom(this.loginEmailService.loginEmail$); - const rememberEmail = this.loginEmailService.getRememberEmail(); - - if (email) { - this.formGroup.controls.email.setValue(email); - this.formGroup.controls.rememberEmail.setValue(rememberEmail); - } else { - // If not in memory, check email on disk - const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); - if (storedEmail) { - // If we have a stored email, rememberEmail should default to true - this.formGroup.controls.email.setValue(storedEmail); - this.formGroup.controls.rememberEmail.setValue(true); - } - } - } - - protected async saveEmailSettings() { - // Save off email for SSO - await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - - this.loginEmailService.setLoginEmail(this.formGroup.value.email); - this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); - await this.loginEmailService.saveEmailSettings(); - } - - // Legacy accounts used the master key to encrypt data. Migration is required but only performed on web - protected async handleMigrateEncryptionKey(result: AuthResult): Promise { - if (!result.requiresEncryptionKeyMigration) { - return false; - } - - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccured"), - message: this.i18nService.t("encryptionKeyMigrationRequired"), - }); - return true; - } - - private getErrorToastMessage() { - const error: AllValidationErrors = this.formValidationErrorService - .getFormValidationErrors(this.formGroup.controls) - .shift(); - - if (error) { - switch (error.errorName) { - case "email": - return this.i18nService.t("invalidEmail"); - case "minlength": - return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength); - default: - return this.i18nService.t(this.errorTag(error)); - } - } - - return; - } - - private errorTag(error: AllValidationErrors): string { - const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1); - return `${error.controlName}${name}`; - } - - async getLoginWithDevice(email: string) { - try { - const deviceIdentifier = await this.appIdService.getAppId(); - this.showLoginWithDevice = await this.devicesApiService.getKnownDevice( - email, - deviceIdentifier, - ); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - this.showLoginWithDevice = false; - } - } -} diff --git a/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.spec.ts b/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.spec.ts deleted file mode 100644 index 887f528d547..00000000000 --- a/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { Navigation, Router, UrlTree } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -import { unauthUiRefreshRedirect } from "./unauth-ui-refresh-redirect"; - -describe("unauthUiRefreshRedirect", () => { - let configService: MockProxy; - let router: MockProxy; - - beforeEach(() => { - configService = mock(); - router = mock(); - - TestBed.configureTestingModule({ - providers: [ - { provide: ConfigService, useValue: configService }, - { provide: Router, useValue: router }, - ], - }); - }); - - it("returns true when UnauthenticatedExtensionUIRefresh flag is disabled", async () => { - configService.getFeatureFlag.mockResolvedValue(false); - - const result = await TestBed.runInInjectionContext(() => - unauthUiRefreshRedirect("/redirect")(), - ); - - expect(result).toBe(true); - expect(configService.getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.UnauthenticatedExtensionUIRefresh, - ); - expect(router.parseUrl).not.toHaveBeenCalled(); - }); - - it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled and preserves query params", async () => { - configService.getFeatureFlag.mockResolvedValue(true); - - const urlTree = new UrlTree(); - urlTree.queryParams = { test: "test" }; - - const navigation: Navigation = { - extras: {}, - id: 0, - initialUrl: new UrlTree(), - extractedUrl: urlTree, - trigger: "imperative", - previousNavigation: undefined, - }; - - router.getCurrentNavigation.mockReturnValue(navigation); - - await TestBed.runInInjectionContext(() => unauthUiRefreshRedirect("/redirect")()); - - expect(configService.getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.UnauthenticatedExtensionUIRefresh, - ); - expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], { - queryParams: urlTree.queryParams, - }); - }); -}); diff --git a/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.ts b/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.ts deleted file mode 100644 index 2cb53d5324f..00000000000 --- a/libs/angular/src/auth/functions/unauth-ui-refresh-redirect.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { inject } from "@angular/core"; -import { UrlTree, Router } from "@angular/router"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -/** - * Helper function to redirect to a new URL based on the UnauthenticatedExtensionUIRefresh feature flag. - * @param redirectUrl - The URL to redirect to if the UnauthenticatedExtensionUIRefresh flag is enabled. - */ -export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise { - return async () => { - const configService = inject(ConfigService); - const router = inject(Router); - const shouldRedirect = await configService.getFeatureFlag( - FeatureFlag.UnauthenticatedExtensionUIRefresh, - ); - if (shouldRedirect) { - const currentNavigation = router.getCurrentNavigation(); - const queryParams = currentNavigation?.extractedUrl?.queryParams || {}; - - // Preserve query params when redirecting as it is likely that the refreshed component - // will be consuming the same query params. - return router.createUrlTree([redirectUrl], { queryParams }); - } else { - return true; - } - }; -} From ae47c12b0eba9fd73bb70875401b75e930a2fe2b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:34:04 -0500 Subject: [PATCH 40/43] chore: organize feature flags (#13809) --- libs/common/src/enums/feature-flag.enum.ts | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7a4c4fdc4aa..8662fbcbc55 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -27,16 +27,18 @@ export enum FeatureFlag { EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", - PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", + /* Vault */ + PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", + NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", + NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", VaultBulkManagementAction = "vault-bulk-management-action", + SecurityTasks = "security-tasks", + + PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", CipherKeyEncryption = "cipher-key-encryption", TrialPaymentOptional = "PM-8163-trial-payment", - SecurityTasks = "security-tasks", - NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", - NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", MacOsNativeCredentialSync = "macos-native-credential-sync", - PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", @@ -80,16 +82,18 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, - [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + /* Vault */ + [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, + [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, + [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, + [FeatureFlag.SecurityTasks]: FALSE, + + [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, - [FeatureFlag.SecurityTasks]: FALSE, - [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, - [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.ResellerManagedOrgAlert]: FALSE, [FeatureFlag.AccountDeprovisioningBanner]: FALSE, From 6b36818e4ad6374100ffb2056089853ae736b618 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:47:09 -0400 Subject: [PATCH 41/43] [PM-17232] Move all desktop native dependencies to workspace Cargo.toml (#13750) * Moved all desktop native dependencies to workspace * Excluded workspace Cargo.toml from code ownership * Added all Cargo dependencies to Renovate config * Fixed from test warnings. * Updates to lockfile --- .github/CODEOWNERS | 2 + .github/renovate.json5 | 54 ++++++++++++++- apps/desktop/desktop_native/Cargo.toml | 50 +++++++++++++- apps/desktop/desktop_native/core/Cargo.toml | 68 +++++++++---------- .../desktop_native/macos_provider/Cargo.toml | 8 +-- apps/desktop/desktop_native/napi/Cargo.toml | 12 ++-- apps/desktop/desktop_native/proxy/Cargo.toml | 6 +- .../windows-plugin-authenticator/Cargo.toml | 3 +- 8 files changed, 151 insertions(+), 52 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ba84c1f195..2550f0fddbe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev +## No ownership for Cargo.toml to allow dependency updates +apps/desktop/desktop_native/Cargo.toml ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev diff --git a/.github/renovate.json5 b/.github/renovate.json5 index bde87563dd1..01361a97404 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -168,15 +168,20 @@ matchPackageNames: [ "@emotion/css", "@webcomponents/custom-elements", + "bitwarden-russh", "bytes", "concurrently", "cross-env", "del", + "ed25519", "lit", "patch-package", + "pkcs8", "prettier", "prettier-plugin-tailwindcss", "rimraf", + "ssh-encoding", + "ssh-key", "@storybook/web-components-webpack5", "tabbable", "tldts", @@ -210,31 +215,68 @@ "@types/node-forge", "@types/node-ipc", "@yao-pkg/pkg", + "anyhow", + "arboard", "babel-loader", + "base64", + "bindgen", "browserslist", + "byteorder", + "bytes", + "core-foundation", "copy-webpack-plugin", + "dirs", "electron", "electron-builder", "electron-log", "electron-reload", "electron-store", "electron-updater", + "embed_plist", + "futures", + "hex", + "homedir", "html-webpack-injector", "html-webpack-plugin", + "interprocess", "json5", + "keytar", + "libc", + "log", "lowdb", + "napi", + "napi-build", + "napi-derive", "node-forge", "node-ipc", + "oo7", + "oslog", + "pin-project", "pkg", + "rand", "rxjs", + "scopeguard", + "security-framework", + "security-framework-sys", + "serde", + "serde_json", + "simplelog", + "sysinfo", "tsconfig-paths-webpack-plugin", "type-fest", + "typenum", "typescript", "typescript-strict-plugin", + "uniffi", "webpack", "webpack-cli", "webpack-dev-server", "webpack-node-externals", + "widestring", + "windows", + "windows-registry", + "zbus", + "zbus_polkit", ], description: "Platform owned dependencies", commitMessagePrefix: "[deps] Platform:", @@ -352,7 +394,17 @@ reviewers: ["team:team-vault-dev"], }, { - matchPackageNames: ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"], + matchPackageNames: [ + "@types/argon2-browser", + "aes", + "argon2", + "argon2-browser", + "big-integer", + "cbc", + "rsa", + "russh-cryptovec", + "sha2", + ], description: "Key Management owned dependencies", commitMessagePrefix: "[deps] KM:", reviewers: ["team:team-key-management-dev"], diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 5478d397651..5d4c547b750 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -9,11 +9,55 @@ edition = "2021" publish = false [workspace.dependencies] +aes = "=0.8.4" anyhow = "=1.0.94" +arboard = { version = "=3.4.1", default-features = false } +argon2 = "=0.5.3" +base64 = "=0.22.1" +bindgen = "0.71.1" +bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" } +byteorder = "=1.5.0" +bytes = "1.9.0" +cbc = "=0.1.2" +core-foundation = "=0.10.0" +dirs = "=6.0.0" +ed25519 = "=2.2.3" +embed_plist = "=1.2.2" +futures = "=0.3.31" +hex = "=0.4.3" +homedir = "=0.3.4" +interprocess = "=2.2.1" +keytar = "=0.1.6" +libc = "=0.2.169" log = "=0.4.25" +napi = "=2.16.15" +napi-build = "=2.1.4" +napi-derive = "=2.16.13" +oo7 = "=0.3.3" +oslog = "=0.2.0" +pin-project = "=1.1.8" +pkcs8 = "=0.10.2" +rand = "=0.8.5" +rsa = "=0.9.6" +russh-cryptovec = "=0.7.3" +scopeguard = "=1.2.0" +security-framework = "=3.1.0" +security-framework-sys = "=2.13.0" serde = "=1.0.209" serde_json = "=1.0.127" -tokio = "=1.43.0" -tokio-util = "=0.7.13" -tokio-stream = "=0.1.15" +sha2 = "=0.10.8" +simplelog = "=0.12.2" +ssh-encoding = "=0.2.0" +ssh-key = {version = "=0.6.7", default-features = false } +sysinfo = "0.33.1" thiserror = "=1.0.69" +tokio = "=1.43.0" +tokio-stream = "=0.1.15" +tokio-util = "=0.7.13" +typenum = "=1.17.0" +uniffi = "=0.28.3" +widestring = "=1.1.0" +windows = "=0.58.0" +windows-registry = "=0.4.0" +zbus = "=4.4.0" +zbus_polkit = "=4.0.0" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 8a0bcd0e0a7..346f3bde96b 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -18,47 +18,47 @@ default = [ manual_test = [] [dependencies] -aes = "=0.8.4" +aes = { workspace = true } anyhow = { workspace = true } -arboard = { version = "=3.4.1", default-features = false, features = [ +arboard = { workspace = true, features = [ "wayland-data-control", ] } -argon2 = { version = "=0.5.3", features = ["zeroize"] } -base64 = "=0.22.1" -byteorder = "=1.5.0" -cbc = { version = "=0.1.2", features = ["alloc"] } -homedir = "=0.3.4" -pin-project = "=1.1.8" -dirs = "=6.0.0" -futures = "=0.3.31" -interprocess = { version = "=2.2.1", features = ["tokio"] } +argon2 = { workspace = true, features = ["zeroize"] } +base64 = { workspace = true } +byteorder = { workspace = true } +cbc = { workspace = true, features = ["alloc"] } +homedir = { workspace = true } +pin-project = { workspace = true } +dirs = { workspace = true } +futures = { workspace = true } +interprocess = { workspace = true, features = ["tokio"] } log = { workspace = true } -rand = "=0.8.5" -russh-cryptovec = "=0.7.3" -scopeguard = "=1.2.0" -sha2 = "=0.10.8" -ssh-encoding = "=0.2.0" -ssh-key = { version = "=0.6.7", default-features = false, features = [ +rand = { workspace = true } +russh-cryptovec = { workspace = true } +scopeguard = { workspace = true } +sha2 = { workspace = true } +ssh-encoding = { workspace = true } +ssh-key = { workspace = true, features = [ "encryption", "ed25519", "rsa", "getrandom", ] } -bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" } +bitwarden-russh = { workspace = true } tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] } tokio-stream = { workspace = true, features = ["net"] } tokio-util = { workspace = true, features = ["codec"] } thiserror = { workspace = true } -typenum = "=1.17.0" -pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } -rsa = "=0.9.6" -ed25519 = { version = "=2.2.3", features = ["pkcs8"] } -bytes = "1.9.0" -sysinfo = { version = "0.33.1", features = ["windows"] } +typenum = { workspace = true } +pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] } +rsa = { workspace = true } +ed25519 = { workspace = true, features = ["pkcs8"] } +bytes = { workspace = true } +sysinfo = { workspace = true, features = ["windows"] } [target.'cfg(windows)'.dependencies] -widestring = { version = "=1.1.0", optional = true } -windows = { version = "=0.58.0", features = [ +widestring = { workspace = true, optional = true } +windows = { workspace = true, features = [ "Foundation", "Security_Credentials_UI", "Security_Cryptography", @@ -72,17 +72,17 @@ windows = { version = "=0.58.0", features = [ ], optional = true } [target.'cfg(windows)'.dev-dependencies] -keytar = "=0.1.6" +keytar = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { version = "=0.10.0", optional = true } -security-framework = { version = "=3.1.0", optional = true } -security-framework-sys = { version = "=2.13.0", optional = true } +core-foundation = { workspace = true, optional = true } +security-framework = { workspace = true, optional = true } +security-framework-sys = { workspace = true, optional = true } desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] -oo7 = "=0.3.3" -libc = "=0.2.169" +oo7 = { workspace = true } +libc = { workspace = true } -zbus = { version = "=4.4.0", optional = true } -zbus_polkit = { version = "=4.0.0", optional = true } +zbus = { workspace = true, optional = true } +zbus_polkit = { workspace = true, optional = true } diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index e160f7f35d6..cf7338f23cf 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -15,16 +15,16 @@ bench = false [dependencies] desktop_core = { path = "../core" } -futures = "=0.3.31" +futures = { workspace = true } log = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } -uniffi = { version = "=0.28.3", features = ["cli"] } +uniffi = { workspace = true, features = ["cli"] } [target.'cfg(target_os = "macos")'.dependencies] -oslog = "=0.2.0" +oslog = { workspace = true } [build-dependencies] -uniffi = { version = "=0.28.3", features = ["build"] } +uniffi = { workspace = true, features = ["build"] } diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index a9513fa49a8..f5b6d6b7c04 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -14,12 +14,12 @@ default = [] manual_test = [] [dependencies] -base64 = "=0.22.1" -hex = "=0.4.3" +base64 = { workspace = true } +hex = { workspace = true } anyhow = { workspace = true } desktop_core = { path = "../core" } -napi = { version = "=2.16.15", features = ["async"] } -napi-derive = "=2.16.13" +napi = { workspace = true, features = ["async"] } +napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true } @@ -27,7 +27,7 @@ tokio-util = { workspace = true } tokio-stream = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-registry = "=0.4.0" +windows-registry = { workspace = true } [build-dependencies] -napi-build = "=2.1.4" +napi-build = { workspace = true } diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index 10ff1ed8eae..a9a1a5aca28 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -8,11 +8,11 @@ publish = { workspace = true } [dependencies] anyhow = { workspace = true } desktop_core = { path = "../core" } -futures = "=0.3.31" +futures = { workspace = true } log = { workspace = true } -simplelog = "=0.12.2" +simplelog = { workspace = true } tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] } tokio-util = { workspace = true, features = ["codec"] } [target.'cfg(target_os = "macos")'.dependencies] -embed_plist = "=1.2.2" +embed_plist = { workspace = true } diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml b/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml index 3443fed4fd6..d58a6ecd748 100644 --- a/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml @@ -6,4 +6,5 @@ version = { workspace = true } publish = { workspace = true } [target.'cfg(target_os = "windows")'.build-dependencies] -bindgen = "0.71.1" +bindgen = { workspace = true } + From 942b80e5d239f6dd7c543ea5f9a595e0807e9263 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:40:05 -0500 Subject: [PATCH 42/43] refactor(auth): [PM-9724] remove LoginViaAuthRequestComponentV1 and related functionality - Remove deprecated LoginViaAuthRequestComponentV1 component - Clean up related ApiService functionality - Remove orphaned translation messages - Remove unused CSS styles Jira PM-9724 --- apps/browser/src/_locales/en/messages.json | 9 - .../login-via-auth-request-v1.component.html | 68 --- .../login-via-auth-request-v1.component.ts | 85 --- apps/browser/src/popup/app.module.ts | 2 - apps/browser/src/popup/scss/base.scss | 32 -- .../login-via-auth-request-v1.component.html | 80 --- .../login-via-auth-request-v1.component.ts | 117 ---- apps/desktop/src/auth/login/login.module.ts | 9 +- apps/desktop/src/locales/en/messages.json | 3 - apps/desktop/src/scss/pages.scss | 37 -- .../login-via-auth-request-v1.component.html | 70 --- .../login-via-auth-request-v1.component.ts | 9 - apps/web/src/app/auth/login/login.module.ts | 13 +- apps/web/src/locales/en/messages.json | 3 - .../login-via-auth-request-v1.component.ts | 538 ------------------ libs/common/src/abstractions/api.service.ts | 4 - libs/common/src/services/api.service.ts | 17 - 17 files changed, 4 insertions(+), 1092 deletions(-) delete mode 100644 apps/browser/src/auth/popup/login-via-auth-request-v1.component.html delete mode 100644 apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts delete mode 100644 apps/desktop/src/auth/login/login-via-auth-request-v1.component.html delete mode 100644 apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts delete mode 100644 apps/web/src/app/auth/login/login-via-auth-request-v1.component.html delete mode 100644 apps/web/src/app/auth/login/login-via-auth-request-v1.component.ts delete mode 100644 libs/angular/src/auth/components/login-via-auth-request-v1.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 127e07f25e8..86977b950f3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3338,9 +3338,6 @@ "loginWithDevice": { "message": "Log in with device" }, - "loginWithDeviceEnabledInfo": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" - }, "fingerprintPhraseHeader": { "message": "Fingerprint phrase" }, @@ -3353,9 +3350,6 @@ "viewAllLogInOptions": { "message": "View all log in options" }, - "viewAllLoginOptionsV1": { - "message": "View all log in options" - }, "notificationSentDevice": { "message": "A notification has been sent to your device." }, @@ -3546,9 +3540,6 @@ "adminApprovalRequestSentToAdmins": { "message": "Your request has been sent to your admin." }, - "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." - }, "troubleLoggingIn": { "message": "Trouble logging in?" }, diff --git a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html deleted file mode 100644 index 34c0cbe9614..00000000000 --- a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html +++ /dev/null @@ -1,68 +0,0 @@ - diff --git a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts deleted file mode 100644 index 66c69d0a41a..00000000000 --- a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Location } from "@angular/common"; -import { Component } from "@angular/core"; -import { Router } from "@angular/router"; - -import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-login-via-auth-request", - templateUrl: "login-via-auth-request-v1.component.html", -}) -export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 { - constructor( - router: Router, - keyService: KeyService, - cryptoFunctionService: CryptoFunctionService, - appIdService: AppIdService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - apiService: ApiService, - authService: AuthService, - logService: LogService, - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - anonymousHubService: AnonymousHubService, - validationService: ValidationService, - loginEmailService: LoginEmailServiceAbstraction, - syncService: SyncService, - deviceTrustService: DeviceTrustServiceAbstraction, - authRequestService: AuthRequestServiceAbstraction, - loginStrategyService: LoginStrategyServiceAbstraction, - accountService: AccountService, - private location: Location, - toastService: ToastService, - ) { - super( - router, - keyService, - cryptoFunctionService, - appIdService, - passwordGenerationService, - apiService, - authService, - logService, - environmentService, - i18nService, - platformUtilsService, - anonymousHubService, - validationService, - accountService, - loginEmailService, - deviceTrustService, - authRequestService, - loginStrategyService, - toastService, - ); - this.onSuccessfulLogin = async () => { - await syncService.fullSync(true); - }; - } - - protected back() { - this.location.back(); - } -} diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 13aeb25b998..72117a2f900 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -23,7 +23,6 @@ import { EnvironmentComponent } from "../auth/popup/environment.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { HintComponent } from "../auth/popup/hint.component"; import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; @@ -96,7 +95,6 @@ import "../platform/popup/locales"; ColorPasswordCountPipe, EnvironmentComponent, HintComponent, - LoginViaAuthRequestComponentV1, LoginDecryptionOptionsComponentV1, SetPasswordComponent, SsoComponentV1, diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index bfa037eea23..7a8d39c6f3e 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -448,38 +448,6 @@ main:not(popup-page main) { width: 100%; } -.login-with-device { - .fingerprint-phrase-header { - padding-top: 1rem; - display: block; - } - - @include themify($themes) { - .fingerprint-text { - color: themed("codeColor"); - font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; - padding: 1rem 0; - } - } - - .resend-notification { - padding-bottom: 1rem; - a { - cursor: pointer; - } - } - - .footer { - padding-top: 1rem; - - a { - padding-top: 1rem; - display: block; - } - } -} - #login-initiated { .margin-auto { margin: auto; diff --git a/apps/desktop/src/auth/login/login-via-auth-request-v1.component.html b/apps/desktop/src/auth/login/login-via-auth-request-v1.component.html deleted file mode 100644 index 9825949f7ec..00000000000 --- a/apps/desktop/src/auth/login/login-via-auth-request-v1.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
-
- Bitwarden - - -

{{ "logInRequestSent" | i18n }}

- -
-
-
-
-

- {{ "notificationSentDevicePart1" | i18n }} - {{ "notificationSentDeviceAnchor" | i18n }}. {{ "notificationSentDevicePart2" | i18n }} -

-
- -
-

{{ "fingerprintPhraseHeader" | i18n }}

- {{ fingerprintPhrase }} -
- - - -
-

- {{ "needAnotherOption" | i18n }} - - {{ "viewAllLoginOptions" | i18n }} - -

-
-
-
-
-
- - -

{{ "adminApprovalRequested" | i18n }}

- -
-
-
-
-

{{ "adminApprovalRequestSentToAdmins" | i18n }}

-

{{ "youWillBeNotifiedOnceApproved" | i18n }}

-
- -
-

{{ "fingerprintPhraseHeader" | i18n }}

- {{ fingerprintPhrase }} -
- -
-

- {{ "troubleLoggingIn" | i18n }} - - {{ "viewAllLoginOptions" | i18n }} - -

-
-
-
-
-
-
-
- diff --git a/apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts b/apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts deleted file mode 100644 index 30e693b9ac6..00000000000 --- a/apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; -import { Router } from "@angular/router"; - -import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { KeyService } from "@bitwarden/key-management"; - -import { EnvironmentComponent } from "../environment.component"; - -@Component({ - selector: "app-login-via-auth-request", - templateUrl: "login-via-auth-request-v1.component.html", -}) -export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 { - @ViewChild("environment", { read: ViewContainerRef, static: true }) - environmentModal: ViewContainerRef; - showingModal = false; - - constructor( - protected router: Router, - keyService: KeyService, - cryptoFunctionService: CryptoFunctionService, - appIdService: AppIdService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - apiService: ApiService, - authService: AuthService, - logService: LogService, - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - anonymousHubService: AnonymousHubService, - validationService: ValidationService, - private modalService: ModalService, - syncService: SyncService, - loginEmailService: LoginEmailServiceAbstraction, - deviceTrustService: DeviceTrustServiceAbstraction, - authRequestService: AuthRequestServiceAbstraction, - loginStrategyService: LoginStrategyServiceAbstraction, - accountService: AccountService, - private location: Location, - toastService: ToastService, - ) { - super( - router, - keyService, - cryptoFunctionService, - appIdService, - passwordGenerationService, - apiService, - authService, - logService, - environmentService, - i18nService, - platformUtilsService, - anonymousHubService, - validationService, - accountService, - loginEmailService, - deviceTrustService, - authRequestService, - loginStrategyService, - toastService, - ); - - this.onSuccessfulLogin = () => { - return syncService.fullSync(true); - }; - } - - async settings() { - const [modal, childComponent] = await this.modalService.openViewRef( - EnvironmentComponent, - this.environmentModal, - ); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - modal.onShown.subscribe(() => { - this.showingModal = true; - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - modal.onClosed.subscribe(() => { - this.showingModal = false; - }); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - childComponent.onSaved.subscribe(() => { - modal.close(); - }); - } - - back() { - this.location.back(); - } -} diff --git a/apps/desktop/src/auth/login/login.module.ts b/apps/desktop/src/auth/login/login.module.ts index 8cd1bdb9212..6fe9f9b07d3 100644 --- a/apps/desktop/src/auth/login/login.module.ts +++ b/apps/desktop/src/auth/login/login.module.ts @@ -6,15 +6,10 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { SharedModule } from "../../app/shared/shared.module"; import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component"; @NgModule({ imports: [SharedModule, RouterModule], - declarations: [ - LoginViaAuthRequestComponentV1, - EnvironmentSelectorComponent, - LoginDecryptionOptionsComponentV1, - ], - exports: [LoginViaAuthRequestComponentV1], + declarations: [EnvironmentSelectorComponent, LoginDecryptionOptionsComponentV1], + exports: [], }) export class LoginModule {} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 7739ab84577..0bc76287adb 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3060,9 +3060,6 @@ "adminApprovalRequestSentToAdmins": { "message": "Your request has been sent to your admin." }, - "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." - }, "troubleLoggingIn": { "message": "Trouble logging in?" }, diff --git a/apps/desktop/src/scss/pages.scss b/apps/desktop/src/scss/pages.scss index fda75e834f3..5f8f501d420 100644 --- a/apps/desktop/src/scss/pages.scss +++ b/apps/desktop/src/scss/pages.scss @@ -1,7 +1,6 @@ @import "variables.scss"; #login-page, -#login-with-device-page, #lock-page, #sso-page, #set-password-page, @@ -191,7 +190,6 @@ } #login-page, -#login-with-device-page, #login-decryption-options-page { flex-direction: column; justify-content: unset; @@ -222,41 +220,6 @@ } } -#login-with-device-page { - .content { - display: block; - padding-top: 70px; - width: 350px !important; - - .fingerprint { - margin: auto; - width: 315px; - - .fingerpint-header { - padding-left: 15px; - } - } - - .section { - margin-bottom: 30px; - } - - .another-method { - display: flex; - margin: auto; - .description-text { - padding-right: 5px; - } - } - - code { - @include themify($themes) { - color: themed("codeColor"); - } - } - } -} - #login-approval-page { .section-title { padding: 20px; diff --git a/apps/web/src/app/auth/login/login-via-auth-request-v1.component.html b/apps/web/src/app/auth/login/login-via-auth-request-v1.component.html deleted file mode 100644 index ed157eb9cf4..00000000000 --- a/apps/web/src/app/auth/login/login-via-auth-request-v1.component.html +++ /dev/null @@ -1,70 +0,0 @@ -
-
- - - -

- {{ "loginOrCreateNewAccount" | i18n }} -

- -
-

{{ "logInRequestSent" | i18n }}

- -

- {{ "notificationSentDeviceComplete" | i18n }} -

- -
-

{{ "fingerprintPhraseHeader" | i18n }}

-

- {{ fingerprintPhrase }} -

-
- - - -
- -
- {{ "loginWithDeviceEnabledNote" | i18n }} - {{ "viewAllLoginOptions" | i18n }} -
-
-
- - -
-

{{ "adminApprovalRequested" | i18n }}

- -
-

{{ "adminApprovalRequestSentToAdmins" | i18n }}

-

{{ "youWillBeNotifiedOnceApproved" | i18n }}

-
- -
-

{{ "fingerprintPhraseHeader" | i18n }}

-

- {{ fingerprintPhrase }} -

-
- -
- -
- {{ "troubleLoggingIn" | i18n }} - {{ "viewAllLoginOptions" | i18n }} -
-
-
-
-
diff --git a/apps/web/src/app/auth/login/login-via-auth-request-v1.component.ts b/apps/web/src/app/auth/login/login-via-auth-request-v1.component.ts deleted file mode 100644 index 8a8883e035b..00000000000 --- a/apps/web/src/app/auth/login/login-via-auth-request-v1.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from "@angular/core"; - -import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component"; - -@Component({ - selector: "app-login-via-auth-request", - templateUrl: "login-via-auth-request-v1.component.html", -}) -export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {} diff --git a/apps/web/src/app/auth/login/login.module.ts b/apps/web/src/app/auth/login/login.module.ts index 60c20eb4b7b..12f908324a3 100644 --- a/apps/web/src/app/auth/login/login.module.ts +++ b/apps/web/src/app/auth/login/login.module.ts @@ -5,20 +5,11 @@ import { CheckboxModule } from "@bitwarden/components"; import { SharedModule } from "../../../app/shared"; import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component"; import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component"; @NgModule({ imports: [SharedModule, CheckboxModule], - declarations: [ - LoginViaAuthRequestComponentV1, - LoginDecryptionOptionsComponentV1, - LoginViaWebAuthnComponent, - ], - exports: [ - LoginViaAuthRequestComponentV1, - LoginDecryptionOptionsComponentV1, - LoginViaWebAuthnComponent, - ], + declarations: [LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent], + exports: [LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent], }) export class LoginModule {} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 68900b2ed74..3e47e2b2279 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8681,9 +8681,6 @@ "adminApprovalRequestSentToAdmins": { "message": "Your request has been sent to your admin." }, - "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." - }, "troubleLoggingIn": { "message": "Trouble logging in?" }, diff --git a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts b/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts deleted file mode 100644 index 7f5a5c3f299..00000000000 --- a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts +++ /dev/null @@ -1,538 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, OnDestroy, OnInit } from "@angular/core"; -import { IsActiveMatchOptions, Router } from "@angular/router"; -import { Subject, firstValueFrom, map, takeUntil } from "rxjs"; - -import { - AuthRequestLoginCredentials, - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; -import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; -import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; -import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum"; -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { KeyService } from "@bitwarden/key-management"; - -import { CaptchaProtectedComponent } from "./captcha-protected.component"; - -enum State { - StandardAuthRequest, - AdminAuthRequest, -} - -@Directive() -export class LoginViaAuthRequestComponentV1 - extends CaptchaProtectedComponent - implements OnInit, OnDestroy -{ - private destroy$ = new Subject(); - userAuthNStatus: AuthenticationStatus; - email: string; - showResendNotification = false; - authRequest: AuthRequest; - fingerprintPhrase: string; - onSuccessfulLoginTwoFactorNavigate: () => Promise; - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; - onSuccessfulLoginForceResetNavigate: () => Promise; - - protected adminApprovalRoute = "admin-approval-requested"; - - protected StateEnum = State; - protected state = State.StandardAuthRequest; - protected webVaultUrl: string; - protected twoFactorRoute = "2fa"; - protected successRoute = "vault"; - protected forcePasswordResetRoute = "update-temp-password"; - private resendTimeout = 12000; - protected deviceManagementUrl: string; - - private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; - - constructor( - protected router: Router, - private keyService: KeyService, - private cryptoFunctionService: CryptoFunctionService, - private appIdService: AppIdService, - private passwordGenerationService: PasswordGenerationServiceAbstraction, - private apiService: ApiService, - private authService: AuthService, - private logService: LogService, - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - private anonymousHubService: AnonymousHubService, - private validationService: ValidationService, - private accountService: AccountService, - private loginEmailService: LoginEmailServiceAbstraction, - private deviceTrustService: DeviceTrustServiceAbstraction, - private authRequestService: AuthRequestServiceAbstraction, - private loginStrategyService: LoginStrategyServiceAbstraction, - protected toastService: ToastService, - ) { - super(environmentService, i18nService, platformUtilsService, toastService); - - // Get the web vault URL from the environment service - environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => { - this.webVaultUrl = env.getWebVaultUrl(); - this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`; - }); - - // Gets signalR push notification - // Only fires on approval to prevent enumeration - this.authRequestService.authRequestPushNotification$ - .pipe(takeUntil(this.destroy$)) - .subscribe((id) => { - this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("error"), - message: e.message, - }); - this.logService.error("Failed to use approved auth request: " + e.message); - }); - }); - } - - async ngOnInit() { - this.email = await firstValueFrom(this.loginEmailService.loginEmail$); - this.userAuthNStatus = await this.authService.getAuthStatus(); - - const matchOptions: IsActiveMatchOptions = { - paths: "exact", - queryParams: "ignored", - fragment: "ignored", - matrixParams: "ignored", - }; - - if (this.router.isActive(this.adminApprovalRoute, matchOptions)) { - this.state = State.AdminAuthRequest; - } - - if (this.state === State.AdminAuthRequest) { - // Pull email from state for admin auth reqs b/c it is available - // This also prevents it from being lost on refresh as the - // login service email does not persist. - this.email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - - if (!this.email) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("userEmailMissing"), - }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/login-initiated"]); - return; - } - - // We only allow a single admin approval request to be active at a time - // so must check state to see if we have an existing one or not - const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId); - - if (adminAuthReqStorable) { - await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId); - } else { - // No existing admin auth request; so we need to create one - await this.startAuthRequestLogin(); - } - } else { - // Standard auth request - // TODO: evaluate if we can remove the setting of this.email in the constructor - this.email = await firstValueFrom(this.loginEmailService.loginEmail$); - - if (!this.email) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("userEmailMissing"), - }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/login"]); - return; - } - - await this.startAuthRequestLogin(); - } - } - - async ngOnDestroy() { - await this.anonymousHubService.stopHubConnection(); - this.destroy$.next(); - this.destroy$.complete(); - } - - private async handleExistingAdminAuthRequest( - adminAuthReqStorable: AdminAuthRequestStorable, - userId: UserId, - ) { - // Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req - // has been approved and handle it if so. - - // Regardless, we always retrieve the auth request from the server verify and handle status changes here as well - let adminAuthReqResponse: AuthRequestResponse; - try { - adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id); - } catch (error) { - if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); - } - } - - // Request doesn't exist anymore - if (!adminAuthReqResponse) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); - } - - // Re-derive the user's fingerprint phrase - // It is important to not use the server's public key here as it could have been compromised via MITM - const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey( - adminAuthReqStorable.privateKey, - ); - this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( - this.email, - derivedPublicKeyArrayBuffer, - ); - - // Request denied - if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); - } - - // Request approved - if (adminAuthReqResponse.requestApproved) { - return await this.handleApprovedAdminAuthRequest( - adminAuthReqResponse, - adminAuthReqStorable.privateKey, - userId, - ); - } - - // Request still pending response from admin - // set keypair and create hub connection so that any approvals will be received via push notification - this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null }; - await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); - } - - private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) { - // clear the admin auth request from state - await this.authRequestService.clearAdminAuthRequest(userId); - - // start new auth request - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.startAuthRequestLogin(); - } - - private async buildAuthRequest(authRequestType: AuthRequestType) { - const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); - - this.authRequestKeyPair = { - publicKey: authRequestKeyPairArray[0], - privateKey: authRequestKeyPairArray[1], - }; - - const deviceIdentifier = await this.appIdService.getAppId(); - const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey); - const accessCode = await this.passwordGenerationService.generatePassword({ - type: "password", - length: 25, - }); - - this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( - this.email, - this.authRequestKeyPair.publicKey, - ); - - this.authRequest = new AuthRequest( - this.email, - deviceIdentifier, - publicKey, - authRequestType, - accessCode, - ); - } - - async startAuthRequestLogin() { - this.showResendNotification = false; - - try { - let reqResponse: AuthRequestResponse; - - if (this.state === State.AdminAuthRequest) { - await this.buildAuthRequest(AuthRequestType.AdminApproval); - reqResponse = await this.apiService.postAdminAuthRequest(this.authRequest); - - const adminAuthReqStorable = new AdminAuthRequestStorable({ - id: reqResponse.id, - privateKey: this.authRequestKeyPair.privateKey, - }); - - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId); - } else { - await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); - reqResponse = await this.apiService.postAuthRequest(this.authRequest); - } - - if (reqResponse.id) { - await this.anonymousHubService.createHubConnection(reqResponse.id); - } - } catch (e) { - this.logService.error(e); - } - - setTimeout(() => { - this.showResendNotification = true; - }, this.resendTimeout); - } - - private async verifyAndHandleApprovedAuthReq(requestId: string) { - try { - // Retrieve the auth request from server and verify it's approved - let authReqResponse: AuthRequestResponse; - - switch (this.state) { - case State.StandardAuthRequest: - // Unauthed - access code required for user verification - authReqResponse = await this.apiService.getAuthResponse( - requestId, - this.authRequest.accessCode, - ); - break; - - case State.AdminAuthRequest: - // Authed - no access code required - authReqResponse = await this.apiService.getAuthRequest(requestId); - break; - - default: - break; - } - - if (!authReqResponse.requestApproved) { - return; - } - - // Approved so proceed: - - // 4 Scenarios to handle for approved auth requests: - // Existing flow 1: - // - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey) - // > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault - - // 3 new flows from TDE: - // Flow 2: - // - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey) - // > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault - // Flow 3: - // - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault - // Flow 4: - // - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey) - // > decrypt userKey > must authenticate > set userKey > proceed to vault - - // if user has authenticated via SSO - if (this.userAuthNStatus === AuthenticationStatus.Locked) { - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - return await this.handleApprovedAdminAuthRequest( - authReqResponse, - this.authRequestKeyPair.privateKey, - userId, - ); - } - - // Flow 1 and 4: - const loginAuthResult = await this.loginViaAuthRequestStrategy(requestId, authReqResponse); - await this.handlePostLoginNavigation(loginAuthResult); - } catch (error) { - if (error instanceof ErrorResponse) { - let errorRoute = "/login"; - if (this.state === State.AdminAuthRequest) { - errorRoute = "/login-initiated"; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([errorRoute]); - this.validationService.showError(error); - return; - } - - this.logService.error(error); - } - } - - async handleApprovedAdminAuthRequest( - adminAuthReqResponse: AuthRequestResponse, - privateKey: ArrayBuffer, - userId: UserId, - ) { - // See verifyAndHandleApprovedAuthReq(...) for flow details - // it's flow 2 or 3 based on presence of masterPasswordHash - if (adminAuthReqResponse.masterPasswordHash) { - // Flow 2: masterPasswordHash is not null - // key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash) - await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( - adminAuthReqResponse, - privateKey, - userId, - ); - } else { - // Flow 3: masterPasswordHash is null - // we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault - await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( - adminAuthReqResponse, - privateKey, - userId, - ); - } - - // clear the admin auth request from state so it cannot be used again (it's a one time use) - // TODO: this should eventually be enforced via deleting this on the server once it is used - await this.authRequestService.clearAdminAuthRequest(userId); - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("loginApproved"), - }); - - // Now that we have a decrypted user key in memory, we can check if we - // need to establish trust on the current device - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); - - // TODO: don't forget to use auto enrollment service everywhere we trust device - - await this.handleSuccessfulLoginNavigation(); - } - - // Authentication helper - private async buildAuthRequestLoginCredentials( - requestId: string, - response: AuthRequestResponse, - ): Promise { - // if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash) - // if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey) - if (response.masterPasswordHash) { - const { masterKey, masterKeyHash } = - await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash( - response.key, - response.masterPasswordHash, - this.authRequestKeyPair.privateKey, - ); - - return new AuthRequestLoginCredentials( - this.email, - this.authRequest.accessCode, - requestId, - null, // no userKey - masterKey, - masterKeyHash, - ); - } else { - const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( - response.key, - this.authRequestKeyPair.privateKey, - ); - return new AuthRequestLoginCredentials( - this.email, - this.authRequest.accessCode, - requestId, - userKey, - null, // no masterKey - null, // no masterKeyHash - ); - } - } - - private async loginViaAuthRequestStrategy( - requestId: string, - authReqResponse: AuthRequestResponse, - ): Promise { - // Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey - const credentials = await this.buildAuthRequestLoginCredentials(requestId, authReqResponse); - - // Note: keys are set by AuthRequestLoginStrategy success handling - return await this.loginStrategyService.logIn(credentials); - } - - // Routing logic - private async handlePostLoginNavigation(loginResponse: AuthResult) { - if (loginResponse.requiresTwoFactor) { - if (this.onSuccessfulLoginTwoFactorNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginTwoFactorNavigate(); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.twoFactorRoute]); - } - } else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) { - if (this.onSuccessfulLoginForceResetNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginForceResetNavigate(); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.forcePasswordResetRoute]); - } - } else { - await this.handleSuccessfulLoginNavigation(); - } - } - - private async handleSuccessfulLoginNavigation() { - if (this.state === State.StandardAuthRequest) { - // Only need to set remembered email on standard login with auth req flow - await this.loginEmailService.saveEmailSettings(); - } - - if (this.onSuccessfulLogin != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLogin(); - } - - if (this.onSuccessfulLoginNavigate != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulLoginNavigate(); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); - } - } -} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 32cabbf7618..013b36f6358 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -38,7 +38,6 @@ import { ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; -import { AuthRequest } from "../auth/models/request/auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; @@ -185,9 +184,6 @@ export abstract class ApiService { postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; postConvertToKeyConnector: () => Promise; //passwordless - postAuthRequest: (request: AuthRequest) => Promise; - postAdminAuthRequest: (request: AuthRequest) => Promise; - getAuthResponse: (id: string, accessCode: string) => Promise; getAuthRequest: (id: string) => Promise; putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise; getAuthRequests: () => Promise>; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 3d89e75cf8a..93e455ada80 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -43,7 +43,6 @@ import { } from "../admin-console/models/response/provider/provider-user.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { TokenService } from "../auth/abstractions/token.service"; -import { AuthRequest } from "../auth/models/request/auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; @@ -275,22 +274,6 @@ export class ApiService implements ApiServiceAbstraction { } // TODO: PM-3519: Create and move to AuthRequest Api service - // TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components - async postAuthRequest(request: AuthRequest): Promise { - const r = await this.send("POST", "/auth-requests/", request, false, true); - return new AuthRequestResponse(r); - } - async postAdminAuthRequest(request: AuthRequest): Promise { - const r = await this.send("POST", "/auth-requests/admin-request", request, true, true); - return new AuthRequestResponse(r); - } - - async getAuthResponse(id: string, accessCode: string): Promise { - const path = `/auth-requests/${id}/response?code=${accessCode}`; - const r = await this.send("GET", path, null, false, true); - return new AuthRequestResponse(r); - } - async getAuthRequest(id: string): Promise { const path = `/auth-requests/${id}`; const r = await this.send("GET", path, null, true, true); From 6c2d7e92e6fd38adc8a9c30b97b3f9e3f9079f2b Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:19:06 -0700 Subject: [PATCH 43/43] allow item history to be selectable (#13708) --- .../cipher-view/item-history/item-history-v2.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html index 19d1cfe1744..c069e36dde1 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html @@ -3,12 +3,12 @@

{{ "itemHistory" | i18n }}

-

+

{{ "lastEdited" | i18n }}: {{ cipher.revisionDate | date: "medium" }}

{{ "datePasswordUpdated" | i18n }}: