1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-17 18:09:17 +00:00

QR login beep

This commit is contained in:
Bernd Schoolmann
2025-07-27 13:30:25 +02:00
parent a6f1cfd260
commit 52474915e4
10 changed files with 355 additions and 4 deletions

View File

@@ -0,0 +1,19 @@
<form [bitSubmit]="submit" [formGroup]="formGroup">
<div class="tw-grid tw-gap-3">
No QR scanning implemented :(
<!-- Connect String input -->
<bit-form-field>
<bit-label>Connect String</bit-label>
<input
type="text"
formControlName="connectString"
bitInput
placeholder="Enter connect string"
/>
</bit-form-field>
<!-- Approve button -->
<button type="submit" bitButton block buttonType="primary">Approve</button>
</div>
</form>

View File

@@ -0,0 +1,117 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuthRequestApiServiceAbstraction, AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AsyncActionsModule, ButtonModule, CalloutModule, DialogModule, DialogService, FormFieldModule, IconButtonModule } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { ServerRelayInitiator } from "@bitwarden/sdk-internal";
import { I18nPipe } from "@bitwarden/ui-common";
import {
InputPasswordComponent,
} from "../input-password/input-password.component";
@Component({
standalone: true,
selector: "auth-debug",
templateUrl: "debug.component.html",
imports: [
CommonModule,
InputPasswordComponent,
JslibModule,
ButtonModule,
CalloutModule,
CommonModule,
FormFieldModule,
DialogModule,
I18nPipe,
InputPasswordComponent,
DialogModule,
CommonModule,
JslibModule,
ButtonModule,
IconButtonModule,
ReactiveFormsModule,
AsyncActionsModule,
FormFieldModule,
]
})
export class DebugComponent implements OnInit {
protected formGroup = new FormGroup({
connectString: this.formBuilder.control(""),
});
constructor(
private formBuilder: FormBuilder,
private dialogService: DialogService,
private keyService: KeyService,
private accountService: AccountService,
private authRequestApiService: AuthRequestApiServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private apiService: ApiService,
private appIdService: AppIdService,
) { }
async ngOnInit() {
}
submit = async () => {
const a = await this.dialogService.openSimpleDialog({
title: "User verification",
acceptButtonText: "Confirm",
content: "Please confirm with biometrics.",
type: "primary"
});
const connectString = this.formGroup.controls.connectString.value;
if (a) {
const email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((account) => account.email)),
);
const request = await this.buildAuthRequest(email, AuthRequestType.AuthenticateAndUnlock);
const response = await this.authRequestApiService.postAuthRequest(request);
const approveRequest = new PasswordlessAuthRequest(
"7.abc",
undefined,
await this.appIdService.getAppId(),
true,
);
await this.apiService.putAuthRequest(response.id, approveRequest);
const uuid = connectString.split(",")[0];
const psk = Utils.fromB64ToArray(connectString.split(",")[1]);
const initiator = await ServerRelayInitiator.connect(
uuid, psk
);
initiator.send_auth_request(
(await this.keyService.getUserKey()).toEncoded(),
email,
response.id
);
}
}
private async buildAuthRequest(
email: string,
authRequestType: AuthRequestType,
): Promise<AuthRequest> {
const code = "ABCDEFGHIJKLMNOPQRSTUVWXY";
return new AuthRequest(email, "00000000-0000-0000-0000-000000000000", "placeholder_public", AuthRequestType.AuthenticateAndUnlock, code);
}
}

View File

View File

@@ -25,6 +25,7 @@ export * from "./login-decryption-options/default-login-decryption-options.servi
// login via auth request
export * from "./login-via-auth-request/login-via-auth-request.component";
export * from "./debug/debug.component";
// password callout
export * from "./password-callout/password-callout.component";

View File

@@ -12,6 +12,17 @@
<form [bitSubmit]="submit" [formGroup]="formGroup">
<div [ngClass]="{ 'tw-hidden': loginUiState !== LoginUiState.EMAIL_ENTRY }">
<!-- Connect String section -->
<div class="tw-m-3 tw-flex tw-justify-center">
<qrcode
[qrdata]="connectString"
[errorCorrectionLevel]="'M'"
(click)="copyConnectString()"
class="tw-cursor-pointer"
title="Click to copy connect string"
></qrcode>
</div>
<!-- Email Address input -->
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>

View File

