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:
BIN
apps/browser/rust_noise_bg.wasm
Normal file
BIN
apps/browser/rust_noise_bg.wasm
Normal file
Binary file not shown.
236
apps/browser/src/tools/popup/settings/noise-protocol.service.ts
Normal file
236
apps/browser/src/tools/popup/settings/noise-protocol.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
apps/browser/src/tools/popup/settings/rust_noise.d.ts
vendored
Normal file
129
apps/browser/src/tools/popup/settings/rust_noise.d.ts
vendored
Normal 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>;
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user