1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-01 09:13:54 +00:00

In-progress work transfer commit

This commit is contained in:
Katherine Reynolds
2025-11-17 11:35:40 -08:00
parent a4beaff5d6
commit dc535770b1
6 changed files with 489 additions and 11 deletions

Binary file not shown.

View File

@@ -0,0 +1,236 @@
import { Injectable } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
type KeyPair = { publicKey: Uint8Array; secretKey: Uint8Array };
/**
* Noise Protocol WASM adapter for browser extension
* Wraps the Rust/WASM implementation to provide secure Noise_XXpsk3_25519_AESGCM_SHA256 handshakes
*/
@Injectable({
providedIn: "root",
})
export class NoiseProtocolService {
private wasmModule: any = null;
private isInitialized = false;
constructor(private logService: LogService) {}
/**
* Initialize the WASM module
* Must be called before using any other methods
*/
async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
// Import the WASM module - hardcoded path for demo
// The WASM file will be in the build output directory alongside other assets
const wasmModule = await import("./rust_noise.js");
// Initialize WASM - for --target web, the default export is the init function
if (typeof wasmModule.default === "function") {
// Pass the WASM file path explicitly to avoid import.meta.url issues in bundled code
await wasmModule.default("./rust_noise_bg.wasm");
} else if (typeof wasmModule.init === "function") {
// Fallback for other targets
await wasmModule.init();
}
this.wasmModule = wasmModule;
this.isInitialized = true;
this.logService.info("[NoiseProtocol] WASM module initialized");
} catch (error) {
this.logService.error("[NoiseProtocol] Failed to initialize WASM module:", error);
throw new Error(`Failed to initialize Noise Protocol WASM: ${error}`);
}
}
/**
* Create a new Noise Protocol instance
*/
createProtocol(isInitiator: boolean, staticKeypair?: KeyPair, psk?: Uint8Array): NoiseProtocol {
if (!this.isInitialized || !this.wasmModule) {
throw new Error("NoiseProtocolService not initialized. Call initialize() first.");
}
return new NoiseProtocol(this.wasmModule, isInitiator, staticKeypair, psk, this.logService);
}
/**
* Perform full XXpsk3 handshake (3 messages)
*/
async performXXHandshake(
isInitiator: boolean,
sendMessage: (msg: Uint8Array) => Promise<Uint8Array>,
staticKeypair?: KeyPair,
psk?: Uint8Array,
): Promise<NoiseProtocol> {
await this.initialize();
const noise = this.createProtocol(isInitiator, staticKeypair, psk);
if (isInitiator) {
this.logService.info("[NoiseProtocol] Step 1: Initiator sending message 1 (-> e)");
const msg1 = noise.writeMessage();
const msg2 = await sendMessage(msg1);
this.logService.info(
"[NoiseProtocol] Step 2: Initiator received message 2 (<- e, ee, s, es)",
);
noise.readMessage(msg2);
this.logService.info("[NoiseProtocol] Step 3: Initiator sending message 3 (-> s, se, psk)");
const msg3 = noise.writeMessage();
await sendMessage(msg3);
noise.split();
} else {
this.logService.info("[NoiseProtocol] Responder waiting for messages...");
// Responder waits for messages, handled separately
}
return noise;
}
}
/**
* Noise Protocol wrapper using Rust/WASM implementation
* Implements Noise_XXpsk3_25519_AESGCM_SHA256 pattern
*/
export class NoiseProtocol {
private wasmProtocol: any;
private isInitiator: boolean;
constructor(
wasmModule: any,
isInitiator: boolean,
staticKeypair?: KeyPair,
psk?: Uint8Array,
private logService?: LogService,
) {
this.isInitiator = isInitiator;
try {
// Convert optional parameters
const staticSecretKey = staticKeypair ? staticKeypair.secretKey : null;
const pskBytes = psk ? psk : null;
// Create WASM protocol instance
this.wasmProtocol = new wasmModule.NoiseProtocol(isInitiator, staticSecretKey, pskBytes);
this.log(
`Noise XX handshake initialized (${isInitiator ? "initiator" : "responder"}, PSK: ${psk ? "yes" : "no"})`,
);
} catch (error) {
this.logError("Failed to create WASM NoiseProtocol:", error);
throw error;
}
}
/**
* Write a handshake message
*/
writeMessage(payload?: Uint8Array): Uint8Array {
try {
const payloadArray = payload && payload.length > 0 ? payload : null;
const message = this.wasmProtocol.writeMessage(payloadArray);
this.log(`Sent handshake message (length: ${message.length})`);
return message;
} catch (error) {
this.logError("Failed to write message:", error);
throw error;
}
}
/**
* Read a handshake message
*/
readMessage(message: Uint8Array): Uint8Array {
try {
const payload = this.wasmProtocol.readMessage(message);
this.log(
`Received handshake message (length: ${message.length}, payload: ${payload.length})`,
);
return payload;
} catch (error) {
this.logError("Failed to read message:", error);
throw error;
}
}
/**
* Complete the handshake and derive transport keys
*/
split(): void {
try {
this.wasmProtocol.split();
this.log("Handshake complete - transport keys derived");
} catch (error) {
this.logError("Failed to split:", error);
throw error;
}
}
/**
* Encrypt a message after handshake is complete
*/
encryptMessage(plaintext: Uint8Array): Uint8Array {
try {
const ciphertext = this.wasmProtocol.encryptMessage(plaintext);
this.log(`Message encrypted (length: ${ciphertext.length})`);
return ciphertext;
} catch (error) {
this.logError("Failed to encrypt:", error);
throw error;
}
}
/**
* Decrypt a message after handshake is complete
*/
decryptMessage(ciphertext: Uint8Array): Uint8Array {
try {
const plaintext = this.wasmProtocol.decryptMessage(ciphertext);
this.log(`Message decrypted (length: ${plaintext.length})`);
return plaintext;
} catch (error) {
this.logError("Failed to decrypt:", error);
throw error;
}
}
/**
* Check if handshake is complete
*/
isHandshakeComplete(): boolean {
return this.wasmProtocol.isHandshakeComplete();
}
/**
* Get static public key
* Note: WASM implementation doesn't expose this directly
*/
getStaticPublicKey(): Uint8Array {
throw new Error("getStaticPublicKey not yet implemented for WASM");
}
private log(message: string) {
if (this.logService) {
this.logService.info(`[NoiseProtocol] ${message}`);
}
}
private logError(message: string, error?: any) {
if (this.logService) {
this.logService.error(`[NoiseProtocol] ${message}`, error);
}
}
}