@@ -2,10 +2,12 @@ import { CommonModule } from "@angular/common";
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { QRCodeComponent } from 'angularx-qrcode';
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
AuthRequestLoginCredentials,
LoginEmailServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginSuccessHandlerService,
@@ -25,15 +27,18 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { UserKey } from "@bitwarden/common/types/key";
import {
AnonLayoutWrapperDataService,
AsyncActionsModule,
@@ -44,11 +49,13 @@ import {
LinkModule,
ToastService,
} from "@bitwarden/components";
import { PureCrypto, ServerRelayInitiator, ServerRelayResponder, ServerRelayResponderPreHandshake } from "@bitwarden/sdk-internal";
import { VaultIcon, WaveIcon } from "../icons";
import { LoginComponentService, PasswordPolicies } from "./login-component.service";
const BroadcasterSubscriptionId = "LoginComponent";
// FIXME: update to use a const object instead of a typescript enum
@@ -71,6 +78,7 @@ export enum LoginUiState {
JslibModule,
ReactiveFormsModule,
RouterModule,
QRCodeComponent
],
})
export class LoginComponent implements OnInit, OnDestroy {
@@ -104,6 +112,8 @@ export class LoginComponent implements OnInit, OnDestroy {
// Desktop properties
deferFocus: boolean | null = null;
connectString: string = "Connecting...";
constructor(
private activatedRoute: ActivatedRoute,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
@@ -127,6 +137,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private loginSuccessHandlerService: LoginSuccessHandlerService,
private masterPasswordService: MasterPasswordServiceAbstraction,
private configService: ConfigService,
private keyGenerationService: KeyGenerationService,
) {
this.clientType = this.platformUtilsService.getClientType();
}
@@ -137,6 +148,28 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.defaultOnInit();
this.logService.info("Connecting");
(async () => {
const responder = await ServerRelayResponderPreHandshake.listen();
const psk = (await this.keyGenerationService.createKey(256)).toEncoded();
this.connectString = `${await responder.get_id()},${Utils.fromBufferToB64(psk)}`;
const a = await responder.wait_for_handshake(psk);
const auth_request = await a.wait_for_auth_request();
const creds = new AuthRequestLoginCredentials(
auth_request.email(),
"ABCDEFGHIJKLMNOPQRSTUVWXY",
auth_request.auth_request_id(),
new SymmetricCryptoKey(auth_request.userkey()) as UserKey,
null, // no masterKey
null, // no masterKeyHash
);
const resp = await this.loginStrategyService.logIn(creds);
console.log("resp", resp)
await this.loginSuccessHandlerService.run(resp.userId);
await this.router.navigate(["vault"]);
})().then(() => { }).catch(() => { });
if (this.clientType === ClientType.Desktop) {
await this.desktopOnInit();
}
@@ -540,6 +573,19 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loginComponentService.redirectToSsoLogin(email);
}
/**
* Copy the connect string to clipboard when QR code is clicked.
*/
async copyConnectString() {
if (this.connectString) {
await this.platformUtilsService.copyToClipboard(this.connectString);
this.toastService.showToast({
variant: "success",
title: null,
message: "Connect string copied to clipboard",
});
}
}
/**
* Call to check if the device is known.
* Known means that the user has logged in with this device before.

162
package-lock.json generated
View File

@@ -35,6 +35,7 @@
"@nx/eslint": "21.1.2",
"@nx/jest": "21.1.2",
"@nx/js": "21.1.2",
"angularx-qrcode": "19.0.0",
"big-integer": "1.6.52",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.44.0",
@@ -14456,6 +14457,19 @@
"typescript-eslint": "^8.0.0"
}
},
"node_modules/angularx-qrcode": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-19.0.0.tgz",
"integrity": "sha512-uH1gO/X1hgSojZwgO3EmaXP+MvWCgZm5WGh3y1ZL2+VMstEGEMtJGZTyR645fB7ABF2ZIBUMB9h/SKvGJQX/zQ==",
"license": "MIT",
"dependencies": {
"qrcode": "1.5.4",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "^19.0.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -18447,6 +18461,12 @@
"node": ">= 6"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dir-compare": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -31936,6 +31956,15 @@
"node": ">=10.4.0"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/polished": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
@@ -32649,6 +32678,23 @@
],
"license": "MIT"
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode-parser": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/qrcode-parser/-/qrcode-parser-2.1.3.tgz",
@@ -32658,6 +32704,119 @@
"jsqr": "^1.4.0"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/qrcode/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrious": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",
@@ -33282,7 +33441,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true,
"license": "ISC"
},
"node_modules/requireindex": {
@@ -34233,7 +34391,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC"
},
"node_modules/set-cookie-parser": {
@@ -39450,7 +39607,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"dev": true,
"license": "ISC"
},
"node_modules/which-typed-array": {

View File

@@ -170,6 +170,7 @@
"@nx/eslint": "21.1.2",
"@nx/jest": "21.1.2",
"@nx/js": "21.1.2",
"angularx-qrcode": "19.0.0",
"big-integer": "1.6.52",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.44.0",