mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Merge branch 'main' into km/test-peercred
This commit is contained in:
4
.github/workflows/build-cli.yml
vendored
4
.github/workflows/build-cli.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/build-desktop.yml
vendored
2
.github/workflows/build-desktop.yml
vendored
@@ -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: |
|
||||
|
||||
6
.github/workflows/deploy-web.yml
vendored
6
.github/workflows/deploy-web.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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::<GenericFilePath>()?;
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: AsyncRead + AsyncWrite>(inner: T) -> Framed<T, LengthDelimitedCodec> {
|
||||
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")]
|
||||
|
||||
@@ -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<Message>,
|
||||
mut server_to_clients_recv: broadcast::Receiver<String>,
|
||||
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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -23,34 +23,52 @@
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
[min]="minLengthMin"
|
||||
[max]="minLengthMax"
|
||||
formControlName="minLength"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
[min]="minNumbersMin"
|
||||
[max]="minNumbersMax"
|
||||
formControlName="minNumbers"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
[min]="minSpecialMin"
|
||||
[max]="minSpecialMax"
|
||||
formControlName="minSpecial"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||
<bit-label>A-Z</bit-label>
|
||||
<bit-label>{{ "uppercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||
<bit-label>a-z</bit-label>
|
||||
<bit-label>{{ "lowercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||
<bit-label>0-9</bit-label>
|
||||
<bit-label>{{ "numbersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
<bit-label>{{ "specialCharactersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +78,13 @@
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="6" max="20" formControlName="minNumberWords" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
[min]="minNumberWordsMin"
|
||||
[max]="minNumberWordsMax"
|
||||
formControlName="minNumberWords"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user