View File

@@ -0,0 +1,129 @@
/* tslint:disable */
/**
* Generate a new Curve25519 keypair
*/
export function generate_keypair(): Keypair;
/**
* Initialize the WASM module
*/
export function init(): void;
/**
* Keypair structure
*/
export class Keypair {
free(): void;
[Symbol.dispose](): void;
constructor(public_key: Uint8Array, secret_key: Uint8Array);
readonly public_key: Uint8Array;
readonly secret_key: Uint8Array;
}
/**
* Noise Protocol state machine
*/
export class NoiseProtocol {
free(): void;
[Symbol.dispose](): void;
/**
* Read a handshake message from the peer
* Returns the payload contained in the message
*/
readMessage(message: Uint8Array): Uint8Array;
/**
* Write a handshake message
* Returns the message to send to the peer
*/
writeMessage(payload?: Uint8Array | null): Uint8Array;
/**
* Decrypt a message (after handshake is complete)
*/
decryptMessage(ciphertext: Uint8Array): Uint8Array;
/**
* Encrypt a message (after handshake is complete)
*/
encryptMessage(plaintext: Uint8Array): Uint8Array;
/**
* Check if handshake is complete
*/
isHandshakeComplete(): boolean;
/**
* Get the remote static public key (available after handshake)
*/
getRemoteStaticPublicKey(): Uint8Array;
/**
* Create a new Noise protocol instance
*
* # Arguments
* * `is_initiator` - Whether this is the initiator (true) or responder (false)
* * `static_keypair` - Optional static keypair (if None, generates new one)
* * `psk` - Optional pre-shared key for additional authentication
*/
constructor(
is_initiator: boolean,
static_secret_key?: Uint8Array | null,
psk?: Uint8Array | null,
);
/**
* Complete the handshake and derive transport keys
*/
split(): void;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly __wbg_keypair_free: (a: number, b: number) => void;
readonly __wbg_noiseprotocol_free: (a: number, b: number) => void;
readonly generate_keypair: (a: number) => void;
readonly init: () => void;
readonly keypair_new: (a: number, b: number, c: number, d: number) => number;
readonly keypair_public_key: (a: number, b: number) => void;
readonly keypair_secret_key: (a: number, b: number) => void;
readonly noiseprotocol_decryptMessage: (a: number, b: number, c: number, d: number) => void;
readonly noiseprotocol_encryptMessage: (a: number, b: number, c: number, d: number) => void;
readonly noiseprotocol_getRemoteStaticPublicKey: (a: number, b: number) => void;
readonly noiseprotocol_isHandshakeComplete: (a: number) => number;
readonly noiseprotocol_new: (
a: number,
b: number,
c: number,
d: number,
e: number,
f: number,
) => void;
readonly noiseprotocol_readMessage: (a: number, b: number, c: number, d: number) => void;
readonly noiseprotocol_split: (a: number, b: number) => void;
readonly noiseprotocol_writeMessage: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_export: (a: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_export2: (a: number, b: number) => number;
readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init(
module_or_path?:
| { module_or_path: InitInput | Promise<InitInput> }
| InitInput
| Promise<InitInput>,
): Promise<InitOutput>;

View File

@@ -16,6 +16,18 @@
<input bitInput type="text" formControlName="tunnelUsername" />
<bit-hint>Enter your username to connect to the remote tunnel.</bit-hint>
</bit-form-field>
<bit-form-field>
<input
type="checkbox"
bitCheckbox
formControlName="useNoiseProtocol"
id="useNoiseProtocol"
/>
<bit-label for="useNoiseProtocol">Use Noise Protocol (XXpsk3) Encryption</bit-label>
<bit-hint
>Enable end-to-end encryption using the Noise Protocol Framework with WASM.</bit-hint
>
</bit-form-field>
<button bitButton buttonType="primary" type="submit" [disabled]="formGroup.invalid">
{{ "demoRetrieve" | i18n }}
</button>

View File

@@ -50,6 +50,7 @@ import { TunnelService } from "./tunnel.service";
export class TunnelDemoComponent {
protected formGroup = this.formBuilder.group({
tunnelUsername: ["", Validators.required],
useNoiseProtocol: [false],
});
constructor(
@@ -125,17 +126,30 @@ export class TunnelDemoComponent {
}
// Send credentials to the localhost tunnel server
try {
await this.tunnelService.sendCredentials({ tunnelUsername, username, password });
const useNoiseProtocol = this.formGroup.value.useNoiseProtocol ?? false;
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Success",
content: `Credentials successfully sent to tunnel server.\n\nUsername:
${username}\nPassword: ${password.replace(/./g, "*")}`,
type: "success",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
try {
if (useNoiseProtocol) {
await this.tunnelService.sendCredentialsWithNoise({ tunnelUsername, username, password });
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Success (Noise Protocol)",
content: `Encrypted credentials successfully sent to tunnel server using Noise Protocol (XXpsk3).\n\nUsername: ${username}\nPassword: ${password.replace(/./g, "*")}`,
type: "success",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
} else {
await this.tunnelService.sendCredentials({ tunnelUsername, username, password });
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Success",
content: `Credentials successfully sent to tunnel server.\n\nUsername: ${username}\nPassword: ${password.replace(/./g, "*")}`,
type: "success",
acceptButtonText: { key: "ok" },
cancelButtonText: null,
});
}
} catch (error) {
await this.dialogService.openSimpleDialog({
title: "Tunnel Demo - Error",

View File

@@ -2,6 +2,8 @@ import { Injectable } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { NoiseProtocolService } from "./noise-protocol.service";
export interface TunnelCredentials {
tunnelUsername: string;
username: string;
@@ -11,6 +13,7 @@ export interface TunnelCredentials {
/**
* Service for securely sending credentials to a localhost tunnel server.
* SECURITY: Only allows connections to loopback addresses (localhost/127.0.0.1/[::1]).
* Uses Noise Protocol (XXpsk3) for end-to-end encryption.
*/
@Injectable({
providedIn: "root",
@@ -20,7 +23,10 @@ export class TunnelService {
private readonly TUNNEL_ENDPOINT = "/bwTunnelDemo";
private readonly ALLOWED_LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
constructor(private logService: LogService) {}
constructor(
private logService: LogService,
private noiseProtocolService: NoiseProtocolService,
) {}
/**
* Sends credentials to the local tunnel server.
@@ -65,6 +71,87 @@ export class TunnelService {
}
}
/**
* Sends credentials to the local tunnel server with Noise Protocol encryption.
* Performs a full XXpsk3 handshake and sends encrypted credentials.
* @param credentials The username and password to send
* @throws Error if the connection is not to a loopback address or if the request fails
*/
async sendCredentialsWithNoise(credentials: TunnelCredentials): Promise<void> {
const url = `http://localhost:${this.TUNNEL_PORT}${this.TUNNEL_ENDPOINT}`;
// Validate that we're only connecting to localhost
const urlObj = new URL(url);
if (!this.isLoopbackAddress(urlObj.hostname)) {
const error = `Security violation: Attempted to connect to non-loopback address: ${urlObj.hostname}`;
this.logService.error(error);
throw new Error("Tunnel service only allows connections to localhost");
}
try {
// Initialize Noise Protocol WASM module
await this.noiseProtocolService.initialize();
// Perform Noise handshake as initiator
const noise = await this.noiseProtocolService.performXXHandshake(
true, // isInitiator
async (message: Uint8Array) => {
// Send handshake message to server
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"X-Noise-Handshake": "true",
},
body: message,
mode: "cors",
credentials: "omit",
cache: "no-store",
});
if (!response.ok) {
throw new Error(
`Tunnel server responded with status ${response.status}: ${response.statusText}`,
);
}
// Return the response message
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
},
);
// Encrypt credentials
const credentialsJson = JSON.stringify(credentials);
const credentialsBytes = new TextEncoder().encode(credentialsJson);
const encryptedCredentials = noise.encryptMessage(credentialsBytes);
// Send encrypted credentials
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"X-Noise-Transport": "true",
},
body: encryptedCredentials,
mode: "cors",
credentials: "omit",
cache: "no-store",
});
if (!response.ok) {
throw new Error(
`Tunnel server responded with status ${response.status}: ${response.statusText}`,
);
}
this.logService.info("Encrypted credentials successfully sent to tunnel server");
} catch (error) {
this.logService.error(`Failed to send encrypted credentials to tunnel server: ${error}`);
throw error;
}
}
/**
* Validates that a hostname is a loopback address.
* @param hostname The hostname to validate