diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 883dc61f49f..d480879fb15 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -72,7 +72,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT cli: - name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}" + name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }} strategy: matrix: os: @@ -177,7 +177,7 @@ jobs: if-no-files-found: error cli-windows: - name: "windows - ${{ matrix.license_type.readable }}" + name: Windows - ${{ matrix.license_type.readable }} strategy: matrix: license_type: diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 27995f1c787..dc15f841c2b 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -272,7 +272,7 @@ jobs: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml if-no-files-found: error - + - name: Build flatpak working-directory: apps/desktop run: | diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 5cc4eb90861..b5e84ff875b 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -266,7 +266,8 @@ jobs: channel_id: ${{ steps.slack-message.outputs.channel_id }} ts: ${{ steps.slack-message.outputs.ts }} steps: - - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main + - name: Notify Slack with start message + uses: bitwarden/gh-actions/report-deployment-status-to-slack@main id: slack-message with: project: Clients @@ -419,7 +420,8 @@ jobs: - azure-deploy - artifact-check steps: - - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main + - name: Notify Slack with result + uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: project: Clients environment: ${{ needs.setup.outputs.environment-name }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c324cb8748..72bc3594beb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,14 +98,14 @@ jobs: rust: name: Run Rust tests on ${{ matrix.os }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} permissions: contents: read strategy: matrix: os: - - ubuntu-latest + - ubuntu-22.04 - macos-latest - windows-latest diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 1617ed84767..68b46ad8810 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -215,7 +215,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.form.controls.vaultTimeoutAction.valueChanges .pipe( - startWith(initialValues.vaultTimeoutAction), // emit to init pairwise map(async (value) => { await this.saveVaultTimeoutAction(value); }), diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 35c4f74144e..08b06e7cf0e 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -53,7 +53,7 @@ ssh-key = { version = "=0.6.6", default-features = false, features = [ bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/allow_additional_data" } tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] } tokio-stream = { version = "=0.1.15", features = ["net"] } -tokio-util = "=0.7.12" +tokio-util = { version = "0.7.11", features = ["codec"] } thiserror = "=1.0.69" typenum = "=1.17.0" rand_chacha = "=0.3.1" diff --git a/apps/desktop/desktop_native/core/src/ipc/client.rs b/apps/desktop/desktop_native/core/src/ipc/client.rs index 7eff8a10974..6c4ca0a6053 100644 --- a/apps/desktop/desktop_native/core/src/ipc/client.rs +++ b/apps/desktop/desktop_native/core/src/ipc/client.rs @@ -1,13 +1,11 @@ use std::path::PathBuf; +use futures::{SinkExt, StreamExt}; use interprocess::local_socket::{ tokio::{prelude::*, Stream}, GenericFilePath, ToFsName, }; use log::{error, info}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE; pub async fn connect( path: PathBuf, @@ -17,7 +15,9 @@ pub async fn connect( info!("Attempting to connect to {}", path.display()); let name = path.as_os_str().to_fs_name::()?; - let mut conn = Stream::connect(name).await?; + let conn = Stream::connect(name).await?; + + let mut conn = crate::ipc::internal_ipc_codec(conn); info!("Connected to {}", path.display()); @@ -26,8 +26,6 @@ pub async fn connect( // As it's only two, we hardcode the JSON values to avoid pulling in a JSON library. send.send("{\"command\":\"connected\"}".to_owned()).await?; - let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE]; - // Listen to IPC messages loop { tokio::select! { @@ -35,7 +33,7 @@ pub async fn connect( msg = recv.recv() => { match msg { Some(msg) => { - conn.write_all(msg.as_bytes()).await?; + conn.send(msg.into()).await?; } None => { info!("Client channel closed"); @@ -45,18 +43,18 @@ pub async fn connect( }, // Forward messages from the IPC server - res = conn.read(&mut buffer[..]) => { + res = conn.next() => { match res { - Err(e) => { + Some(Err(e)) => { error!("Error reading from IPC server: {e}"); break; } - Ok(0) => { + None => { info!("Connection closed"); break; } - Ok(n) => { - let message = String::from_utf8_lossy(&buffer[..n]).to_string(); + Some(Ok(bytes)) => { + let message = String::from_utf8_lossy(&bytes).to_string(); send.send(message).await?; } } diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs index d406b6aa137..6873f0cfb80 100644 --- a/apps/desktop/desktop_native/core/src/ipc/mod.rs +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -1,3 +1,6 @@ +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + pub mod client; pub mod server; @@ -16,6 +19,16 @@ pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024; /// but ideally the messages should be processed as quickly as possible. pub const MESSAGE_CHANNEL_BUFFER: usize = 32; +/// This is the codec used for communication through the UNIX socket / Windows named pipe. +/// It's an internal implementation detail, but we want to make sure that both the client +/// and the server use the same one. +fn internal_ipc_codec(inner: T) -> Framed { + LengthDelimitedCodec::builder() + .max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE) + .native_endian() + .new_framed(inner) +} + /// Resolve the path to the IPC socket. pub fn path(name: &str) -> std::path::PathBuf { #[cfg(target_os = "windows")] diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs index 0aa1cf30177..a1c77e7ab16 100644 --- a/apps/desktop/desktop_native/core/src/ipc/server.rs +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -1,21 +1,20 @@ use std::{ error::Error, path::{Path, PathBuf}, - vec, }; -use futures::TryFutureExt; +use futures::{SinkExt, StreamExt, TryFutureExt}; use anyhow::Result; use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions}; use log::{error, info}; use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + io::{AsyncRead, AsyncWrite}, sync::{broadcast, mpsc}, }; use tokio_util::sync::CancellationToken; -use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE}; +use super::MESSAGE_CHANNEL_BUFFER; #[derive(Debug)] pub struct Message { @@ -158,7 +157,7 @@ async fn listen_incoming( } async fn handle_connection( - mut client_stream: impl AsyncRead + AsyncWrite + Unpin, + client_stream: impl AsyncRead + AsyncWrite + Unpin, client_to_server_send: mpsc::Sender, mut server_to_clients_recv: broadcast::Receiver, cancel_token: CancellationToken, @@ -172,7 +171,7 @@ async fn handle_connection( }) .await?; - let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE]; + let mut client_stream = crate::ipc::internal_ipc_codec(client_stream); loop { tokio::select! { @@ -185,7 +184,7 @@ async fn handle_connection( msg = server_to_clients_recv.recv() => { match msg { Ok(msg) => { - client_stream.write_all(msg.as_bytes()).await?; + client_stream.send(msg.into()).await?; }, Err(e) => { info!("Error reading message: {}", e); @@ -197,9 +196,9 @@ async fn handle_connection( // Forwards messages from the IPC clients to the server // Note that we also send connect and disconnect events so that // the server can keep track of multiple clients - result = client_stream.read(&mut buf) => { + result = client_stream.next() => { match result { - Err(e) => { + Some(Err(e)) => { info!("Error reading from client {client_id}: {e}"); client_to_server_send.send(Message { @@ -209,7 +208,7 @@ async fn handle_connection( }).await?; break; }, - Ok(0) => { + None => { info!("Client {client_id} disconnected."); client_to_server_send.send(Message { @@ -219,8 +218,8 @@ async fn handle_connection( }).await?; break; }, - Ok(size) => { - let msg = std::str::from_utf8(&buf[..size])?; + Some(Ok(bytes)) => { + let msg = std::str::from_utf8(&bytes)?; client_to_server_send.send(Message { client_id, 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 a3a9c929159..015a7c6b21b 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -111,9 +111,19 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On ) { this.cipher = 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 - super.load(); + + await super.load(); + + if (!this.editMode || this.cloneMode) { + // Creating an ssh key directly while filtering to the ssh key category + // must force a key to be set. SSH keys must never be created with an empty private key field + if ( + this.cipher.type === CipherType.SshKey && + (this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "") + ) { + await this.generateSshKey(false); + } + } } onWindowHidden() { @@ -145,16 +155,19 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On ); } - async generateSshKey() { + async generateSshKey(showNotification: boolean = true) { const sshKey = await ipc.platform.sshAgent.generateKey("ed25519"); this.cipher.sshKey.privateKey = sshKey.privateKey; this.cipher.sshKey.publicKey = sshKey.publicKey; this.cipher.sshKey.keyFingerprint = sshKey.keyFingerprint; - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("sshKeyGenerated"), - }); + + if (showNotification) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } } async importSshKeyFromClipboard() { diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html index a31679ce484..fcf03d27acc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html @@ -23,34 +23,52 @@
{{ "minLength" | i18n }} - +
{{ "minNumbers" | i18n }} - + {{ "minSpecial" | i18n }} - +
- A-Z + {{ "uppercaseLabel" | i18n }} - a-z + {{ "lowercaseLabel" | i18n }} - 0-9 + {{ "numbersLabel" | i18n }} - !@#$%^&* + {{ "specialCharactersLabel" | i18n }} @@ -60,7 +78,13 @@
{{ "minimumNumberOfWords" | i18n }} - +
diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts index 9876876b295..818a0853ad3 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts @@ -21,39 +21,29 @@ export class PasswordGeneratorPolicy extends BasePolicy { templateUrl: "password-generator.component.html", }) export class PasswordGeneratorPolicyComponent extends BasePolicyComponent { + // these properties forward the application default settings to the UI + // for HTML attribute bindings + protected readonly minLengthMin = Generators.password.settings.constraints.length.min; + protected readonly minLengthMax = Generators.password.settings.constraints.length.max; + protected readonly minNumbersMin = Generators.password.settings.constraints.minNumber.min; + protected readonly minNumbersMax = Generators.password.settings.constraints.minNumber.max; + protected readonly minSpecialMin = Generators.password.settings.constraints.minSpecial.min; + protected readonly minSpecialMax = Generators.password.settings.constraints.minSpecial.max; + protected readonly minNumberWordsMin = Generators.passphrase.settings.constraints.numWords.min; + protected readonly minNumberWordsMax = Generators.passphrase.settings.constraints.numWords.max; + data = this.formBuilder.group({ overridePasswordType: [null], - minLength: [ - null, - [ - Validators.min(Generators.password.settings.constraints.length.min), - Validators.max(Generators.password.settings.constraints.length.max), - ], - ], + minLength: [null, [Validators.min(this.minLengthMin), Validators.max(this.minLengthMax)]], useUpper: [null], useLower: [null], useNumbers: [null], useSpecial: [null], - minNumbers: [ - null, - [ - Validators.min(Generators.password.settings.constraints.minNumber.min), - Validators.max(Generators.password.settings.constraints.minNumber.max), - ], - ], - minSpecial: [ - null, - [ - Validators.min(Generators.password.settings.constraints.minSpecial.min), - Validators.max(Generators.password.settings.constraints.minSpecial.max), - ], - ], + minNumbers: [null, [Validators.min(this.minNumbersMin), Validators.max(this.minNumbersMax)]], + minSpecial: [null, [Validators.min(this.minSpecialMin), Validators.max(this.minSpecialMax)]], minNumberWords: [ null, - [ - Validators.min(Generators.passphrase.settings.constraints.numWords.min), - Validators.max(Generators.passphrase.settings.constraints.numWords.max), - ], + [Validators.min(this.minNumberWordsMin), Validators.max(this.minNumberWordsMax)], ], capitalize: [null], includeNumber: [null], diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 614401e8708..20361c7edc2 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -112,7 +112,7 @@ const routes: Routes = [ path: "register", component: TrialInitiationComponent, canActivate: [ - canAccessFeature(FeatureFlag.EmailVerification, false, "/signup"), + canAccessFeature(FeatureFlag.EmailVerification, false, "/signup", false), unauthGuardFn(), ], data: { titleId: "createAccount" } satisfies RouteDataProperties, diff --git a/libs/angular/src/platform/guard/feature-flag.guard.ts b/libs/angular/src/platform/guard/feature-flag.guard.ts index 1f82d810e36..65a29e4d8d8 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.ts @@ -16,11 +16,13 @@ type FlagValue = boolean | number | string; * @param featureFlag - The feature flag to check * @param requiredFlagValue - Optional value to the feature flag must be equal to, defaults to true * @param redirectUrlOnDisabled - Optional url to redirect to if the feature flag is disabled + * @param showToast - Optional boolean to show a toast if the feature flag is disabled - defaults to true */ export const canAccessFeature = ( featureFlag: FeatureFlag, requiredFlagValue: FlagValue = true, redirectUrlOnDisabled?: string, + showToast = true, ): CanActivateFn => { return async () => { const configService = inject(ConfigService); @@ -36,11 +38,13 @@ export const canAccessFeature = ( return true; } - toastService.showToast({ - variant: "error", - title: null, - message: i18nService.t("accessDenied"), - }); + if (showToast) { + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accessDenied"), + }); + } if (redirectUrlOnDisabled != null) { return router.createUrlTree([redirectUrlOnDisabled]); diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index 4fedb1f8a36..9f0cc8abb88 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -17,6 +17,10 @@ export class SshKeyView extends ItemView { } get maskedPrivateKey(): string { + if (!this.privateKey || this.privateKey.length === 0) { + return ""; + } + let lines = this.privateKey.split("\n").filter((l) => l.trim() !== ""); lines = lines.map((l, i) => { if (i === 0 || i === lines.length - 1) {