1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-9473] Add messaging for macOS passkey extension and desktop (#10768)

* Add messaging for macos passkey provider

* fix: credential id conversion

* Make build.sh executable

Co-authored-by: Colton Hurst <colton@coltonhurst.com>

* chore: add TODO

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
This commit is contained in:
Daniel García
2024-12-19 09:00:21 +01:00
committed by GitHub
parent 456046e095
commit 51f6594d4b
37 changed files with 1935 additions and 149 deletions

View File

@@ -25,6 +25,7 @@ import { BrowserScriptInjectorService } from "../../../platform/services/browser
import { AbortManager } from "../../../vault/background/abort-manager"; import { AbortManager } from "../../../vault/background/abort-manager";
import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum";
import { Fido2PortName } from "../enums/fido2-port-name.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum";
import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service";
import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; import { Fido2ExtensionMessage } from "./abstractions/fido2.background";
import { Fido2Background } from "./fido2.background"; import { Fido2Background } from "./fido2.background";
@@ -56,7 +57,7 @@ describe("Fido2Background", () => {
let senderMock!: MockProxy<chrome.runtime.MessageSender>; let senderMock!: MockProxy<chrome.runtime.MessageSender>;
let logService!: MockProxy<LogService>; let logService!: MockProxy<LogService>;
let fido2ActiveRequestManager: MockProxy<Fido2ActiveRequestManager>; let fido2ActiveRequestManager: MockProxy<Fido2ActiveRequestManager>;
let fido2ClientService!: MockProxy<Fido2ClientService>; let fido2ClientService!: MockProxy<Fido2ClientService<BrowserFido2ParentWindowReference>>;
let vaultSettingsService!: MockProxy<VaultSettingsService>; let vaultSettingsService!: MockProxy<VaultSettingsService>;
let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>; let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>;
let configServiceMock!: MockProxy<ConfigService>; let configServiceMock!: MockProxy<ConfigService>;
@@ -73,7 +74,7 @@ describe("Fido2Background", () => {
}); });
senderMock = mock<chrome.runtime.MessageSender>({ id: "1", tab: tabMock }); senderMock = mock<chrome.runtime.MessageSender>({ id: "1", tab: tabMock });
logService = mock<LogService>(); logService = mock<LogService>();
fido2ClientService = mock<Fido2ClientService>(); fido2ClientService = mock<Fido2ClientService<BrowserFido2ParentWindowReference>>();
vaultSettingsService = mock<VaultSettingsService>(); vaultSettingsService = mock<VaultSettingsService>();
abortManagerMock = mock<AbortManager>(); abortManagerMock = mock<AbortManager>();
abortController = mock<AbortController>(); abortController = mock<AbortController>();

View File

@@ -23,10 +23,11 @@ import { ScriptInjectorService } from "../../../platform/services/abstractions/s
import { AbortManager } from "../../../vault/background/abort-manager"; import { AbortManager } from "../../../vault/background/abort-manager";
import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum";
import { Fido2PortName } from "../enums/fido2-port-name.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum";
import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service";
import { import {
Fido2Background as Fido2BackgroundInterface,
Fido2BackgroundExtensionMessageHandlers, Fido2BackgroundExtensionMessageHandlers,
Fido2Background as Fido2BackgroundInterface,
Fido2ExtensionMessage, Fido2ExtensionMessage,
SharedFido2ScriptInjectionDetails, SharedFido2ScriptInjectionDetails,
SharedFido2ScriptRegistrationOptions, SharedFido2ScriptRegistrationOptions,
@@ -56,7 +57,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
constructor( constructor(
private logService: LogService, private logService: LogService,
private fido2ActiveRequestManager: Fido2ActiveRequestManager, private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private fido2ClientService: Fido2ClientService, private fido2ClientService: Fido2ClientService<BrowserFido2ParentWindowReference>,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private scriptInjectorService: ScriptInjectorService, private scriptInjectorService: ScriptInjectorService,
private configService: ConfigService, private configService: ConfigService,

View File

@@ -111,11 +111,15 @@ export type BrowserFido2Message = { sessionId: string } & (
} }
); );
export type BrowserFido2ParentWindowReference = chrome.tabs.Tab;
/** /**
* Browser implementation of the {@link Fido2UserInterfaceService}. * Browser implementation of the {@link Fido2UserInterfaceService}.
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it. * The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
*/ */
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { export class BrowserFido2UserInterfaceService
implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>
{
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
async newSession( async newSession(

View File

@@ -201,11 +201,11 @@ import {
ImportServiceAbstraction, ImportServiceAbstraction,
} from "@bitwarden/importer/core"; } from "@bitwarden/importer/core";
import { import {
DefaultKdfConfigService,
KdfConfigService,
BiometricStateService, BiometricStateService,
BiometricsService, BiometricsService,
DefaultBiometricStateService, DefaultBiometricStateService,
DefaultKdfConfigService,
KdfConfigService,
KeyService as KeyServiceAbstraction, KeyService as KeyServiceAbstraction,
} from "@bitwarden/key-management"; } from "@bitwarden/key-management";
import { import {
@@ -232,7 +232,10 @@ import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-ha
import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated";
import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background";
import { Fido2Background } from "../autofill/fido2/background/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background";
import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service"; import {
BrowserFido2ParentWindowReference,
BrowserFido2UserInterfaceService,
} from "../autofill/fido2/services/browser-fido2-user-interface.service";
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
import AutofillService from "../autofill/services/autofill.service"; import AutofillService from "../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service"; import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
@@ -337,10 +340,10 @@ export default class MainBackground {
policyApiService: PolicyApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction;
sendApiService: SendApiServiceAbstraction; sendApiService: SendApiServiceAbstraction;
userVerificationApiService: UserVerificationApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction;
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>;
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<BrowserFido2ParentWindowReference>;
fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction; fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction;
fido2ClientService: Fido2ClientServiceAbstraction; fido2ClientService: Fido2ClientServiceAbstraction<BrowserFido2ParentWindowReference>;
avatarService: AvatarServiceAbstraction; avatarService: AvatarServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler; mainContextMenuHandler: MainContextMenuHandler;
cipherContextMenuHandler: CipherContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler;

View File

@@ -62,12 +62,55 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.10" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.94" version = "1.0.94"
@@ -103,6 +146,47 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "askama"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
dependencies = [
"askama_derive",
"askama_escape",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"serde",
"syn",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.1" version = "0.7.1"
@@ -318,6 +402,15 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "basic-toml"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bcrypt-pbkdf" name = "bcrypt-pbkdf"
version = "0.10.0" version = "0.10.0"
@@ -329,6 +422,15 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
@@ -422,6 +524,38 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]] [[package]]
name = "cbc" name = "cbc"
version = "0.1.2" version = "0.1.2"
@@ -487,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
] ]
[[package]] [[package]]
@@ -495,11 +630,24 @@ version = "4.5.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
dependencies = [ dependencies = [
"anstream",
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim", "strsim",
] ]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.3" version = "0.7.3"
@@ -525,6 +673,12 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -724,6 +878,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@@ -815,6 +982,8 @@ dependencies = [
"napi", "napi",
"napi-build", "napi-build",
"napi-derive", "napi-derive",
"serde",
"serde_json",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
@@ -1035,6 +1204,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
[[package]]
name = "fs-err"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.31"
@@ -1190,12 +1368,35 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "goblin"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.4.0" version = "0.4.0"
@@ -1245,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.15.2",
] ]
[[package]] [[package]]
@@ -1273,6 +1474,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.14" version = "1.0.14"
@@ -1372,6 +1579,21 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "macos_provider"
version = "0.0.0"
dependencies = [
"desktop_core",
"futures",
"log",
"oslog",
"serde",
"serde_json",
"tokio",
"tokio-util",
"uniffi",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@@ -1397,6 +1619,22 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@@ -1811,6 +2049,17 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "oslog"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
dependencies = [
"cc",
"dashmap",
"log",
]
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@@ -1851,6 +2100,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pbkdf2" name = "pbkdf2"
version = "0.12.2" version = "0.12.2"
@@ -1967,6 +2222,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]] [[package]]
name = "polling" name = "polling"
version = "3.7.4" version = "3.7.4"
@@ -2235,6 +2496,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "salsa20" name = "salsa20"
version = "0.10.2" version = "0.10.2"
@@ -2262,6 +2529,26 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "scroll"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "scrypt" name = "scrypt"
version = "0.11.0" version = "0.11.0"
@@ -2301,6 +2588,9 @@ name = "semver"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde" name = "serde"
@@ -2322,6 +2612,18 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_json"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_repr" name = "serde_repr"
version = "0.1.19" version = "0.1.19"
@@ -2391,6 +2693,12 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2406,6 +2714,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.8" version = "0.5.8"
@@ -2544,6 +2858,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -2648,6 +2971,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.8" version = "0.6.8"
@@ -2726,6 +3058,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "unicase"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.14"
@@ -2744,6 +3082,136 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "uniffi"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"clap",
"uniffi_bindgen",
"uniffi_build",
"uniffi_core",
"uniffi_macros",
]
[[package]]
name = "uniffi_bindgen"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f"
dependencies = [
"anyhow",
"askama",
"camino",
"cargo_metadata",
"fs-err",
"glob",
"goblin",
"heck",
"once_cell",
"paste",
"serde",
"textwrap",
"toml",
"uniffi_meta",
"uniffi_udl",
]
[[package]]
name = "uniffi_build"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b"
dependencies = [
"anyhow",
"camino",
"uniffi_bindgen",
]
[[package]]
name = "uniffi_checksum_derive"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "uniffi_core"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f"
dependencies = [
"anyhow",
"bytes",
"log",
"once_cell",
"paste",
"static_assertions",
]
[[package]]
name = "uniffi_macros"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b"
dependencies = [
"bincode",
"camino",
"fs-err",
"once_cell",
"proc-macro2",
"quote",
"serde",
"syn",
"toml",
"uniffi_meta",
]
[[package]]
name = "uniffi_meta"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4"
dependencies = [
"anyhow",
"bytes",
"siphasher",
"uniffi_checksum_derive",
]
[[package]]
name = "uniffi_testing"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"fs-err",
"once_cell",
]
[[package]]
name = "uniffi_udl"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc"
dependencies = [
"anyhow",
"textwrap",
"uniffi_meta",
"uniffi_testing",
"weedle2",
]
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"
@@ -2754,6 +3222,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -2839,6 +3313,15 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "weedle2"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "widestring" name = "widestring"
version = "1.1.0" version = "1.1.0"

