From 52474915e4025dd6fc5c7b9bafe1458a2de1626c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sun, 27 Jul 2025 13:30:25 +0200 Subject: [PATCH] QR login beep --- .../src/angular/debug/debug.component.html | 19 ++ .../auth/src/angular/debug/debug.component.ts | 117 +++++++++++++ libs/auth/src/angular/debug/debug.html | 0 libs/auth/src/angular/index.ts | 1 + .../src/angular/login/login.component.html | 11 ++ .../auth/src/angular/login/login.component.ts | 48 +++++- .../encrypt.service.implementation.spec.ts | 0 .../kdf/services/change-kdf-service.spec.ts | 0 package-lock.json | 162 +++++++++++++++++- package.json | 1 + 10 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 libs/auth/src/angular/debug/debug.component.html create mode 100644 libs/auth/src/angular/debug/debug.component.ts create mode 100644 libs/auth/src/angular/debug/debug.html create mode 100644 libs/common/src/key-management/crypto/services/encrypt.service.implementation.spec.ts create mode 100644 libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts diff --git a/libs/auth/src/angular/debug/debug.component.html b/libs/auth/src/angular/debug/debug.component.html new file mode 100644 index 00000000000..3739a087e67 --- /dev/null +++ b/libs/auth/src/angular/debug/debug.component.html @@ -0,0 +1,19 @@ +
+
+ No QR scanning implemented :( + + + + Connect String + + + + + +
+
diff --git a/libs/auth/src/angular/debug/debug.component.ts b/libs/auth/src/angular/debug/debug.component.ts new file mode 100644 index 00000000000..332a8fe6203 --- /dev/null +++ b/libs/auth/src/angular/debug/debug.component.ts @@ -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 { + const code = "ABCDEFGHIJKLMNOPQRSTUVWXY"; + return new AuthRequest(email, "00000000-0000-0000-0000-000000000000", "placeholder_public", AuthRequestType.AuthenticateAndUnlock, code); + } +} diff --git a/libs/auth/src/angular/debug/debug.html b/libs/auth/src/angular/debug/debug.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index aa0041c7ec3..e61ec1185f5 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -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"; diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index 35ef1fa9b50..ddec3ee632e 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -12,6 +12,17 @@
+ +
+ +
+ {{ "emailAddress" | i18n }} diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index b3509850ac0..ada6fb7bb59 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -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. diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.spec.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts b/libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/package-lock.json b/package-lock.json index e44797997f1..1f3ad52da10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 089ef3342e9..dd89bbfa405 100644 --- a/package.json +++ b/package.json @@ -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",