mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
Merged with master and fixed conflicts
This commit is contained in:
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../shared/jest.config.base");
|
||||
const sharedConfig = require("../shared/jest.config.ts");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "libs/common tests",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@ export abstract class ApiService {
|
||||
putDeviceVerificationSettings: (
|
||||
request: DeviceVerificationRequest
|
||||
) => Promise<DeviceVerificationResponse>;
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
|
||||
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;
|
||||
|
||||
@@ -2,8 +2,20 @@ export interface MessageBase {
|
||||
command: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
export abstract class BroadcasterService {
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
send: (message: MessageBase, id?: string) => void;
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
subscribe: (id: string, messageCallback: (message: MessageBase) => void) => void;
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
unsubscribe: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DeviceKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
|
||||
export abstract class DeviceCryptoServiceAbstraction {
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DeviceResponse } from "./responses/device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
}
|
||||
@@ -7,6 +7,9 @@ export class DeviceResponse extends BaseResponse {
|
||||
identifier: string;
|
||||
type: DeviceType;
|
||||
creationDate: string;
|
||||
encryptedUserKey: string;
|
||||
encryptedPublicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -15,5 +18,8 @@ export class DeviceResponse extends BaseResponse {
|
||||
this.identifier = this.getResponseProperty("Identifier");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
|
||||
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
|
||||
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountSettingsSettings } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@@ -163,6 +163,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
|
||||
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
describe("ServerConfigData", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should create a ServerConfigData from a JSON object", () => {
|
||||
const serverConfigData = ServerConfigData.fromJSON({
|
||||
const json = {
|
||||
version: "1.0.0",
|
||||
gitHash: "1234567890",
|
||||
server: {
|
||||
@@ -22,18 +22,11 @@ describe("ServerConfigData", () => {
|
||||
sso: "https://sso.com",
|
||||
},
|
||||
utcDate: "2020-01-01T00:00:00.000Z",
|
||||
});
|
||||
featureStates: { feature: "state" },
|
||||
};
|
||||
const serverConfigData = ServerConfigData.fromJSON(json);
|
||||
|
||||
expect(serverConfigData.version).toEqual("1.0.0");
|
||||
expect(serverConfigData.gitHash).toEqual("1234567890");
|
||||
expect(serverConfigData.server.name).toEqual("test");
|
||||
expect(serverConfigData.server.url).toEqual("https://test.com");
|
||||
expect(serverConfigData.environment.vault).toEqual("https://vault.com");
|
||||
expect(serverConfigData.environment.api).toEqual("https://api.com");
|
||||
expect(serverConfigData.environment.identity).toEqual("https://identity.com");
|
||||
expect(serverConfigData.environment.notifications).toEqual("https://notifications.com");
|
||||
expect(serverConfigData.environment.sso).toEqual("https://sso.com");
|
||||
expect(serverConfigData.utcDate).toEqual("2020-01-01T00:00:00.000Z");
|
||||
expect(serverConfigData).toEqual(json);
|
||||
});
|
||||
|
||||
it("should be an instance of ServerConfigData", () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { EventData } from "../data/event.data";
|
||||
import { ServerConfigData } from "../data/server-config.data";
|
||||
|
||||
import { EncString } from "./enc-string";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
@@ -107,6 +107,7 @@ export class AccountKeys {
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
deviceKey?: DeviceKey;
|
||||
organizationKeys?: EncryptionPair<
|
||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
||||
Record<string, SymmetricCryptoKey>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
@@ -75,3 +75,6 @@ export class SymmetricCryptoKey {
|
||||
return SymmetricCryptoKey.fromString(obj?.keyB64);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup all separate key types as opaque types
|
||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||
|
||||
@@ -1110,14 +1110,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new DeviceVerificationResponse(r);
|
||||
}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.send("GET", "/devices/knowndevice", null, false, true, null, (headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
});
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
// Emergency Access APIs
|
||||
|
||||
async getEmergencyAccessTrusted(): Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>> {
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
import { BehaviorSubject, concatMap, timer } from "rxjs";
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, concatMap, from, takeUntil, timer } from "rxjs";
|
||||
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
|
||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
|
||||
export class ConfigService implements ConfigServiceAbstraction {
|
||||
@Injectable()
|
||||
export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
|
||||
protected _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
|
||||
serverConfig$ = this._serverConfig.asObservable();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private configApiService: ConfigApiServiceAbstraction,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private environmentService: EnvironmentService
|
||||
) {
|
||||
// Re-fetch the server config every hour
|
||||
timer(0, 1000 * 3600)
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
return await this.fetchServerConfig();
|
||||
})
|
||||
)
|
||||
.pipe(concatMap(() => from(this.fetchServerConfig())))
|
||||
.subscribe((serverConfig) => {
|
||||
this._serverConfig.next(serverConfig);
|
||||
});
|
||||
|
||||
this.environmentService.urls.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.fetchServerConfig();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async fetchServerConfig(): Promise<ServerConfig> {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "../abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected encryptService: EncryptService,
|
||||
protected stateService: StateService,
|
||||
protected appIdService: AppIdService,
|
||||
protected devicesApiService: DevicesApiServiceAbstraction
|
||||
) {}
|
||||
|
||||
async trustDevice(): Promise<DeviceResponse> {
|
||||
// Attempt to get user symmetric key
|
||||
const userSymKey: SymmetricCryptoKey = await this.cryptoService.getEncKey();
|
||||
|
||||
// If user symmetric key is not found, throw error
|
||||
if (!userSymKey) {
|
||||
throw new Error("User symmetric key not found");
|
||||
}
|
||||
|
||||
// Generate deviceKey
|
||||
const deviceKey = await this.makeDeviceKey();
|
||||
|
||||
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
|
||||
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
|
||||
2048
|
||||
);
|
||||
|
||||
const [
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user symmetric key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userSymKey.encKey, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user symmetric key
|
||||
this.encryptService.encrypt(devicePublicKey, userSymKey),
|
||||
|
||||
// Encrypt devicePrivateKey with deviceKey
|
||||
this.encryptService.encrypt(devicePrivateKey, deviceKey),
|
||||
]);
|
||||
|
||||
// Send encrypted keys to server
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
return this.devicesApiService.updateTrustedDeviceKeys(
|
||||
deviceIdentifier,
|
||||
devicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
userSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
deviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
}
|
||||
|
||||
async getDeviceKey(): Promise<DeviceKey> {
|
||||
// Check if device key is already stored
|
||||
const existingDeviceKey = await this.stateService.getDeviceKey();
|
||||
|
||||
if (existingDeviceKey != null) {
|
||||
return existingDeviceKey;
|
||||
} else {
|
||||
return this.makeDeviceKey();
|
||||
}
|
||||
}
|
||||
|
||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||
// Create 512-bit device key
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
|
||||
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
|
||||
|
||||
// Save device key in secure storage
|
||||
await this.stateService.setDeviceKey(deviceKey);
|
||||
|
||||
return deviceKey;
|
||||
}
|
||||
}
|
||||
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EncryptionType } from "../enums/encryption-type.enum";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../services/crypto.service";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
import { DeviceCryptoService } from "./device-crypto.service.implementation";
|
||||
|
||||
describe("deviceCryptoService", () => {
|
||||
let deviceCryptoService: DeviceCryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const stateService = mock<StateService>();
|
||||
const appIdService = mock<AppIdService>();
|
||||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(cryptoFunctionService);
|
||||
mockReset(encryptService);
|
||||
mockReset(stateService);
|
||||
mockReset(appIdService);
|
||||
mockReset(devicesApiService);
|
||||
|
||||
deviceCryptoService = new DeviceCryptoService(
|
||||
cryptoFunctionService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
stateService,
|
||||
appIdService,
|
||||
devicesApiService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(deviceCryptoService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const userSymKeyBytesLength = 64;
|
||||
|
||||
describe("getDeviceKey", () => {
|
||||
let mockRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: SymmetricCryptoKey;
|
||||
let existingDeviceKey: DeviceKey;
|
||||
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
|
||||
existingDeviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
||||
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
|
||||
});
|
||||
|
||||
it("gets a device key when there is not an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
|
||||
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(mockDeviceKey);
|
||||
});
|
||||
|
||||
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(existingDeviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeDeviceKey", () => {
|
||||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
|
||||
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trustDevice", () => {
|
||||
let mockDeviceKeyRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: DeviceKey;
|
||||
|
||||
let mockUserSymKeyRandomBytes: CsprngArray;
|
||||
let mockUserSymKey: SymmetricCryptoKey;
|
||||
|
||||
const deviceRsaKeyLength = 2048;
|
||||
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
|
||||
let mockDevicePrivateKey: ArrayBuffer;
|
||||
let mockDevicePublicKey: ArrayBuffer;
|
||||
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
|
||||
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
|
||||
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
|
||||
|
||||
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
|
||||
Id: "mockId",
|
||||
Name: "mockName",
|
||||
Identifier: "mockIdentifier",
|
||||
Type: "mockType",
|
||||
CreationDate: "mockCreationDate",
|
||||
});
|
||||
|
||||
const mockDeviceId = "mockDeviceId";
|
||||
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
let rsaGenerateKeyPairSpy: jest.SpyInstance;
|
||||
let cryptoSvcGetEncKeySpy: jest.SpyInstance;
|
||||
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
|
||||
let encryptServiceEncryptSpy: jest.SpyInstance;
|
||||
let appIdServiceGetAppIdSpy: jest.SpyInstance;
|
||||
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup all spies and default return values for the happy path
|
||||
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
|
||||
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
|
||||
|
||||
mockDeviceRsaKeyPair = [
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
];
|
||||
|
||||
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
|
||||
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
|
||||
|
||||
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
|
||||
EncryptionType.Rsa2048_OaepSha1_B64,
|
||||
"mockDevicePublicKeyEncryptedUserSymKey"
|
||||
);
|
||||
|
||||
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockUserSymKeyEncryptedDevicePublicKey"
|
||||
);
|
||||
|
||||
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockDeviceKeyEncryptedDevicePrivateKey"
|
||||
);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
makeDeviceKeySpy = jest
|
||||
.spyOn(deviceCryptoService as any, "makeDeviceKey")
|
||||
.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
rsaGenerateKeyPairSpy = jest
|
||||
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
|
||||
.mockResolvedValue(mockDeviceRsaKeyPair);
|
||||
|
||||
cryptoSvcGetEncKeySpy = jest
|
||||
.spyOn(cryptoService, "getEncKey")
|
||||
.mockResolvedValue(mockUserSymKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaEncrypt")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
.spyOn(encryptService, "encrypt")
|
||||
.mockImplementation((plainValue, key) => {
|
||||
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
|
||||
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
|
||||
}
|
||||
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
|
||||
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
|
||||
}
|
||||
});
|
||||
|
||||
appIdServiceGetAppIdSpy = jest
|
||||
.spyOn(appIdService, "getAppId")
|
||||
.mockResolvedValue(mockDeviceId);
|
||||
|
||||
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
|
||||
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
|
||||
.mockResolvedValue(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
||||
const response = await deviceCryptoService.trustDevice();
|
||||
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcGetEncKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
|
||||
mockDeviceId,
|
||||
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
|
||||
expect(response).toBeInstanceOf(DeviceResponse);
|
||||
expect(response).toEqual(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("throws specific error if user symmetric key is not found", async () => {
|
||||
// setup the spy to return null
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(null);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
|
||||
// reset the spy
|
||||
cryptoSvcGetEncKeySpy.mockReset();
|
||||
|
||||
// setup the spy to return undefined
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(undefined);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
});
|
||||
|
||||
const methodsToTestForErrorsOrInvalidReturns = [
|
||||
{
|
||||
method: "makeDeviceKey",
|
||||
spy: () => makeDeviceKeySpy,
|
||||
errorText: "makeDeviceKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaGenerateKeyPair",
|
||||
spy: () => rsaGenerateKeyPairSpy,
|
||||
errorText: "rsaGenerateKeyPair error",
|
||||
},
|
||||
{
|
||||
method: "getEncKey",
|
||||
spy: () => cryptoSvcGetEncKeySpy,
|
||||
errorText: "getEncKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaEncrypt",
|
||||
spy: () => cryptoSvcRsaEncryptSpy,
|
||||
errorText: "rsaEncrypt error",
|
||||
},
|
||||
{
|
||||
method: "encryptService.encrypt",
|
||||
spy: () => encryptServiceEncryptSpy,
|
||||
errorText: "encryptService.encrypt error",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(methodsToTestForErrorsOrInvalidReturns)(
|
||||
"trustDevice error handling and invalid return testing",
|
||||
({ method, spy, errorText }) => {
|
||||
// ensures that error propagation works correctly
|
||||
it(`throws an error if ${method} fails`, async () => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockRejectedValue(new Error(errorText));
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
|
||||
});
|
||||
|
||||
test.each([null, undefined])(
|
||||
`throws an error if ${method} returns %s`,
|
||||
async (invalidValue) => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockResolvedValue(invalidValue);
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ApiService } from "../api.service";
|
||||
|
||||
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
|
||||
|
||||
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/devices/knowndevice",
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
(headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
}
|
||||
);
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by identifier
|
||||
* @param deviceIdentifier - client generated id (not device id in DB)
|
||||
*/
|
||||
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/devices/identifier/${deviceIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new DeviceResponse(r);
|
||||
}
|
||||
|
||||
async updateTrustedDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
): Promise<DeviceResponse> {
|
||||
const request = new TrustedDeviceKeysRequest(
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey
|
||||
);
|
||||
|
||||
const result = await this.apiService.send(
|
||||
"PUT",
|
||||
`/devices/${deviceIdentifier}/keys`,
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return new DeviceResponse(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class TrustedDeviceKeysRequest {
|
||||
constructor(
|
||||
public encryptedUserKey: string,
|
||||
public encryptedPublicKey: string,
|
||||
public encryptedPrivateKey: string
|
||||
) {}
|
||||
}
|
||||
@@ -218,6 +218,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
return ![
|
||||
"http://vault.bitwarden.com",
|
||||
"https://vault.bitwarden.com",
|
||||
"http://vault.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu",
|
||||
"http://vault.qa.bitwarden.pw",
|
||||
"https://vault.qa.bitwarden.pw",
|
||||
].includes(this.getWebVaultUrl());
|
||||
|
||||
@@ -35,7 +35,7 @@ import { EncString } from "../models/domain/enc-string";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@@ -1054,6 +1054,32 @@ export class StateService<
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
return account?.keys?.deviceKey as DeviceKey;
|
||||
}
|
||||
|
||||
async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
account.keys.deviceKey = value;
|
||||
|
||||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
@@ -2751,7 +2777,10 @@ export class StateService<
|
||||
|
||||
// settings persist even on reset, and are not effected by this method
|
||||
protected resetAccount(account: TAccount) {
|
||||
const persistentAccountInformation = { settings: account.settings };
|
||||
const persistentAccountInformation = {
|
||||
settings: account.settings,
|
||||
keys: { deviceKey: account.keys.deviceKey },
|
||||
};
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
@@ -2830,7 +2859,7 @@ export class StateService<
|
||||
return this.reconcileOptions(options, defaultOptions);
|
||||
}
|
||||
|
||||
private async saveSecureStorageKey<T extends JsonValue>(
|
||||
protected async saveSecureStorageKey<T extends JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: StorageOptions
|
||||
|
||||
4
libs/common/src/types/csprng.d.ts
vendored
4
libs/common/src/types/csprng.d.ts
vendored
@@ -1,5 +1,9 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
// You would typically use these types when you want to create a type that
|
||||
// represents an array or string value generated from a
|
||||
// cryptographic secure pseudorandom number generator (CSPRNG)
|
||||
|
||||
type CsprngArray = Opaque<ArrayBuffer, "CSPRNG">;
|
||||
|
||||
type CsprngString = Opaque<string, "CSPRNG">;
|
||||
|
||||
Reference in New Issue
Block a user