View File

@@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["napi", "core", "proxy"] members = ["napi", "core", "proxy", "macos_provider"]

View File

@@ -0,0 +1 @@
BitwardenMacosProviderFFI.xcframework

View File

@@ -0,0 +1,30 @@
[package]
name = "macos_provider"
license = "GPL-3.0"
version = "0.0.0"
edition = "2021"
publish = false
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
[lib]
crate-type = ["staticlib", "cdylib"]
bench = false
[dependencies]
desktop_core = { path = "../core" }
futures = "=0.3.31"
log = "0.4.22"
serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["sync"] }
tokio-util = "0.7.11"
uniffi = { version = "0.28.0", features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
oslog = "0.2.0"
[build-dependencies]
uniffi = { version = "0.28.0", features = ["build"] }

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
rm -r BitwardenMacosProviderFFI.xcframework
rm -r tmp
mkdir -p ./tmp/target/universal-darwin/release/
cargo build --package macos_provider --target aarch64-apple-darwin --release
cargo build --package macos_provider --target x86_64-apple-darwin --release
# Create universal libraries
lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \
../target/x86_64-apple-darwin/release/libmacos_provider.a \
-output ./tmp/target/universal-darwin/release/libmacos_provider.a
# Generate swift bindings
cargo run --bin uniffi-bindgen --features uniffi/cli generate \
../target/aarch64-apple-darwin/release/libmacos_provider.dylib \
--library \
--language swift \
--no-format \
--out-dir tmp/bindings
# Move generated swift bindings
mkdir -p ../../macos/autofill-extension/
mv ./tmp/bindings/*.swift ../../macos/autofill-extension/
# Massage the generated files to fit xcframework
mkdir tmp/Headers
mv ./tmp/bindings/*.h ./tmp/Headers/
cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap
# Build xcframework
xcodebuild -create-xcframework \
-library ./tmp/target/universal-darwin/release/libmacos_provider.a \
-headers ./tmp/Headers \
-output ./BitwardenMacosProviderFFI.xcframework
# Cleanup temporary files
rm -r tmp

View File

@@ -0,0 +1,46 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, UserVerification};
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
rp_id: String,
credential_id: Vec<u8>,
user_name: String,
user_handle: Vec<u8>,
record_identifier: Option<String>,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
}
#[derive(uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionResponse {
rp_id: String,
user_handle: Vec<u8>,
signature: Vec<u8>,
client_data_hash: Vec<u8>,
authenticator_data: Vec<u8>,
credential_id: Vec<u8>,
}
#[uniffi::export(with_foreign)]
pub trait PreparePasskeyAssertionCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyAssertionResponse);
fn on_error(&self, error: BitwardenError);
}
impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
let credential = serde_json::from_value(credential)?;
PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential);
Ok(())
}
fn error(&self, error: BitwardenError) {
PreparePasskeyAssertionCallback::on_error(self.as_ref(), error);
}
}

View File

@@ -0,0 +1,205 @@
#![cfg(target_os = "macos")]
use std::{
collections::HashMap,
sync::{atomic::AtomicU32, Arc, Mutex},
time::Instant,
};
use futures::FutureExt;
use log::{error, info};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
uniffi::setup_scaffolding!();
mod assertion;
mod registration;
use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback};
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
Preferred,
Required,
Discouraged,
}
#[derive(Debug, uniffi::Error, Serialize, Deserialize)]
pub enum BitwardenError {
Internal(String),
}
// TODO: These have to be named differently than the actual Uniffi traits otherwise
// the generated code will lead to ambiguous trait implementations
// These are only used internally, so it doesn't matter that much
trait Callback: Send + Sync {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>;
fn error(&self, error: BitwardenError);
}
#[derive(uniffi::Object)]
pub struct MacOSProviderClient {
to_server_send: tokio::sync::mpsc::Sender<String>,
// We need to keep track of the callbacks so we can call them when we receive a response
response_callbacks_counter: AtomicU32,
#[allow(clippy::type_complexity)]
response_callbacks_queue: Arc<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
}
#[uniffi::export]
impl MacOSProviderClient {
#[uniffi::constructor]
pub fn connect() -> Self {
let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension")
.level_filter(log::LevelFilter::Trace)
.init();
let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32);
let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32);
let client = MacOSProviderClient {
to_server_send,
response_callbacks_counter: AtomicU32::new(0),
response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())),
};
let path = desktop_core::ipc::path("autofill");
let queue = client.response_callbacks_queue.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Can't create runtime");
rt.spawn(
desktop_core::ipc::client::connect(path, from_server_send, to_server_recv)
.map(|r| r.map_err(|e| e.to_string())),
);
rt.block_on(async move {
while let Some(message) = from_server_recv.recv().await {
match serde_json::from_str::<SerializedMessage>(&message) {
Ok(SerializedMessage::Command(CommandMessage::Connected)) => {
info!("Connected to server");
}
Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => {
info!("Disconnected from server");
}
Ok(SerializedMessage::Message {
sequence_number,
value,
}) => match queue.lock().unwrap().remove(&sequence_number) {
Some((cb, request_start_time)) => {
info!(
"Time to process request: {:?}",
request_start_time.elapsed()
);
match value {
Ok(value) => {
if let Err(e) = cb.complete(value) {
error!("Error deserializing message: {e}");
}
}
Err(e) => {
error!("Error processing message: {e:?}");
cb.error(e)
}
}
}
None => {
error!("No callback found for sequence number: {sequence_number}")
}
},
Err(e) => {
error!("Error deserializing message: {e}");
}
};
}
});
});
client
}
pub fn prepare_passkey_registration(
&self,
request: PasskeyRegistrationRequest,
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
) {
self.send_message(request, Box::new(callback));
}
pub fn prepare_passkey_assertion(
&self,
request: PasskeyAssertionRequest,
callback: Arc<dyn PreparePasskeyAssertionCallback>,
) {
self.send_message(request, Box::new(callback));
}
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "camelCase")]
enum CommandMessage {
Connected,
Disconnected,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")]
enum SerializedMessage {
Command(CommandMessage),
Message {
sequence_number: u32,
value: Result<serde_json::Value, BitwardenError>,
},
}
impl MacOSProviderClient {
fn add_callback(&self, callback: Box<dyn Callback>) -> u32 {
let sequence_number = self
.response_callbacks_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.response_callbacks_queue
.lock()
.unwrap()
.insert(sequence_number, (callback, Instant::now()));
sequence_number
}
fn send_message(
&self,
message: impl Serialize + DeserializeOwned,
callback: Box<dyn Callback>,
) {
let sequence_number = self.add_callback(callback);
let message = serde_json::to_string(&SerializedMessage::Message {
sequence_number,
value: Ok(serde_json::to_value(message).unwrap()),
})
.expect("Can't serialize message");
if let Err(e) = self.to_server_send.blocking_send(message) {
// Make sure we remove the callback from the queue if we can't send the message
if let Some((cb, _)) = self
.response_callbacks_queue
.lock()
.unwrap()
.remove(&sequence_number)
{
cb.error(BitwardenError::Internal(format!(
"Error sending message: {}",
e
)));
}
}
}
}

View File

@@ -0,0 +1,43 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{BitwardenError, Callback, UserVerification};
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
rp_id: String,
user_name: String,
user_handle: Vec<u8>,
client_data_hash: Vec<u8>,
user_verification: UserVerification,
supported_algorithms: Vec<i32>,
}
#[derive(uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
rp_id: String,
client_data_hash: Vec<u8>,
credential_id: Vec<u8>,
attestation_object: Vec<u8>,
}
#[uniffi::export(with_foreign)]
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
fn on_complete(&self, credential: PasskeyRegistrationResponse);
fn on_error(&self, error: BitwardenError);
}
impl Callback for Arc<dyn PreparePasskeyRegistrationCallback> {
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
let credential = serde_json::from_value(credential)?;
PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential);
Ok(())
}
fn error(&self, error: BitwardenError) {
PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error);
}
}

View File

@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}

View File

@@ -0,0 +1,4 @@
[bindings.swift]
ffi_module_name = "BitwardenMacosProviderFFI"
module_name = "BitwardenMacosProvider"
generate_immutable_records = true

View File

@@ -20,6 +20,8 @@ anyhow = "=1.0.94"
desktop_core = { path = "../core" } desktop_core = { path = "../core" }
napi = { version = "=2.16.13", features = ["async"] } napi = { version = "=2.16.13", features = ["async"] }
napi-derive = "=2.16.13" napi-derive = "=2.16.13"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "=1.41.1" } tokio = { version = "=1.41.1" }
tokio-util = "=0.7.12" tokio-util = "=0.7.12"
tokio-stream = "=0.1.15" tokio-stream = "=0.1.15"

View File

@@ -124,6 +124,58 @@ export declare namespace ipc {
} }
export declare namespace autofill { export declare namespace autofill {
export function runCommand(value: string): Promise<string> export function runCommand(value: string): Promise<string>
export const enum UserVerification {
Preferred = 'preferred',
Required = 'required',
Discouraged = 'discouraged'
}
export interface PasskeyRegistrationRequest {
rpId: string
userName: string
userHandle: Array<number>
clientDataHash: Array<number>
userVerification: UserVerification
supportedAlgorithms: Array<number>
}
export interface PasskeyRegistrationResponse {
rpId: string
clientDataHash: Array<number>
credentialId: Array<number>
attestationObject: Array<number>
}
export interface PasskeyAssertionRequest {
rpId: string
credentialId: Array<number>
userName: string
userHandle: Array<number>
recordIdentifier?: string
clientDataHash: Array<number>
userVerification: UserVerification
}
export interface PasskeyAssertionResponse {
rpId: string
userHandle: Array<number>
signature: Array<number>
clientDataHash: Array<number>
authenticatorData: Array<number>
credentialId: Array<number>
}
export class IpcServer {
/**
* Create and start the IPC server without blocking.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
* @param callback This function will be called whenever a message is received from a client.
*/
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise<IpcServer>
/** Return the path to the IPC server. */
getPath(): string
/** Stop the IPC server. */
stop(): void
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
completeError(clientId: number, sequenceNumber: number, error: string): number
}
} }
export declare namespace crypto { export declare namespace crypto {
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer> export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer>

View File

@@ -545,12 +545,256 @@ pub mod ipc {
#[napi] #[napi]
pub mod autofill { pub mod autofill {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[napi] #[napi]
pub async fn run_command(value: String) -> napi::Result<String> { pub async fn run_command(value: String) -> napi::Result<String> {
desktop_core::autofill::run_command(value) desktop_core::autofill::run_command(value)
.await .await
.map_err(|e| napi::Error::from_reason(e.to_string())) .map_err(|e| napi::Error::from_reason(e.to_string()))
} }
#[derive(Debug, serde::Serialize, serde:: Deserialize)]
pub enum BitwardenError {
Internal(String),
}
#[napi(string_enum)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
#[napi(value = "preferred")]
Preferred,
#[napi(value = "required")]
Required,
#[napi(value = "discouraged")]
Discouraged,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize + DeserializeOwned")]
pub struct PasskeyMessage<T: Serialize + DeserializeOwned> {
pub sequence_number: u32,
pub value: Result<T, BitwardenError>,
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub user_name: String,
pub user_handle: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub supported_algorithms: Vec<i32>,
}
#[napi(object)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub credential_id: Vec<u8>,
pub attestation_object: Vec<u8>,
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
pub rp_id: String,
pub credential_id: Vec<u8>,
pub user_name: String,
pub user_handle: Vec<u8>,
pub record_identifier: Option<String>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
}
#[napi(object)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionResponse {
pub rp_id: String,
pub user_handle: Vec<u8>,
pub signature: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub authenticator_data: Vec<u8>,
pub credential_id: Vec<u8>,
}
#[napi]
pub struct IpcServer {
server: desktop_core::ipc::server::Server,
}
#[napi]
impl IpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
#[napi(factory)]
pub async fn listen(
name: String,
// Ideally we'd have a single callback that has an enum containing the request values,
// but NAPI doesn't support that just yet
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
)]
registration_callback: ThreadsafeFunction<
(u32, u32, PasskeyRegistrationRequest),
ErrorStrategy::CalleeHandled,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
)]
assertion_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionRequest),
ErrorStrategy::CalleeHandled,
>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
while let Some(Message {
client_id,
kind,
message,
}) = recv.recv().await
{
match kind {
// TODO: We're ignoring the connection and disconnection messages for now
MessageType::Connected | MessageType::Disconnected => continue,
MessageType::Message => {
let Some(message) = message else {
println!("[ERROR] Message is empty");
continue;
};
match serde_json::from_str::<PasskeyMessage<PasskeyAssertionRequest>>(
&message,
) {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
continue;
}
Err(e) => {
println!("[ERROR] Error deserializing message1: {e}");
}
}
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
&message,
) {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
registration_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
continue;
}
Err(e) => {
println!("[ERROR] Error deserializing message2: {e}");
}
}
println!("[ERROR] Received an unknown message2: {message:?}");
}
}
}
});
let path = desktop_core::ipc::path(&name);
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
napi::Error::from_reason(format!(
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
))
})?;
Ok(IpcServer { server })
}
/// Return the path to the IPC server.
#[napi]
pub fn get_path(&self) -> String {
self.server.path.to_string_lossy().to_string()
}
/// Stop the IPC server.
#[napi]
pub fn stop(&self) -> napi::Result<()> {
self.server.stop();
Ok(())
}
#[napi]
pub fn complete_registration(
&self,
client_id: u32,
sequence_number: u32,
response: PasskeyRegistrationResponse,
) -> napi::Result<u32> {
let message = PasskeyMessage {
sequence_number,
value: Ok(response),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
#[napi]
pub fn complete_assertion(
&self,
client_id: u32,
sequence_number: u32,
response: PasskeyAssertionResponse,
) -> napi::Result<u32> {
let message = PasskeyMessage {
sequence_number,
value: Ok(response),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
#[napi]
pub fn complete_error(
&self,
client_id: u32,
sequence_number: u32,
error: String,
) -> napi::Result<u32> {
let message: PasskeyMessage<()> = PasskeyMessage {
sequence_number,
value: Err(BitwardenError::Internal(error)),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
// TODO: Add a way to send a message to a specific client?
fn send(&self, _client_id: u32, message: String) -> napi::Result<u32> {
self.server
.send(message)
.map_err(|e| {
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
})
// NAPI doesn't support u64 or usize, so we need to convert to u32
.map(|u| u32::try_from(u).unwrap_or_default())
}
}
} }
#[napi] #[napi]

1
apps/desktop/macos/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
BitwardenMacosProvider.swift

View File

@@ -9,8 +9,46 @@ import AuthenticationServices
import os import os
class CredentialProviderViewController: ASCredentialProviderViewController { class CredentialProviderViewController: ASCredentialProviderViewController {
let logger = Logger() let logger: Logger
// There is something a bit strange about the initialization/deinitialization in this class.
// Sometimes deinit won't be called after a request has successfully finished,
// which would leave this class hanging in memory and the IPC connection open.
//
// If instead I make this a static, the deinit gets called correctly after each request.
// I think we still might want a static regardless, to be able to reuse the connection if possible.
static let client: MacOsProviderClient = {
let instance = MacOsProviderClient.connect()
// setup code
return instance
}()
init() {
logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
logger.log("[autofill-extension] initializing extension")
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
logger.log("[autofill-extension] deinitializing extension")
}
@IBAction func cancel(_ sender: AnyObject?) {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
}
@IBAction func passwordSelected(_ sender: AnyObject?) {
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
}
/* /*
Implement this method if your extension supports showing credentials in the QuickType bar. Implement this method if your extension supports showing credentials in the QuickType bar.
When the user selects a credential from your app, this method will be called with the When the user selects a credential from your app, this method will be called with the
@@ -21,7 +59,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
*/ */
// Deprecated
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)")
logger.log("[autofill-extension] user \(credentialIdentity.user)")
logger.log("[autofill-extension] id \(credentialIdentity.recordIdentifier ?? "")")
logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)")
logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)")
// let databaseIsUnlocked = true // let databaseIsUnlocked = true
// if (databaseIsUnlocked) { // if (databaseIsUnlocked) {
let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234")
@@ -31,6 +76,67 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
// } // }
} }
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
if let request = credentialRequest as? ASPasskeyCredentialRequest {
if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity {
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)")
class CallbackImpl: PreparePasskeyAssertionCallback {
let ctx: ASCredentialProviderExtensionContext
required init(_ ctx: ASCredentialProviderExtensionContext) {
self.ctx = ctx
}
func onComplete(credential: PasskeyAssertionResponse) {
ctx.completeAssertionRequest(using: ASPasskeyAssertionCredential(
userHandle: credential.userHandle,
relyingParty: credential.rpId,
signature: credential.signature,
clientDataHash: credential.clientDataHash,
authenticatorData: credential.authenticatorData,
credentialID: credential.credentialId
))
}
func onError(error: BitwardenError) {
ctx.cancelRequest(withError: error)
}
}
let userVerification = switch request.userVerificationPreference {
case .preferred:
UserVerification.preferred
case .required:
UserVerification.required
default:
UserVerification.discouraged
}
let req = PasskeyAssertionRequest(
rpId: passkeyIdentity.relyingPartyIdentifier,
credentialId: passkeyIdentity.credentialID,
userName: passkeyIdentity.userName,
userHandle: passkeyIdentity.userHandle,
recordIdentifier: passkeyIdentity.recordIdentifier,
clientDataHash: request.clientDataHash,
userVerification: userVerification
)
CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
return
}
}
if let request = credentialRequest as? ASPasswordCredentialRequest {
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)")
return;
}
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong")
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request"))
}
/* /*
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
@@ -41,34 +147,65 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
} }
*/ */
@IBAction func cancel(_ sender: AnyObject?) {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
}
@IBAction func passwordSelected(_ sender: AnyObject?) {
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
}
override func prepareInterfaceForExtensionConfiguration() { override func prepareInterfaceForExtensionConfiguration() {
logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called")
} }
override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) {
logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)") if let request = registrationRequest as? ASPasskeyCredentialRequest {
if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity {
class CallbackImpl: PreparePasskeyRegistrationCallback {
let ctx: ASCredentialProviderExtensionContext
required init(_ ctx: ASCredentialProviderExtensionContext) {
self.ctx = ctx
}
// self.extensionContext.cancelRequest(withError: ExampleError.nope) func onComplete(credential: PasskeyRegistrationResponse) {
} ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential(
relyingParty: credential.rpId,
clientDataHash: credential.clientDataHash,
credentialID: credential.credentialId,
attestationObject: credential.attestationObject
))
}
override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { func onError(error: BitwardenError) {
logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)") ctx.cancelRequest(withError: error)
}
}
let userVerification = switch request.userVerificationPreference {
case .preferred:
UserVerification.preferred
case .required:
UserVerification.required
default:
UserVerification.discouraged
}
let req = PasskeyRegistrationRequest(
rpId: passkeyIdentity.relyingPartyIdentifier,
userName: passkeyIdentity.userName,
userHandle: passkeyIdentity.userHandle,
clientDataHash: request.clientDataHash,
userVerification: userVerification,
supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }
)
CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext))
return
}
}
// If we didn't get a passkey, return an error
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request"))
} }
/* /*
Prepare your UI to list available credentials for the user to choose from. The items in Prepare your UI to list available credentials for the user to choose from. The items in
'serviceIdentifiers' describe the service the user is logging in to, so your extension can 'serviceIdentifiers' describe the service the user is logging in to, so your extension can
prioritize the most relevant credentials in the list. prioritize the most relevant credentials in the list.
*/ */
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)")
@@ -77,18 +214,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
} }
} }
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)")
}
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) {
logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)")
logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
for serviceIdentifier in serviceIdentifiers { for serviceIdentifier in serviceIdentifiers {
logger.log(" service: \(serviceIdentifier.identifier)") logger.log(" service: \(serviceIdentifier.identifier)")
} }
logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
} }
} }

