1
0
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:
Bernd Schoolmann
2024-11-20 09:56:40 -08:00
committed by GitHub
15 changed files with 127 additions and 81 deletions

View File

@@ -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:

View File

@@ -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: |

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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);
}),

View File

@@ -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"

View File

@@ -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?;
}
}

View File

@@ -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")]

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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>!&#64;#$%^&amp;*</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>

View File

@@ -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],

View File

@@ -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,

View File

@@ -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]);

View File

@@ -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) {