mirror of
https://github.com/bitwarden/browser
synced 2026-02-17 09:59:41 +00:00
Merge branch 'feature/passkey-provider' into neuronull/testing-tracing-macos-provider
This commit is contained in:
@@ -37,6 +37,8 @@ export class FakeAccountService implements AccountService {
|
||||
accountActivitySubject = new ReplaySubject<Record<UserId, Date>>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
accountVerifyDevicesSubject = new ReplaySubject<boolean>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
showHeaderSubject = new ReplaySubject<boolean>(1);
|
||||
private _activeUserId: UserId;
|
||||
get activeUserId() {
|
||||
return this._activeUserId;
|
||||
@@ -55,6 +57,7 @@ export class FakeAccountService implements AccountService {
|
||||
}),
|
||||
);
|
||||
}
|
||||
showHeader$ = this.showHeaderSubject.asObservable();
|
||||
get nextUpAccount$(): Observable<Account> {
|
||||
return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe(
|
||||
map(([accounts, activeAccount, sortedUserIds]) => {
|
||||
@@ -114,6 +117,10 @@ export class FakeAccountService implements AccountService {
|
||||
this.accountsSubject.next(updated);
|
||||
await this.mock.clean(userId);
|
||||
}
|
||||
|
||||
async setShowHeader(value: boolean): Promise<void> {
|
||||
this.showHeaderSubject.next(value);
|
||||
}
|
||||
}
|
||||
|
||||
const loggedOutInfo: AccountInfo = {
|
||||
|
||||
@@ -47,6 +47,8 @@ export abstract class AccountService {
|
||||
abstract sortedUserIds$: Observable<UserId[]>;
|
||||
/** Next account that is not the current active account */
|
||||
abstract nextUpAccount$: Observable<Account>;
|
||||
/** Observable to display the header */
|
||||
abstract showHeader$: Observable<boolean>;
|
||||
/**
|
||||
* Updates the `accounts$` observable with the new account data.
|
||||
*
|
||||
@@ -100,6 +102,11 @@ export abstract class AccountService {
|
||||
* @param lastActivity
|
||||
*/
|
||||
abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>;
|
||||
/**
|
||||
* Show the account switcher.
|
||||
* @param value
|
||||
*/
|
||||
abstract setShowHeader(visible: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class InternalAccountService extends AccountService {
|
||||
|
||||
@@ -429,6 +429,16 @@ describe("accountService", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("setShowHeader", () => {
|
||||
it("should update _showHeader$ when setShowHeader is called", async () => {
|
||||
expect(sut["_showHeader$"].value).toBe(true);
|
||||
|
||||
await sut.setShowHeader(false);
|
||||
|
||||
expect(sut["_showHeader$"].value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
distinctUntilChanged,
|
||||
shareReplay,
|
||||
combineLatest,
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
switchMap,
|
||||
filter,
|
||||
@@ -84,6 +85,7 @@ export const getOptionalUserId = map<Account | null, UserId | null>(
|
||||
export class AccountServiceImplementation implements InternalAccountService {
|
||||
private accountsState: GlobalState<Record<UserId, AccountInfo>>;
|
||||
private activeAccountIdState: GlobalState<UserId | undefined>;
|
||||
private _showHeader$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||
activeAccount$: Observable<Account | null>;
|
||||
@@ -91,6 +93,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
accountVerifyNewDeviceLogin$: Observable<boolean>;
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
nextUpAccount$: Observable<Account>;
|
||||
showHeader$ = this._showHeader$.asObservable();
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
@@ -262,6 +265,10 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
async setShowHeader(visible: boolean): Promise<void> {
|
||||
this._showHeader$.next(visible);
|
||||
}
|
||||
|
||||
private async setAccountInfo(userId: UserId, update: Partial<AccountInfo>): Promise<void> {
|
||||
function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo {
|
||||
return { ...oldAccountInfo, ...update };
|
||||
|
||||
@@ -138,7 +138,7 @@ export interface Fido2AuthenticatorGetAssertionParams {
|
||||
rpId: string;
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
hash: BufferSource;
|
||||
allowCredentialDescriptorList: PublicKeyCredentialDescriptor[];
|
||||
allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[];
|
||||
/** The effective user verification requirement for assertion, a Boolean value provided by the client. */
|
||||
requireUserVerification: boolean;
|
||||
/** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */
|
||||
|
||||
@@ -95,7 +95,7 @@ export abstract class Fido2UserInterfaceSession {
|
||||
*/
|
||||
abstract confirmNewCredential(
|
||||
params: NewCredentialParams,
|
||||
): Promise<{ cipherId: string; userVerified: boolean }>;
|
||||
): Promise<{ cipherId?: string; userVerified: boolean }>;
|
||||
|
||||
/**
|
||||
* Make sure that the vault is unlocked.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
|
||||
|
||||
import { Fido2Utils } from "./fido2-utils";
|
||||
|
||||
describe("Fido2 Utils", () => {
|
||||
@@ -67,4 +73,62 @@ describe("Fido2 Utils", () => {
|
||||
expect(expectedArray).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cipherHasNoOtherPasskeys(...)", () => {
|
||||
const emptyPasskeyCipher = mock<CipherView>({
|
||||
id: "id-5",
|
||||
localData: { lastUsedDate: 222 },
|
||||
name: "name-5",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "username-5",
|
||||
password: "password",
|
||||
uri: "https://example.com",
|
||||
fido2Credentials: [],
|
||||
},
|
||||
});
|
||||
|
||||
const passkeyCipher = mock<CipherView>({
|
||||
id: "id-5",
|
||||
localData: { lastUsedDate: 222 },
|
||||
name: "name-5",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "username-5",
|
||||
password: "password",
|
||||
uri: "https://example.com",
|
||||
fido2Credentials: [
|
||||
mock<Fido2CredentialView>({
|
||||
credentialId: "credential-id",
|
||||
rpName: "credential-name",
|
||||
userHandle: "user-handle-1",
|
||||
userName: "credential-username",
|
||||
rpId: "jest-testing-website.com",
|
||||
}),
|
||||
mock<Fido2CredentialView>({
|
||||
credentialId: "credential-id",
|
||||
rpName: "credential-name",
|
||||
userHandle: "user-handle-2",
|
||||
userName: "credential-username",
|
||||
rpId: "jest-testing-website.com",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
it("should return true when there is no userHandle", () => {
|
||||
const userHandle = "user-handle-1";
|
||||
expect(Fido2Utils.cipherHasNoOtherPasskeys(emptyPasskeyCipher, userHandle)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return true when userHandle matches", () => {
|
||||
const userHandle = "user-handle-1";
|
||||
expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false when userHandle doesn't match", () => {
|
||||
const userHandle = "testing";
|
||||
expect(Fido2Utils.cipherHasNoOtherPasskeys(passkeyCipher, userHandle)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
import type {
|
||||
AssertCredentialResult,
|
||||
@@ -111,4 +113,16 @@ export class Fido2Utils {
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||
* @param userHandle
|
||||
*/
|
||||
static cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
|
||||
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,63 @@
|
||||
import { guidToRawFormat } from "./guid-utils";
|
||||
import { guidToRawFormat, guidToStandardFormat } from "./guid-utils";
|
||||
|
||||
const workingExamples: [string, Uint8Array][] = [
|
||||
[
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
]),
|
||||
],
|
||||
[
|
||||
"08d70b74-e9f5-4522-a425-e5dcd40107e7",
|
||||
new Uint8Array([
|
||||
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||
0xe7,
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
describe("guid-utils", () => {
|
||||
describe("guidToRawFormat", () => {
|
||||
it.each(workingExamples)(
|
||||
"returns UUID in binary format when given a valid UUID string",
|
||||
(input, expected) => {
|
||||
const result = guidToRawFormat(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
[
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
],
|
||||
"08d70b74-e9f5-4522-a425-e5dcd40107e7",
|
||||
[
|
||||
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||
0xe7,
|
||||
],
|
||||
],
|
||||
])("returns UUID in binary format when given a valid UUID string", (input, expected) => {
|
||||
const result = guidToRawFormat(input);
|
||||
|
||||
expect(result).toEqual(new Uint8Array(expected));
|
||||
"invalid",
|
||||
"",
|
||||
"",
|
||||
"00000000-0000-0000-0000-0000000000000000",
|
||||
"00000000-0000-0000-0000-000000",
|
||||
])("throws an error when given an invalid UUID string", (input) => {
|
||||
expect(() => guidToRawFormat(input)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
it("throws an error when given an invalid UUID string", () => {
|
||||
expect(() => guidToRawFormat("invalid")).toThrow(TypeError);
|
||||
describe("guidToStandardFormat", () => {
|
||||
it.each(workingExamples)(
|
||||
"returns UUID in standard format when given a valid UUID array buffer",
|
||||
(expected, input) => {
|
||||
const result = guidToStandardFormat(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
new Uint8Array(),
|
||||
new Uint8Array([]),
|
||||
new Uint8Array([
|
||||
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||
0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7,
|
||||
]),
|
||||
])("throws an error when given an invalid UUID array buffer", (input) => {
|
||||
expect(() => guidToStandardFormat(input)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,10 @@ export function guidToRawFormat(guid: string) {
|
||||
|
||||
/** Convert raw 16 byte array to standard format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID. */
|
||||
export function guidToStandardFormat(bufferSource: BufferSource) {
|
||||
if (bufferSource.byteLength !== 16) {
|
||||
throw TypeError("BufferSource length is invalid");
|
||||
}
|
||||
|
||||
const arr =
|
||||
bufferSource instanceof ArrayBuffer
|
||||
? new Uint8Array(bufferSource)
|
||||
|
||||
@@ -68,6 +68,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
/** When true, will override the match strategy for the cipher if it is Never. */
|
||||
overrideNeverMatchStrategy?: true,
|
||||
): Promise<CipherView[]>;
|
||||
abstract getAllDecryptedForIds(userId: UserId, ids: string[]): Promise<CipherView[]>;
|
||||
abstract filterCiphersForUrl<C extends CipherViewLike = CipherView>(
|
||||
ciphers: C[],
|
||||
url: string,
|
||||
|
||||
@@ -619,6 +619,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async getAllDecryptedForIds(userId: UserId, ids: string[]): Promise<CipherView[]> {
|
||||
return firstValueFrom(
|
||||
this.cipherViews$(userId).pipe(
|
||||
filter((ciphers) => ciphers != null),
|
||||
map((ciphers) => ciphers.filter((cipher) => ids.includes(cipher.id))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async filterCiphersForUrl<C extends CipherViewLike>(
|
||||
ciphers: C[],
|
||||
url: string,
|
||||
|
||||
Reference in New Issue
Block a user