View File

@@ -6,5 +6,9 @@
<true/> <true/>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -7,12 +7,16 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; };
3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; };
E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; };
E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; };
E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = "<group>"; };
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = "<group>"; };
968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = "<group>"; }; 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = "<group>"; };
E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
@@ -28,6 +32,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */, E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */,
3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -56,6 +61,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */, E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */,
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -63,6 +69,7 @@
E1DF71402B342F6900F29026 /* autofill-extension */ = { E1DF71402B342F6900F29026 /* autofill-extension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */,
E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */,
E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */,
E1DF71462B342F6900F29026 /* Info.plist */, E1DF71462B342F6900F29026 /* Info.plist */,
@@ -140,6 +147,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */,
E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */, E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@@ -23,7 +23,7 @@
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
"build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch",
"build:macos-extension": "node scripts/build-macos-extension.js", "build:macos-extension": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js",
"build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js",
"build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js",
"build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch",

View File

@@ -56,6 +56,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@@ -73,6 +75,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection // eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
@@ -80,6 +83,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -99,6 +103,7 @@ import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
import { flagEnabled } from "../../platform/flags"; import { flagEnabled } from "../../platform/flags";
import { DesktopFido2UserInterfaceService } from "../../platform/services/desktop-fido2-user-interface.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { ElectronKeyService } from "../../platform/services/electron-key.service"; import { ElectronKeyService } from "../../platform/services/electron-key.service";
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
@@ -309,7 +314,29 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: DesktopAutofillService, provide: DesktopAutofillService,
deps: [LogService, CipherServiceAbstraction, ConfigService], deps: [
LogService,
CipherServiceAbstraction,
ConfigService,
Fido2AuthenticatorServiceAbstraction,
AccountService,
],
}),
safeProvider({
provide: Fido2UserInterfaceServiceAbstraction,
useClass: DesktopFido2UserInterfaceService,
deps: [AuthServiceAbstraction, CipherServiceAbstraction, AccountService, LogService],
}),
safeProvider({
provide: Fido2AuthenticatorServiceAbstraction,
useClass: Fido2AuthenticatorService,
deps: [
CipherServiceAbstraction,
Fido2UserInterfaceServiceAbstraction,
SyncService,
AccountService,
LogService,
],
}), }),
safeProvider({ safeProvider({
provide: NativeMessagingManifestService, provide: NativeMessagingManifestService,

View File

@@ -1,9 +1,92 @@
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import type { autofill } from "@bitwarden/desktop-napi";
import { Command } from "../platform/main/autofill/command"; import { Command } from "../platform/main/autofill/command";
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
export default { export default {
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> => runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
ipcRenderer.invoke("autofill.runCommand", params), ipcRenderer.invoke("autofill.runCommand", params),
listenPasskeyRegistration: (
fn: (
clientId: number,
sequenceNumber: number,
request: autofill.PasskeyRegistrationRequest,
completeCallback: (
error: Error | null,
response: autofill.PasskeyRegistrationResponse,
) => void,
) => void,
) => {
ipcRenderer.on(
"autofill.passkeyRegistration",
(
event,
data: {
clientId: number;
sequenceNumber: number;
request: autofill.PasskeyRegistrationRequest;
},
) => {
const { clientId, sequenceNumber, request } = data;
fn(clientId, sequenceNumber, request, (error, response) => {
if (error) {
ipcRenderer.send("autofill.completeError", {
clientId,
sequenceNumber,
error: error.message,
});
return;
}
ipcRenderer.send("autofill.completePasskeyRegistration", {
clientId,
sequenceNumber,
response,
});
});
},
);
},
listenPasskeyAssertion: (
fn: (
clientId: number,
sequenceNumber: number,
request: autofill.PasskeyAssertionRequest,
completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void,
) => void,
) => {
ipcRenderer.on(
"autofill.passkeyAssertion",
(
event,
data: {
clientId: number;
sequenceNumber: number;
request: autofill.PasskeyAssertionRequest;
},
) => {
const { clientId, sequenceNumber, request } = data;
fn(clientId, sequenceNumber, request, (error, response) => {
if (error) {
ipcRenderer.send("autofill.completeError", {
clientId,
sequenceNumber,
error: error.message,
});
return;
}
ipcRenderer.send("autofill.completePasskeyAssertion", {
clientId,
sequenceNumber,
response,
});
});
},
);
},
}; };

View File

@@ -1,12 +1,32 @@
import { Injectable, OnDestroy } from "@angular/core"; import { Injectable, OnDestroy } from "@angular/core";
import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs"; import { autofill } from "desktop_native/napi";
import {
EMPTY,
Subject,
distinctUntilChanged,
firstValueFrom,
map,
mergeMap,
switchMap,
takeUntil,
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
Fido2AuthenticatorGetAssertionParams,
Fido2AuthenticatorGetAssertionResult,
Fido2AuthenticatorMakeCredentialResult,
Fido2AuthenticatorMakeCredentialsParams,
Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -26,6 +46,8 @@ export class DesktopAutofillService implements OnDestroy {
private logService: LogService, private logService: LogService,
private cipherService: CipherService, private cipherService: CipherService,
private configService: ConfigService, private configService: ConfigService,
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<void>,
private accountService: AccountService,
) {} ) {}
async init() { async init() {
@@ -47,6 +69,8 @@ export class DesktopAutofillService implements OnDestroy {
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(); .subscribe();
this.listenIpc();
} }
/** Give metadata about all available credentials in the users vault */ /** Give metadata about all available credentials in the users vault */
@@ -114,6 +138,146 @@ export class DesktopAutofillService implements OnDestroy {
}); });
} }
listenIpc() {
ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => {
this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request);
this.logService.warning(
"listenPasskeyRegistration2",
this.convertRegistrationRequest(request),
);
const controller = new AbortController();
void this.fido2AuthenticatorService
.makeCredential(this.convertRegistrationRequest(request), null, controller)
.then((response) => {
callback(null, this.convertRegistrationResponse(request, response));
})
.catch((error) => {
this.logService.error("listenPasskeyRegistration error", error);
callback(error, null);
});
});
ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => {
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
// TODO: For some reason the credentialId is passed as an empty array in the request, so we need to
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
if (request.recordIdentifier && request.credentialId.length === 0) {
const cipher = await this.cipherService.get(request.recordIdentifier);
if (!cipher) {
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
callback(new Error("Cipher not found"), null);
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const decrypted = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const fido2Credential = decrypted.login.fido2Credentials?.[0];
if (!fido2Credential) {
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
callback(new Error("Fido2Credential not found"), null);
return;
}
request.credentialId = Array.from(
guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId),
);
}
const controller = new AbortController();
void this.fido2AuthenticatorService
.getAssertion(this.convertAssertionRequest(request), null, controller)
.then((response) => {
callback(null, this.convertAssertionResponse(request, response));
})
.catch((error) => {
this.logService.error("listenPasskeyAssertion error", error);
callback(error, null);
});
});
}
private convertRegistrationRequest(
request: autofill.PasskeyRegistrationRequest,
): Fido2AuthenticatorMakeCredentialsParams {
return {
hash: new Uint8Array(request.clientDataHash),
rpEntity: {
name: request.rpId,
id: request.rpId,
},
userEntity: {
id: new Uint8Array(request.userHandle),
name: request.userName,
displayName: undefined,
icon: undefined,
},
credTypesAndPubKeyAlgs: request.supportedAlgorithms.map((alg) => ({
alg,
type: "public-key",
})),
excludeCredentialDescriptorList: [],
requireResidentKey: true,
requireUserVerification:
request.userVerification === "required" || request.userVerification === "preferred",
fallbackSupported: false,
};
}
private convertRegistrationResponse(
request: autofill.PasskeyRegistrationRequest,
response: Fido2AuthenticatorMakeCredentialResult,
): autofill.PasskeyRegistrationResponse {
return {
rpId: request.rpId,
clientDataHash: request.clientDataHash,
credentialId: Array.from(Fido2Utils.bufferSourceToUint8Array(response.credentialId)),
attestationObject: Array.from(
Fido2Utils.bufferSourceToUint8Array(response.attestationObject),
),
};
}
private convertAssertionRequest(
request: autofill.PasskeyAssertionRequest,
): Fido2AuthenticatorGetAssertionParams {
return {
rpId: request.rpId,
hash: new Uint8Array(request.clientDataHash),
allowCredentialDescriptorList: [
{
id: new Uint8Array(request.credentialId),
type: "public-key",
},
],
extensions: {},
requireUserVerification:
request.userVerification === "required" || request.userVerification === "preferred",
fallbackSupported: false,
};
}
private convertAssertionResponse(
request: autofill.PasskeyAssertionRequest,
response: Fido2AuthenticatorGetAssertionResult,
): autofill.PasskeyAssertionResponse {
return {
userHandle: Array.from(response.selectedCredential.userHandle),
rpId: request.rpId,
signature: Array.from(response.signature),
clientDataHash: request.clientDataHash,
authenticatorData: Array.from(response.authenticatorData),
credentialId: Array.from(response.selectedCredential.id),
};
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

View File

@@ -261,7 +261,7 @@ export class Main {
new EphemeralValueStorageService(); new EphemeralValueStorageService();
new SSOLocalhostCallbackService(this.environmentService, this.messagingService); new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
this.nativeAutofillMain = new NativeAutofillMain(this.logService); this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain);
void this.nativeAutofillMain.init(); void this.nativeAutofillMain.init();
} }

View File

@@ -3,6 +3,8 @@ import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { autofill } from "@bitwarden/desktop-napi"; import { autofill } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../../main/window.main";
import { CommandDefinition } from "./command"; import { CommandDefinition } from "./command";
export type RunCommandParams<C extends CommandDefinition> = { export type RunCommandParams<C extends CommandDefinition> = {
@@ -14,7 +16,12 @@ export type RunCommandParams<C extends CommandDefinition> = {
export type RunCommandResult<C extends CommandDefinition> = C["output"]; export type RunCommandResult<C extends CommandDefinition> = C["output"];
export class NativeAutofillMain { export class NativeAutofillMain {
constructor(private logService: LogService) {} private ipcServer: autofill.IpcServer | null;
constructor(
private logService: LogService,
private windowMain: WindowMain,
) {}
async init() { async init() {
ipcMain.handle( ipcMain.handle(
@@ -26,6 +33,52 @@ export class NativeAutofillMain {
return this.runCommand(params); return this.runCommand(params);
}, },
); );
this.ipcServer = await autofill.IpcServer.listen(
"autofill",
// RegistrationCallback
(error, clientId, sequenceNumber, request) => {
if (error) {
this.logService.error("autofill.IpcServer.registration", error);
return;
}
this.windowMain.win.webContents.send("autofill.passkeyRegistration", {
clientId,
sequenceNumber,
request,
});
},
// AssertionCallback
(error, clientId, sequenceNumber, request) => {
if (error) {
this.logService.error("autofill.IpcServer.assertion", error);
return;
}
this.windowMain.win.webContents.send("autofill.passkeyAssertion", {
clientId,
sequenceNumber,
request,
});
},
);
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
this.logService.warning("autofill.completePasskeyRegistration", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
});
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
this.logService.warning("autofill.completePasskeyAssertion", data);
const { clientId, sequenceNumber, response } = data;
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
});
ipcMain.on("autofill.completeError", (event, data) => {
this.logService.warning("autofill.completeError", data);
const { clientId, sequenceNumber, error } = data;
this.ipcServer.completeAssertion(clientId, sequenceNumber, error);
});
} }
private async runCommand<C extends CommandDefinition>( private async runCommand<C extends CommandDefinition>(

View File

@@ -0,0 +1,125 @@
import { firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
Fido2UserInterfaceSession,
NewCredentialParams,
PickCredentialParams,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
// TODO: This should be moved to the directory of whatever team takes this on
export class DesktopFido2UserInterfaceService
implements Fido2UserInterfaceServiceAbstraction<void>
{
constructor(
private authService: AuthService,
private cipherService: CipherService,
private accountService: AccountService,
private logService: LogService,
) {}
async newSession(
fallbackSupported: boolean,
_tab: void,
abortController?: AbortController,
): Promise<DesktopFido2UserInterfaceSession> {
this.logService.warning("newSession", fallbackSupported, abortController);
return new DesktopFido2UserInterfaceSession(
this.authService,
this.cipherService,
this.accountService,
this.logService,
);
}
}
export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSession {
constructor(
private authService: AuthService,
private cipherService: CipherService,
private accountService: AccountService,
private logService: LogService,
) {}
async pickCredential({
cipherIds,
userVerification,
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
this.logService.warning("pickCredential", cipherIds, userVerification);
return { cipherId: cipherIds[0], userVerified: userVerification };
}
async confirmNewCredential({
credentialName,
userName,
userVerification,
rpId,
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
this.logService.warning(
"confirmNewCredential",
credentialName,
userName,
userVerification,
rpId,
);
// Store the passkey on a new cipher to avoid replacing something important
const cipher = new CipherView();
cipher.name = credentialName;
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.login.username = userName;
cipher.login.uris = [new LoginUriView()];
cipher.login.uris[0].uri = "https://" + rpId;
cipher.card = new CardView();
cipher.identity = new IdentityView();
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
cipher.reprompt = CipherRepromptType.None;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
const createdCipher = await this.cipherService.createWithServer(encCipher);
return { cipherId: createdCipher.id, userVerified: userVerification };
}
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
this.logService.warning("informExcludedCredential", existingCipherIds);
}
async ensureUnlockedVault(): Promise<void> {
this.logService.warning("ensureUnlockedVault");
const status = await firstValueFrom(this.authService.activeAccountStatus$);
if (status !== AuthenticationStatus.Unlocked) {
throw new Error("Vault is not unlocked");
}
}
async informCredentialNotFound(): Promise<void> {
this.logService.warning("informCredentialNotFound");
}
async close() {
this.logService.warning("close");
}
}

View File

@@ -8,7 +8,7 @@ import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential
* *
* The authenticator provides key management and cryptographic signatures. * The authenticator provides key management and cryptographic signatures.
*/ */
export abstract class Fido2AuthenticatorService { export abstract class Fido2AuthenticatorService<ParentWindowReference> {
/** /**
* Create and save a new credential as described in: * Create and save a new credential as described in:
* https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred * https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred
@@ -19,7 +19,7 @@ export abstract class Fido2AuthenticatorService {
**/ **/
makeCredential: ( makeCredential: (
params: Fido2AuthenticatorMakeCredentialsParams, params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
) => Promise<Fido2AuthenticatorMakeCredentialResult>; ) => Promise<Fido2AuthenticatorMakeCredentialResult>;
@@ -33,7 +33,7 @@ export abstract class Fido2AuthenticatorService {
*/ */
getAssertion: ( getAssertion: (
params: Fido2AuthenticatorGetAssertionParams, params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
) => Promise<Fido2AuthenticatorGetAssertionResult>; ) => Promise<Fido2AuthenticatorGetAssertionResult>;

View File

@@ -15,7 +15,7 @@ export type UserVerification = "discouraged" | "preferred" | "required";
* It is responsible for both marshalling the inputs for the underlying authenticator operations, * It is responsible for both marshalling the inputs for the underlying authenticator operations,
* and for returning the results of the latter operations to the Web Authentication API's callers. * and for returning the results of the latter operations to the Web Authentication API's callers.
*/ */
export abstract class Fido2ClientService { export abstract class Fido2ClientService<ParentWindowReference> {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>; isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
/** /**
@@ -28,7 +28,7 @@ export abstract class Fido2ClientService {
*/ */
createCredential: ( createCredential: (
params: CreateCredentialParams, params: CreateCredentialParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
) => Promise<CreateCredentialResult>; ) => Promise<CreateCredentialResult>;
@@ -43,7 +43,7 @@ export abstract class Fido2ClientService {
*/ */
assertCredential: ( assertCredential: (
params: AssertCredentialParams, params: AssertCredentialParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
) => Promise<AssertCredentialResult>; ) => Promise<AssertCredentialResult>;
} }

View File

@@ -61,7 +61,7 @@ export interface PickCredentialParams {
* The service is session based and is intended to be used by the FIDO2 authenticator to open a window, * The service is session based and is intended to be used by the FIDO2 authenticator to open a window,
* and then use this window to ask the user for input and/or display messages to the user. * and then use this window to ask the user for input and/or display messages to the user.
*/ */
export abstract class Fido2UserInterfaceService { export abstract class Fido2UserInterfaceService<ParentWindowReference> {
/** /**
* Creates a new session. * Creates a new session.
* Note: This will not necessarily open a window until it is needed to request something from the user. * Note: This will not necessarily open a window until it is needed to request something from the user.
@@ -71,7 +71,7 @@ export abstract class Fido2UserInterfaceService {
*/ */
newSession: ( newSession: (
fallbackSupported: boolean, fallbackSupported: boolean,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
) => Promise<Fido2UserInterfaceSession>; ) => Promise<Fido2UserInterfaceSession>;
} }

View File

@@ -30,6 +30,8 @@ import { parseCredentialId } from "./credential-id-utils";
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service"; import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
import { Fido2Utils } from "./fido2-utils"; import { Fido2Utils } from "./fido2-utils";
type ParentWindowReference = string;
const RpId = "bitwarden.com"; const RpId = "bitwarden.com";
describe("FidoAuthenticatorService", () => { describe("FidoAuthenticatorService", () => {
@@ -41,16 +43,16 @@ describe("FidoAuthenticatorService", () => {
}); });
let cipherService!: MockProxy<CipherService>; let cipherService!: MockProxy<CipherService>;
let userInterface!: MockProxy<Fido2UserInterfaceService>; let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>;
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>; let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
let syncService!: MockProxy<SyncService>; let syncService!: MockProxy<SyncService>;
let accountService!: MockProxy<AccountService>; let accountService!: MockProxy<AccountService>;
let authenticator!: Fido2AuthenticatorService; let authenticator!: Fido2AuthenticatorService<ParentWindowReference>;
let tab!: chrome.tabs.Tab; let windowReference!: ParentWindowReference;
beforeEach(async () => { beforeEach(async () => {
cipherService = mock<CipherService>(); cipherService = mock<CipherService>();
userInterface = mock<Fido2UserInterfaceService>(); userInterface = mock<Fido2UserInterfaceService<ParentWindowReference>>();
userInterfaceSession = mock<Fido2UserInterfaceSession>(); userInterfaceSession = mock<Fido2UserInterfaceSession>();
userInterface.newSession.mockResolvedValue(userInterfaceSession); userInterface.newSession.mockResolvedValue(userInterfaceSession);
syncService = mock<SyncService>({ syncService = mock<SyncService>({
@@ -63,7 +65,7 @@ describe("FidoAuthenticatorService", () => {
syncService, syncService,
accountService, accountService,
); );
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; windowReference = Utils.newGuid();
accountService.activeAccount$ = activeAccountSubject; accountService.activeAccount$ = activeAccountSubject;
}); });
@@ -78,19 +80,21 @@ describe("FidoAuthenticatorService", () => {
// Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. // Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation.
it("should throw error when input does not contain any supported algorithms", async () => { it("should throw error when input does not contain any supported algorithms", async () => {
const result = async () => const result = async () =>
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab); await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported);
}); });
it("should throw error when requireResidentKey has invalid value", async () => { it("should throw error when requireResidentKey has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab); const result = async () =>
await authenticator.makeCredential(invalidParams.invalidRk, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });
it("should throw error when requireUserVerification has invalid value", async () => { it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab); const result = async () =>
await authenticator.makeCredential(invalidParams.invalidUv, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });
@@ -103,7 +107,7 @@ describe("FidoAuthenticatorService", () => {
it.skip("should throw error if requireUserVerification is set to true", async () => { it.skip("should throw error if requireUserVerification is set to true", async () => {
const params = await createParams({ requireUserVerification: true }); const params = await createParams({ requireUserVerification: true });
const result = async () => await authenticator.makeCredential(params, tab); const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
}); });
@@ -117,7 +121,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) { for (const p of Object.values(invalidParams)) {
try { try {
await authenticator.makeCredential(p, tab); await authenticator.makeCredential(p, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
} }
@@ -158,7 +162,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue(); userInterfaceSession.informExcludedCredential.mockResolvedValue();
try { try {
await authenticator.makeCredential(params, tab); await authenticator.makeCredential(params, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
@@ -169,7 +173,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error", async () => { it("should throw error", async () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue(); userInterfaceSession.informExcludedCredential.mockResolvedValue();
const result = async () => await authenticator.makeCredential(params, tab); const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
}); });
@@ -180,7 +184,7 @@ describe("FidoAuthenticatorService", () => {
excludedCipher.organizationId = "someOrganizationId"; excludedCipher.organizationId = "someOrganizationId";
try { try {
await authenticator.makeCredential(params, tab); await authenticator.makeCredential(params, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
@@ -193,7 +197,7 @@ describe("FidoAuthenticatorService", () => {
for (const p of Object.values(invalidParams)) { for (const p of Object.values(invalidParams)) {
try { try {
await authenticator.makeCredential(p, tab); await authenticator.makeCredential(p, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
} }
@@ -230,7 +234,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification, userVerified: userVerification,
}); });
await authenticator.makeCredential(params, tab); await authenticator.makeCredential(params, windowReference);
expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({
credentialName: params.rpEntity.name, credentialName: params.rpEntity.name,
@@ -250,7 +254,7 @@ describe("FidoAuthenticatorService", () => {
}); });
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
await authenticator.makeCredential(params, tab); await authenticator.makeCredential(params, windowReference);
const saved = cipherService.encrypt.mock.lastCall?.[0]; const saved = cipherService.encrypt.mock.lastCall?.[0];
expect(saved).toEqual( expect(saved).toEqual(
@@ -288,7 +292,7 @@ describe("FidoAuthenticatorService", () => {
}); });
const params = await createParams(); const params = await createParams();
const result = async () => await authenticator.makeCredential(params, tab); const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
}); });
@@ -302,7 +306,7 @@ describe("FidoAuthenticatorService", () => {
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password }; const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
const result = async () => await authenticator.makeCredential(params, tab); const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });
@@ -317,7 +321,7 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
const result = async () => await authenticator.makeCredential(params, tab); const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });
@@ -358,7 +362,7 @@ describe("FidoAuthenticatorService", () => {
}); });
it("should return attestation object", async () => { it("should return attestation object", async () => {
const result = await authenticator.makeCredential(params, tab); const result = await authenticator.makeCredential(params, windowReference);
const attestationObject = CBOR.decode( const attestationObject = CBOR.decode(
Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer, Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer,
@@ -455,7 +459,8 @@ describe("FidoAuthenticatorService", () => {
describe("invalid input parameters", () => { describe("invalid input parameters", () => {
it("should throw error when requireUserVerification has invalid value", async () => { it("should throw error when requireUserVerification has invalid value", async () => {
const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab); const result = async () =>
await authenticator.getAssertion(invalidParams.invalidUv, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });
@@ -468,7 +473,7 @@ describe("FidoAuthenticatorService", () => {
it.skip("should throw error if requireUserVerification is set to true", async () => { it.skip("should throw error if requireUserVerification is set to true", async () => {
const params = await createParams({ requireUserVerification: true }); const params = await createParams({ requireUserVerification: true });
const result = async () => await authenticator.getAssertion(params, tab); const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
}); });
@@ -498,7 +503,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue(); userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try { try {
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
@@ -513,7 +518,7 @@ describe("FidoAuthenticatorService", () => {
userInterfaceSession.informCredentialNotFound.mockResolvedValue(); userInterfaceSession.informCredentialNotFound.mockResolvedValue();
try { try {
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch {} } catch {}
@@ -534,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
/** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */ /** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */
it("should throw error", async () => { it("should throw error", async () => {
const result = async () => await authenticator.getAssertion(params, tab); const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
}); });
@@ -573,7 +578,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false, userVerified: false,
}); });
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id), cipherIds: ciphers.map((c) => c.id),
@@ -590,7 +595,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false, userVerified: false,
}); });
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: [discoverableCiphers[0].id], cipherIds: [discoverableCiphers[0].id],
@@ -608,7 +613,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: userVerification, userVerified: userVerification,
}); });
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
cipherIds: ciphers.map((c) => c.id), cipherIds: ciphers.map((c) => c.id),
@@ -625,7 +630,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false, userVerified: false,
}); });
const result = async () => await authenticator.getAssertion(params, tab); const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
}); });
@@ -637,7 +642,7 @@ describe("FidoAuthenticatorService", () => {
userVerified: false, userVerified: false,
}); });
const result = async () => await authenticator.getAssertion(params, tab); const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
}); });
@@ -686,7 +691,7 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encrypted as any); cipherService.encrypt.mockResolvedValue(encrypted as any);
ciphers[0].login.fido2Credentials[0].counter = 9000; ciphers[0].login.fido2Credentials[0].counter = 9000;
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
expect(cipherService.encrypt).toHaveBeenCalledWith( expect(cipherService.encrypt).toHaveBeenCalledWith(
@@ -710,13 +715,13 @@ describe("FidoAuthenticatorService", () => {
cipherService.encrypt.mockResolvedValue(encrypted as any); cipherService.encrypt.mockResolvedValue(encrypted as any);
ciphers[0].login.fido2Credentials[0].counter = 0; ciphers[0].login.fido2Credentials[0].counter = 0;
await authenticator.getAssertion(params, tab); await authenticator.getAssertion(params, windowReference);
expect(cipherService.updateWithServer).not.toHaveBeenCalled(); expect(cipherService.updateWithServer).not.toHaveBeenCalled();
}); });
it("should return an assertion result", async () => { it("should return an assertion result", async () => {
const result = await authenticator.getAssertion(params, tab); const result = await authenticator.getAssertion(params, windowReference);
const encAuthData = result.authenticatorData; const encAuthData = result.authenticatorData;
const rpIdHash = encAuthData.slice(0, 32); const rpIdHash = encAuthData.slice(0, 32);
@@ -757,7 +762,7 @@ describe("FidoAuthenticatorService", () => {
for (let i = 0; i < 10; ++i) { for (let i = 0; i < 10; ++i) {
await init(); // Reset inputs await init(); // Reset inputs
const result = await authenticator.getAssertion(params, tab); const result = await authenticator.getAssertion(params, windowReference);
const counter = result.authenticatorData.slice(33, 37); const counter = result.authenticatorData.slice(33, 37);
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
@@ -774,7 +779,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw unkown error if creation fails", async () => { it("should throw unkown error if creation fails", async () => {
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
const result = async () => await authenticator.getAssertion(params, tab); const result = async () => await authenticator.getAssertion(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
}); });

View File

@@ -43,10 +43,12 @@ const KeyUsages: KeyUsage[] = ["sign"];
* *
* It is highly recommended that the W3C specification is used a reference when reading this code. * It is highly recommended that the W3C specification is used a reference when reading this code.
*/ */
export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction { export class Fido2AuthenticatorService<ParentWindowReference>
implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference>
{
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private userInterface: Fido2UserInterfaceService, private userInterface: Fido2UserInterfaceService<ParentWindowReference>,
private syncService: SyncService, private syncService: SyncService,
private accountService: AccountService, private accountService: AccountService,
private logService?: LogService, private logService?: LogService,
@@ -54,12 +56,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async makeCredential( async makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams, params: Fido2AuthenticatorMakeCredentialsParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
): Promise<Fido2AuthenticatorMakeCredentialResult> { ): Promise<Fido2AuthenticatorMakeCredentialResult> {
const userInterfaceSession = await this.userInterface.newSession( const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported, params.fallbackSupported,
tab, window,
abortController, abortController,
); );
@@ -209,12 +211,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
async getAssertion( async getAssertion(
params: Fido2AuthenticatorGetAssertionParams, params: Fido2AuthenticatorGetAssertionParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController?: AbortController, abortController?: AbortController,
): Promise<Fido2AuthenticatorGetAssertionResult> { ): Promise<Fido2AuthenticatorGetAssertionResult> {
const userInterfaceSession = await this.userInterface.newSession( const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported, params.fallbackSupported,
tab, window,
abortController, abortController,
); );
try { try {

View File

@@ -3,6 +3,9 @@
import { CipherType } from "../../../vault/enums"; import { CipherType } from "../../../vault/enums";
import { CipherView } from "../../../vault/models/view/cipher.view"; import { CipherView } from "../../../vault/models/view/cipher.view";
import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view"; import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view";
import { Utils } from "../../misc/utils";
import { parseCredentialId } from "./credential-id-utils";
// TODO: Move into Fido2AuthenticatorService // TODO: Move into Fido2AuthenticatorService
export async function getCredentialsForAutofill( export async function getCredentialsForAutofill(
@@ -15,9 +18,14 @@ export async function getCredentialsForAutofill(
) )
.map((cipher) => { .map((cipher) => {
const credential = cipher.login.fido2Credentials[0]; const credential = cipher.login.fido2Credentials[0];
// Credentials are stored as a GUID or b64 string with `b64.` prepended,
// but we need to return them as a URL-safe base64 string
const credId = Utils.fromBufferToUrlB64(parseCredentialId(credential.credentialId));
return { return {
cipherId: cipher.id, cipherId: cipher.id,
credentialId: credential.credentialId, credentialId: credId,
rpId: credential.rpId, rpId: credential.rpId,
userHandle: credential.userHandle, userHandle: credential.userHandle,
userName: credential.userName, userName: credential.userName,

View File

@@ -32,12 +32,14 @@ import { Fido2ClientService } from "./fido2-client.service";
import { Fido2Utils } from "./fido2-utils"; import { Fido2Utils } from "./fido2-utils";
import { guidToRawFormat } from "./guid-utils"; import { guidToRawFormat } from "./guid-utils";
type ParentWindowReference = string;
const RpId = "bitwarden.com"; const RpId = "bitwarden.com";
const Origin = "https://bitwarden.com"; const Origin = "https://bitwarden.com";
const VaultUrl = "https://vault.bitwarden.com"; const VaultUrl = "https://vault.bitwarden.com";
describe("FidoAuthenticatorService", () => { describe("FidoAuthenticatorService", () => {
let authenticator!: MockProxy<Fido2AuthenticatorService>; let authenticator!: MockProxy<Fido2AuthenticatorService<ParentWindowReference>>;
let configService!: MockProxy<ConfigService>; let configService!: MockProxy<ConfigService>;
let authService!: MockProxy<AuthService>; let authService!: MockProxy<AuthService>;
let vaultSettingsService: MockProxy<VaultSettingsService>; let vaultSettingsService: MockProxy<VaultSettingsService>;
@@ -45,12 +47,12 @@ describe("FidoAuthenticatorService", () => {
let taskSchedulerService: MockProxy<TaskSchedulerService>; let taskSchedulerService: MockProxy<TaskSchedulerService>;
let activeRequest!: MockProxy<ActiveRequest>; let activeRequest!: MockProxy<ActiveRequest>;
let requestManager!: MockProxy<Fido2ActiveRequestManager>; let requestManager!: MockProxy<Fido2ActiveRequestManager>;
let client!: Fido2ClientService; let client!: Fido2ClientService<ParentWindowReference>;
let tab!: chrome.tabs.Tab; let windowReference!: ParentWindowReference;
let isValidRpId!: jest.SpyInstance; let isValidRpId!: jest.SpyInstance;
beforeEach(async () => { beforeEach(async () => {
authenticator = mock<Fido2AuthenticatorService>(); authenticator = mock<Fido2AuthenticatorService<ParentWindowReference>>();
configService = mock<ConfigService>(); configService = mock<ConfigService>();
authService = mock<AuthService>(); authService = mock<AuthService>();
vaultSettingsService = mock<VaultSettingsService>(); vaultSettingsService = mock<VaultSettingsService>();
@@ -82,7 +84,7 @@ describe("FidoAuthenticatorService", () => {
vaultSettingsService.enablePasskeys$ = of(true); vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({}); domainSettingsService.neverDomains$ = of({});
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; windowReference = Utils.newGuid();
}); });
afterEach(() => { afterEach(() => {
@@ -95,7 +97,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if sameOriginWithAncestors is false", async () => { it("should throw error if sameOriginWithAncestors is false", async () => {
const params = createParams({ sameOriginWithAncestors: false }); const params = createParams({ sameOriginWithAncestors: false });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" }); await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -106,7 +108,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw error if user.id is too small", async () => { it("should throw error if user.id is too small", async () => {
const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } }); const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toBeInstanceOf(TypeError); await expect(result).rejects.toBeInstanceOf(TypeError);
}); });
@@ -121,7 +123,7 @@ describe("FidoAuthenticatorService", () => {
}, },
}); });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toBeInstanceOf(TypeError); await expect(result).rejects.toBeInstanceOf(TypeError);
}); });
@@ -136,7 +138,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name", origin: "invalid-domain-name",
}); });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -151,7 +153,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwarden" }, rp: { id: "bitwarden.com", name: "Bitwarden" },
}); });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -165,7 +167,7 @@ describe("FidoAuthenticatorService", () => {
// `params` actually has a valid rp.id, but we're mocking the function to return false // `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false); isValidRpId.mockReturnValue(false);
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -179,7 +181,7 @@ describe("FidoAuthenticatorService", () => {
}); });
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
await expect(result).rejects.toThrow(FallbackRequestedError); await expect(result).rejects.toThrow(FallbackRequestedError);
}); });
@@ -190,7 +192,7 @@ describe("FidoAuthenticatorService", () => {
rp: { id: "bitwarden.com", name: "Bitwarden" }, rp: { id: "bitwarden.com", name: "Bitwarden" },
}); });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -204,7 +206,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab); await client.createCredential(params, windowReference);
}); });
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm. // Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
@@ -216,7 +218,7 @@ describe("FidoAuthenticatorService", () => {
], ],
}); });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotSupportedError" }); await rejects.toMatchObject({ name: "NotSupportedError" });
@@ -231,7 +233,8 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController(); const abortController = new AbortController();
abortController.abort(); abortController.abort();
const result = async () => await client.createCredential(params, tab, abortController); const result = async () =>
await client.createCredential(params, windowReference, abortController);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" }); await rejects.toMatchObject({ name: "AbortError" });
@@ -246,7 +249,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab); await client.createCredential(params, windowReference);
expect(authenticator.makeCredential).toHaveBeenCalledWith( expect(authenticator.makeCredential).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -259,7 +262,7 @@ describe("FidoAuthenticatorService", () => {
displayName: params.user.displayName, displayName: params.user.displayName,
}), }),
}), }),
tab, windowReference,
expect.anything(), expect.anything(),
); );
}); });
@@ -271,7 +274,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab); const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps?.rk).toBe(true); expect(result.extensions.credProps?.rk).toBe(true);
}); });
@@ -283,7 +286,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab); const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps?.rk).toBe(false); expect(result.extensions.credProps?.rk).toBe(false);
}); });
@@ -295,7 +298,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab); const result = await client.createCredential(params, windowReference);
expect(result.extensions.credProps).toBeUndefined(); expect(result.extensions.credProps).toBeUndefined();
}); });
@@ -307,7 +310,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
); );
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" }); await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -319,7 +322,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
authenticator.makeCredential.mockRejectedValue(new Error("unknown error")); authenticator.makeCredential.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" }); await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -330,7 +333,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false); vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -340,7 +343,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -349,7 +352,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl }); const params = createParams({ origin: VaultUrl });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -408,7 +411,7 @@ describe("FidoAuthenticatorService", () => {
origin: "invalid-domain-name", origin: "invalid-domain-name",
}); });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -423,7 +426,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com", rpId: "bitwarden.com",
}); });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -437,7 +440,7 @@ describe("FidoAuthenticatorService", () => {
// `params` actually has a valid rp.id, but we're mocking the function to return false // `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false); isValidRpId.mockReturnValue(false);
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -451,7 +454,7 @@ describe("FidoAuthenticatorService", () => {
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
await expect(result).rejects.toThrow(FallbackRequestedError); await expect(result).rejects.toThrow(FallbackRequestedError);
}); });
@@ -462,7 +465,7 @@ describe("FidoAuthenticatorService", () => {
rpId: "bitwarden.com", rpId: "bitwarden.com",
}); });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" }); await rejects.toMatchObject({ name: "SecurityError" });
@@ -477,7 +480,8 @@ describe("FidoAuthenticatorService", () => {
const abortController = new AbortController(); const abortController = new AbortController();
abortController.abort(); abortController.abort();
const result = async () => await client.assertCredential(params, tab, abortController); const result = async () =>
await client.assertCredential(params, windowReference, abortController);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" }); await rejects.toMatchObject({ name: "AbortError" });
@@ -493,7 +497,7 @@ describe("FidoAuthenticatorService", () => {
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
); );
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" }); await rejects.toMatchObject({ name: "InvalidStateError" });
@@ -505,7 +509,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
authenticator.getAssertion.mockRejectedValue(new Error("unknown error")); authenticator.getAssertion.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" }); await rejects.toMatchObject({ name: "NotAllowedError" });
@@ -516,7 +520,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false); vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -526,7 +530,7 @@ describe("FidoAuthenticatorService", () => {
const params = createParams(); const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -535,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl }); const params = createParams({ origin: VaultUrl });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, windowReference);
const rejects = expect(result).rejects; const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError); await rejects.toThrow(FallbackRequestedError);
@@ -555,7 +559,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledWith( expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -573,7 +577,7 @@ describe("FidoAuthenticatorService", () => {
}), }),
], ],
}), }),
tab, windowReference,
expect.anything(), expect.anything(),
); );
}); });
@@ -585,7 +589,7 @@ describe("FidoAuthenticatorService", () => {
params.rpId = undefined; params.rpId = undefined;
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
}); });
}); });
@@ -597,7 +601,7 @@ describe("FidoAuthenticatorService", () => {
}); });
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledWith( expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -605,7 +609,7 @@ describe("FidoAuthenticatorService", () => {
rpId: RpId, rpId: RpId,
allowCredentialDescriptorList: [], allowCredentialDescriptorList: [],
}), }),
tab, windowReference,
expect.anything(), expect.anything(),
); );
}); });
@@ -627,7 +631,7 @@ describe("FidoAuthenticatorService", () => {
}); });
it("creates an active mediated conditional request", async () => { it("creates an active mediated conditional request", async () => {
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
expect(requestManager.newActiveRequest).toHaveBeenCalled(); expect(requestManager.newActiveRequest).toHaveBeenCalled();
expect(authenticator.getAssertion).toHaveBeenCalledWith( expect(authenticator.getAssertion).toHaveBeenCalledWith(
@@ -635,14 +639,14 @@ describe("FidoAuthenticatorService", () => {
assumeUserPresence: true, assumeUserPresence: true,
rpId: RpId, rpId: RpId,
}), }),
tab, windowReference,
); );
}); });
it("restarts the mediated conditional request if a user aborts the request", async () => { it("restarts the mediated conditional request if a user aborts the request", async () => {
authenticator.getAssertion.mockRejectedValueOnce(new Error()); authenticator.getAssertion.mockRejectedValueOnce(new Error());
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
}); });
@@ -652,7 +656,7 @@ describe("FidoAuthenticatorService", () => {
abortController.abort(); abortController.abort();
authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError")); authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError"));
await client.assertCredential(params, tab); await client.assertCredential(params, windowReference);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
}); });

View File

@@ -47,7 +47,9 @@ import { guidToRawFormat } from "./guid-utils";
* *
* It is highly recommended that the W3C specification is used a reference when reading this code. * It is highly recommended that the W3C specification is used a reference when reading this code.
*/ */
export class Fido2ClientService implements Fido2ClientServiceAbstraction { export class Fido2ClientService<ParentWindowReference>
implements Fido2ClientServiceAbstraction<ParentWindowReference>
{
private timeoutAbortController: AbortController; private timeoutAbortController: AbortController;
private readonly TIMEOUTS = { private readonly TIMEOUTS = {
NO_VERIFICATION: { NO_VERIFICATION: {
@@ -63,7 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
}; };
constructor( constructor(
private authenticator: Fido2AuthenticatorService, private authenticator: Fido2AuthenticatorService<ParentWindowReference>,
private configService: ConfigService, private configService: ConfigService,
private authService: AuthService, private authService: AuthService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
@@ -102,7 +104,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async createCredential( async createCredential(
params: CreateCredentialParams, params: CreateCredentialParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController = new AbortController(), abortController = new AbortController(),
): Promise<CreateCredentialResult> { ): Promise<CreateCredentialResult> {
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
@@ -201,7 +203,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try { try {
makeCredentialResult = await this.authenticator.makeCredential( makeCredentialResult = await this.authenticator.makeCredential(
makeCredentialParams, makeCredentialParams,
tab, window,
abortController, abortController,
); );
} catch (error) { } catch (error) {
@@ -256,7 +258,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
async assertCredential( async assertCredential(
params: AssertCredentialParams, params: AssertCredentialParams,
tab: chrome.tabs.Tab, window: ParentWindowReference,
abortController = new AbortController(), abortController = new AbortController(),
): Promise<AssertCredentialResult> { ): Promise<AssertCredentialResult> {
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
@@ -300,7 +302,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
if (params.mediation === "conditional") { if (params.mediation === "conditional") {
return this.handleMediatedConditionalRequest( return this.handleMediatedConditionalRequest(
params, params,
tab, window,
abortController, abortController,
clientDataJSONBytes, clientDataJSONBytes,
); );
@@ -324,7 +326,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
try { try {
getAssertionResult = await this.authenticator.getAssertion( getAssertionResult = await this.authenticator.getAssertion(
getAssertionParams, getAssertionParams,
tab, window,
abortController, abortController,
); );
} catch (error) { } catch (error) {
@@ -363,7 +365,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private async handleMediatedConditionalRequest( private async handleMediatedConditionalRequest(
params: AssertCredentialParams, params: AssertCredentialParams,
tab: chrome.tabs.Tab, tab: ParentWindowReference,
abortController: AbortController, abortController: AbortController,
clientDataJSONBytes: Uint8Array, clientDataJSONBytes: Uint8Array,
): Promise<AssertCredentialResult> { ): Promise<AssertCredentialResult> {
@@ -379,7 +381,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`, `[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
); );
const requestResult = await this.requestManager.newActiveRequest( const requestResult = await this.requestManager.newActiveRequest(
tab.id, // TODO: This isn't correct, but this.requestManager.newActiveRequest expects a number,
// while this class is currently generic over ParentWindowReference.
// Consider moving requestManager into browser and adding support for ParentWindowReference => tab.id
(tab as any).id,
availableCredentials, availableCredentials,
abortController, abortController,
); );

View File

@@ -7,7 +7,7 @@ import {
* Noop implementation of the {@link Fido2UserInterfaceService}. * Noop implementation of the {@link Fido2UserInterfaceService}.
* This implementation does not provide any user interface. * This implementation does not provide any user interface.
*/ */
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<void> {
newSession(): Promise<Fido2UserInterfaceSession> { newSession(): Promise<Fido2UserInterfaceSession> {
throw new Error("Not implemented exception"); throw new Error("Not implemented exception");
} }