1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'feature/org-admin-refresh' into EC-86-groups-table

This commit is contained in:
Shane Melton
2022-09-28 16:00:56 -07:00
573 changed files with 11184 additions and 4053 deletions

View File

@@ -0,0 +1,4 @@
export abstract class AnonymousHubService {
createHubConnection: (token: string) => void;
stopHubConnection: () => void;
}

View File

@@ -48,6 +48,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
import { PasswordRequest } from "../models/request/passwordRequest";
import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
import { PaymentRequest } from "../models/request/paymentRequest";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
@@ -86,6 +87,8 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
import { AttachmentResponse } from "../models/response/attachmentResponse";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { AuthRequestResponse } from "../models/response/authRequestResponse";
import { RegisterResponse } from "../models/response/authentication/registerResponse";
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
@@ -191,7 +194,7 @@ export abstract class ApiService {
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
getAccountRevisionDate: () => Promise<number>;
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
postRegister: (request: RegisterRequest) => Promise<any>;
postRegister: (request: RegisterRequest) => Promise<RegisterResponse>;
postPremium: (data: FormData) => Promise<PaymentResponse>;
postIapCheck: (request: IapCheckRequest) => Promise<any>;
postReinstatePremium: () => Promise<any>;
@@ -211,6 +214,9 @@ export abstract class ApiService {
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;

View File

@@ -1,18 +1,26 @@
import { Observable } from "rxjs";
import { AuthenticationStatus } from "../enums/authenticationStatus";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { AuthRequestPushNotification } from "../models/response/notificationResponse";
export abstract class AuthService {
masterPasswordHash: string;
email: string;
logIn: (
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
credentials:
| ApiLogInCredentials
| PasswordLogInCredentials
| SsoLogInCredentials
| PasswordlessLogInCredentials
) => Promise<AuthResult>;
logInTwoFactor: (
twoFactor: TokenRequestTwoFactor,
@@ -24,4 +32,7 @@ export abstract class AuthService {
authingWithSso: () => boolean;
authingWithPassword: () => boolean;
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise<any>;
getPushNotifcationObs$: () => Observable<any>;
}

View File

@@ -1,11 +0,0 @@
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export abstract class OrganizationService {
get: (id: string) => Promise<Organization>;
getByIdentifier: (identifier: string) => Promise<Organization>;
getAll: (userId?: string) => Promise<Organization[]>;
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: (userId?: string) => Promise<boolean>;
}

View File

@@ -0,0 +1,55 @@
import { map, Observable } from "rxjs";
import { Utils } from "../../misc/utils";
import { Organization } from "../../models/domain/organization";
import { I18nService } from "../i18n.service";
export function canAccessSettingsTab(org: Organization): boolean {
return org.isOwner;
}
export function canAccessMembersTab(org: Organization): boolean {
return org.canManageUsers || org.canManageUsersPassword;
}
export function canAccessGroupsTab(org: Organization): boolean {
return org.canManageGroups;
}
export function canAccessReportingTab(org: Organization): boolean {
return org.canAccessReports || org.canAccessEventLogs;
}
export function canAccessBillingTab(org: Organization): boolean {
return org.canManageBilling;
}
export function canAccessOrgAdmin(org: Organization): boolean {
return (
canAccessMembersTab(org) ||
canAccessGroupsTab(org) ||
canAccessReportingTab(org) ||
canAccessBillingTab(org) ||
canAccessSettingsTab(org)
);
}
export function getOrganizationById(id: string) {
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
}
export function canAccessAdmin(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) =>
orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name"))
);
}
export abstract class OrganizationService {
organizations$: Observable<Organization[]>;
get: (id: string) => Organization;
getByIdentifier: (identifier: string) => Organization;
getAll: (userId?: string) => Promise<Organization[]>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: () => boolean;
}

View File

@@ -78,8 +78,6 @@ export abstract class StateService<T extends Account = Account> {
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
getDecodedToken: (options?: StorageOptions) => Promise<any>;
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
@@ -141,6 +139,8 @@ export abstract class StateService<T extends Account = Account> {
setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>;
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
@@ -160,6 +160,11 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableDuckDuckGoBrowserIntegration: (
value: boolean,
options?: StorageOptions
) => Promise<void>;
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableGravitars: (options?: StorageOptions) => Promise<boolean>;
@@ -268,7 +273,13 @@ export abstract class StateService<T extends Account = Account> {
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use OrganizationService
*/
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
/**
* @deprecated Do not call this directly, use OrganizationService
*/
setOrganizations: (
value: { [id: string]: OrganizationData },
options?: StorageOptions

View File

@@ -1,4 +1,4 @@
import { StorageOptions } from "../models/domain/storageOptions";
import { MemoryStorageOptions, StorageOptions } from "../models/domain/storageOptions";
export abstract class AbstractStorageService {
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
@@ -8,5 +8,9 @@ export abstract class AbstractStorageService {
}
export abstract class AbstractCachedStorageService extends AbstractStorageService {
abstract getBypassCache<T>(key: string, options?: StorageOptions): Promise<T>;
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}
export interface MemoryStorageServiceInterface {
get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}

View File

@@ -1,17 +1,12 @@
import { Observable } from "rxjs";
import {
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../../models/response/notificationResponse";
import { SyncEventArgs } from "../../types/syncEventArgs";
export abstract class SyncService {
syncInProgress: boolean;
sync$: Observable<SyncEventArgs>;
getLastSync: () => Promise<Date>;
setLastSync: (date: Date, userId?: string) => Promise<any>;
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;

View File

@@ -0,0 +1,8 @@
import { Observable } from "rxjs";
import { SyncEventArgs } from "../../types/syncEventArgs";
export abstract class SyncNotifierService {
sync$: Observable<SyncEventArgs>;
next: (event: SyncEventArgs) => void;
}

View File

@@ -0,0 +1,4 @@
export enum AuthRequestType {
AuthenticateAndUnlock = 0,
Unlock = 1,
}

View File

@@ -2,4 +2,5 @@ export enum AuthenticationType {
Password = 0,
Sso = 1,
Api = 2,
Passwordless = 3,
}

View File

@@ -0,0 +1,4 @@
export enum NativeMessagingVersion {
One = 1, // Original implementation
Latest = One,
}

View File

@@ -17,4 +17,7 @@ export enum NotificationType {
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
AuthRequest = 15,
AuthRequestResponse = 16,
}

View File

@@ -14,6 +14,7 @@ import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
} from "../../models/domain/logInCredentials";
import { DeviceRequest } from "../../models/request/deviceRequest";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
@@ -42,7 +43,11 @@ export abstract class LogInStrategy {
) {}
abstract logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
credentials:
| ApiLogInCredentials
| PasswordLogInCredentials
| SsoLogInCredentials
| PasswordlessLogInCredentials
): Promise<AuthResult>;
async logInTwoFactor(

View File

@@ -0,0 +1,86 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { AuthService } from "../../abstractions/auth.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { TokenService } from "../../abstractions/token.service";
import { TwoFactorService } from "../../abstractions/twoFactor.service";
import { AuthResult } from "../../models/domain/authResult";
import { PasswordlessLogInCredentials } from "../../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { LogInStrategy } from "./logIn.strategy";
export class PasswordlessLogInStrategy extends LogInStrategy {
get email() {
return this.tokenRequest.email;
}
get masterPasswordHash() {
return this.tokenRequest.masterPasswordHash;
}
tokenRequest: PasswordTokenRequest;
private localHashedPassword: string;
private key: SymmetricCryptoKey;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private authService: AuthService
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService
);
}
async onSuccessfulLogin() {
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
async logIn(credentials: PasswordlessLogInCredentials) {
this.localHashedPassword = credentials.localPasswordHash;
this.key = credentials.decKey;
this.tokenRequest = new PasswordTokenRequest(
credentials.email,
credentials.accessCode,
null,
await this.buildTwoFactor(credentials.twoFactor),
await this.buildDeviceRequest()
);
this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId);
return this.startLogIn();
}
}

View File

@@ -99,6 +99,9 @@ export class Utils {
}
static fromByteStringToArray(str: string): Uint8Array {
if (str == null) {
return null;
}
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
@@ -307,8 +310,11 @@ export class Utils {
return map;
}
static getSortFunction(i18nService: I18nService, prop: string) {
return (a: any, b: any) => {
static getSortFunction<T>(
i18nService: I18nService,
prop: { [K in keyof T]: T[K] extends string ? K : never }[keyof T]
): (a: T, b: T) => number {
return (a, b) => {
if (a[prop] == null && b[prop] != null) {
return -1;
}
@@ -319,9 +325,10 @@ export class Utils {
return 0;
}
// The `as unknown as string` here is unfortunate because typescript doesn't property understand that the return of T[prop] will be a string
return i18nService.collator
? i18nService.collator.compare(a[prop], b[prop])
: a[prop].localeCompare(b[prop]);
? i18nService.collator.compare(a[prop] as unknown as string, b[prop] as unknown as string)
: (a[prop] as unknown as string).localeCompare(b[prop] as unknown as string);
};
}

View File

@@ -74,7 +74,7 @@ export class SsoConfigApi extends BaseResponse {
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spMinIncomingSigningAlgorithm: string;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;

View File

@@ -0,0 +1,55 @@
import {
EnvironmentServerConfigData,
ServerConfigData,
ThirdPartyServerConfigData,
} from "./server-config.data";
describe("ServerConfigData", () => {
describe("fromJSON", () => {
it("should create a ServerConfigData from a JSON object", () => {
const serverConfigData = ServerConfigData.fromJSON({
version: "1.0.0",
gitHash: "1234567890",
server: {
name: "test",
url: "https://test.com",
},
environment: {
vault: "https://vault.com",
api: "https://api.com",
identity: "https://identity.com",
notifications: "https://notifications.com",
sso: "https://sso.com",
},
utcDate: "2020-01-01T00:00:00.000Z",
});
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");
});
it("should be an instance of ServerConfigData", () => {
const serverConfigData = ServerConfigData.fromJSON({} as any);
expect(serverConfigData).toBeInstanceOf(ServerConfigData);
});
it("should deserialize sub objects", () => {
const serverConfigData = ServerConfigData.fromJSON({
server: {},
environment: {},
} as any);
expect(serverConfigData.server).toBeInstanceOf(ThirdPartyServerConfigData);
expect(serverConfigData.environment).toBeInstanceOf(EnvironmentServerConfigData);
});
});
});

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import {
ServerConfigResponse,
ThirdPartyServerConfigResponse,
@@ -11,27 +13,38 @@ export class ServerConfigData {
environment?: EnvironmentServerConfigData;
utcDate: string;
constructor(serverConfigReponse: ServerConfigResponse) {
this.version = serverConfigReponse?.version;
this.gitHash = serverConfigReponse?.gitHash;
this.server = serverConfigReponse?.server
? new ThirdPartyServerConfigData(serverConfigReponse.server)
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
this.version = serverConfigResponse?.version;
this.gitHash = serverConfigResponse?.gitHash;
this.server = serverConfigResponse?.server
? new ThirdPartyServerConfigData(serverConfigResponse.server)
: null;
this.utcDate = new Date().toISOString();
this.environment = serverConfigReponse?.environment
? new EnvironmentServerConfigData(serverConfigReponse.environment)
this.environment = serverConfigResponse?.environment
? new EnvironmentServerConfigData(serverConfigResponse.environment)
: null;
}
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {
return Object.assign(new ServerConfigData({}), obj, {
server: obj?.server ? ThirdPartyServerConfigData.fromJSON(obj.server) : null,
environment: obj?.environment ? EnvironmentServerConfigData.fromJSON(obj.environment) : null,
});
}
}
export class ThirdPartyServerConfigData {
name: string;
url: string;
constructor(response: ThirdPartyServerConfigResponse) {
constructor(response: Partial<ThirdPartyServerConfigResponse>) {
this.name = response.name;
this.url = response.url;
}
static fromJSON(obj: Jsonify<ThirdPartyServerConfigData>): ThirdPartyServerConfigData {
return Object.assign(new ThirdPartyServerConfigData({}), obj);
}
}
export class EnvironmentServerConfigData {
@@ -41,11 +54,15 @@ export class EnvironmentServerConfigData {
notifications: string;
sso: string;
constructor(response: EnvironmentServerConfigResponse) {
constructor(response: Partial<EnvironmentServerConfigResponse>) {
this.vault = response.vault;
this.api = response.api;
this.identity = response.identity;
this.notifications = response.notifications;
this.sso = response.sso;
}
static fromJSON(obj: Jsonify<EnvironmentServerConfigData>): EnvironmentServerConfigData {
return Object.assign(new EnvironmentServerConfigData({}), obj);
}
}

View File

@@ -0,0 +1,62 @@
import { Utils } from "@bitwarden/common/misc/utils";
import { makeStaticByteArray } from "../../../spec/utils";
import { AccountKeys, EncryptionPair } from "./account";
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
describe("AccountKeys", () => {
describe("toJSON", () => {
it("should serialize itself", () => {
const keys = new AccountKeys();
const buffer = makeStaticByteArray(64).buffer;
keys.publicKey = buffer;
const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString");
keys.toJSON();
expect(bufferSpy).toHaveBeenCalledWith(buffer);
});
it("should serialize public key as a string", () => {
const keys = new AccountKeys();
keys.publicKey = Utils.fromByteStringToArray("hello").buffer;
const json = JSON.stringify(keys);
expect(json).toContain('"publicKey":"hello"');
});
});
describe("fromJSON", () => {
it("should deserialize public key to a buffer", () => {
const keys = AccountKeys.fromJSON({
publicKey: "hello",
});
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello").buffer);
});
it("should deserialize cryptoMasterKey", () => {
const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
AccountKeys.fromJSON({} as any);
expect(spy).toHaveBeenCalled();
});
it("should deserialize organizationKeys", () => {
const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
AccountKeys.fromJSON({ organizationKeys: [{ orgId: "keyJSON" }] } as any);
expect(spy).toHaveBeenCalled();
});
it("should deserialize providerKeys", () => {
const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
AccountKeys.fromJSON({ providerKeys: [{ providerId: "keyJSON" }] } as any);
expect(spy).toHaveBeenCalled();
});
it("should deserialize privateKey", () => {
const spy = jest.spyOn(EncryptionPair, "fromJSON");
AccountKeys.fromJSON({
privateKey: { encrypted: "encrypted", decrypted: "decrypted" },
} as any);
expect(spy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,9 @@
import { AccountProfile } from "./account";
describe("AccountProfile", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(AccountProfile.fromJSON({})).toBeInstanceOf(AccountProfile);
});
});
});

View File

@@ -0,0 +1,24 @@
import { AccountSettings, EncryptionPair } from "./account";
import { EncString } from "./encString";
describe("AccountSettings", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
});
it("should deserialize pinProtected", () => {
const accountSettings = new AccountSettings();
accountSettings.pinProtected = EncryptionPair.fromJSON<string, EncString>({
encrypted: "encrypted",
decrypted: "3.data",
});
const jsonObj = JSON.parse(JSON.stringify(accountSettings));
const actual = AccountSettings.fromJSON(jsonObj);
expect(actual.pinProtected).toBeInstanceOf(EncryptionPair);
expect(actual.pinProtected.encrypted).toEqual("encrypted");
expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data");
});
});
});

View File

@@ -0,0 +1,9 @@
import { AccountTokens } from "./account";
describe("AccountTokens", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens);
});
});
});

View File

@@ -0,0 +1,23 @@
import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account";
describe("Account", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(Account.fromJSON({})).toBeInstanceOf(Account);
});
it("should call all the sub-fromJSONs", () => {
const keysSpy = jest.spyOn(AccountKeys, "fromJSON");
const profileSpy = jest.spyOn(AccountProfile, "fromJSON");
const settingsSpy = jest.spyOn(AccountSettings, "fromJSON");
const tokensSpy = jest.spyOn(AccountTokens, "fromJSON");
Account.fromJSON({});
expect(keysSpy).toHaveBeenCalled();
expect(profileSpy).toHaveBeenCalled();
expect(settingsSpy).toHaveBeenCalled();
expect(tokensSpy).toHaveBeenCalled();
});
});
});

View File

@@ -1,3 +1,8 @@
import { Except, Jsonify } from "type-fest";
import { Utils } from "@bitwarden/common/misc/utils";
import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify";
import { AuthenticationStatus } from "../../enums/authenticationStatus";
import { KdfType } from "../../enums/kdfType";
import { UriMatchType } from "../../enums/uriMatchType";
@@ -24,7 +29,39 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export class EncryptionPair<TEncrypted, TDecrypted> {
encrypted?: TEncrypted;
decrypted?: TDecrypted;
decryptedSerialized?: string;
toJSON() {
return {
encrypted: this.encrypted,
decrypted:
this.decrypted instanceof ArrayBuffer
? Utils.fromBufferToByteString(this.decrypted)
: this.decrypted,
};
}
static fromJSON<TEncrypted, TDecrypted>(
obj: Jsonify<EncryptionPair<Jsonify<TEncrypted>, Jsonify<TDecrypted>>>,
decryptedFromJson?: (decObj: Jsonify<TDecrypted> | string) => TDecrypted,
encryptedFromJson?: (encObj: Jsonify<TEncrypted>) => TEncrypted
) {
if (obj == null) {
return null;
}
const pair = new EncryptionPair<TEncrypted, TDecrypted>();
if (obj?.encrypted != null) {
pair.encrypted = encryptedFromJson
? encryptedFromJson(obj.encrypted)
: (obj.encrypted as TEncrypted);
}
if (obj?.decrypted != null) {
pair.decrypted = decryptedFromJson
? decryptedFromJson(obj.decrypted)
: (obj.decrypted as TDecrypted);
}
return pair;
}
}
export class DataEncryptionPair<TEncrypted, TDecrypted> {
@@ -73,19 +110,66 @@ export class AccountKeys {
>();
organizationKeys?: EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
> = new EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
>();
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
providerKeys?: EncryptionPair<any, Record<string, SymmetricCryptoKey>> = new EncryptionPair<
any,
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
>();
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
publicKey?: ArrayBuffer;
publicKeySerialized?: string;
apiKeyClientSecret?: string;
toJSON() {
return Object.assign(this as Except<AccountKeys, "publicKey">, {
publicKey: Utils.fromBufferToByteString(this.publicKey),
});
}
static fromJSON(obj: DeepJsonify<AccountKeys>): AccountKeys {
if (obj == null) {
return null;
}
return Object.assign(
new AccountKeys(),
{ cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey) },
{
cryptoSymmetricKey: EncryptionPair.fromJSON(
obj?.cryptoSymmetricKey,
SymmetricCryptoKey.fromJSON
),
},
{ organizationKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.organizationKeys) },
{ providerKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.providerKeys) },
{
privateKey: EncryptionPair.fromJSON<string, ArrayBuffer>(
obj?.privateKey,
(decObj: string) => Utils.fromByteStringToArray(decObj).buffer
),
},
{
publicKey: Utils.fromByteStringToArray(obj?.publicKey)?.buffer,
}
);
}
static initRecordEncryptionPairsFromJSON(obj: any) {
return EncryptionPair.fromJSON(obj, (decObj: any) => {
if (obj == null) {
return null;
}
const record: Record<string, SymmetricCryptoKey> = {};
for (const id in decObj) {
record[id] = SymmetricCryptoKey.fromJSON(decObj[id]);
}
return record;
});
}
}
export class AccountProfile {
@@ -106,6 +190,14 @@ export class AccountProfile {
keyHash?: string;
kdfIterations?: number;
kdfType?: KdfType;
static fromJSON(obj: Jsonify<AccountProfile>): AccountProfile {
if (obj == null) {
return null;
}
return Object.assign(new AccountProfile(), obj);
}
}
export class AccountSettings {
@@ -142,6 +234,21 @@ export class AccountSettings {
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData;
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) {
return null;
}
return Object.assign(new AccountSettings(), obj, {
environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls),
pinProtected: EncryptionPair.fromJSON<string, EncString>(
obj?.pinProtected,
EncString.fromJSON
),
serverConfig: ServerConfigData.fromJSON(obj?.serverConfig),
});
}
}
export type AccountSettingsSettings = {
@@ -150,9 +257,16 @@ export type AccountSettingsSettings = {
export class AccountTokens {
accessToken?: string;
decodedToken?: any;
refreshToken?: string;
securityStamp?: string;
static fromJSON(obj: Jsonify<AccountTokens>): AccountTokens {
if (obj == null) {
return null;
}
return Object.assign(new AccountTokens(), obj);
}
}
export class Account {
@@ -186,4 +300,17 @@ export class Account {
},
});
}
static fromJSON(json: Jsonify<Account>): Account {
if (json == null) {
return null;
}
return Object.assign(new Account({}), json, {
keys: AccountKeys.fromJSON(json?.keys),
profile: AccountProfile.fromJSON(json?.profile),
settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens),
});
}
}

View File

@@ -0,0 +1,34 @@
import { Utils } from "@bitwarden/common/misc/utils";
import { EncryptionPair } from "./account";
describe("EncryptionPair", () => {
describe("toJSON", () => {
it("should populate decryptedSerialized for buffer arrays", () => {
const pair = new EncryptionPair<string, ArrayBuffer>();
pair.decrypted = Utils.fromByteStringToArray("hello").buffer;
const json = pair.toJSON();
expect(json.decrypted).toEqual("hello");
});
it("should serialize encrypted and decrypted", () => {
const pair = new EncryptionPair<string, string>();
pair.encrypted = "hello";
pair.decrypted = "world";
const json = pair.toJSON();
expect(json.encrypted).toEqual("hello");
expect(json.decrypted).toEqual("world");
});
});
describe("fromJSON", () => {
it("should deserialize encrypted and decrypted", () => {
const pair = EncryptionPair.fromJSON({
encrypted: "hello",
decrypted: "world",
});
expect(pair.encrypted).toEqual("hello");
expect(pair.decrypted).toEqual("world");
});
});
});

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
export class EnvironmentUrls {
base: string = null;
api: string = null;
@@ -7,4 +9,8 @@ export class EnvironmentUrls {
events: string = null;
webVault: string = null;
keyConnector: string = null;
static fromJSON(obj: Jsonify<EnvironmentUrls>): EnvironmentUrls {
return Object.assign(new EnvironmentUrls(), obj);
}
}

View File

@@ -37,4 +37,5 @@ export class GlobalState {
alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
}

View File

@@ -1,3 +1,5 @@
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { AuthenticationType } from "../../enums/authenticationType";
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
@@ -29,3 +31,16 @@ export class ApiLogInCredentials {
constructor(public clientId: string, public clientSecret: string) {}
}
export class PasswordlessLogInCredentials {
readonly type = AuthenticationType.Passwordless;
constructor(
public email: string,
public accessCode: string,
public authRequestId: string,
public decKey: SymmetricCryptoKey,
public localPasswordHash: string,
public twoFactor?: TokenRequestTwoFactor
) {}
}

View File

@@ -0,0 +1,28 @@
import { Account } from "./account";
import { State } from "./state";
describe("state", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(State.fromJSON({})).toBeInstanceOf(State);
});
it("should always assign an object to accounts", () => {
const state = State.fromJSON({});
expect(state.accounts).not.toBeNull();
expect(state.accounts).toEqual({});
});
it("should build an account map", () => {
const accountsSpy = jest.spyOn(Account, "fromJSON");
const state = State.fromJSON({
accounts: {
userId: {},
},
});
expect(state.accounts["userId"]).toBeInstanceOf(Account);
expect(accountsSpy).toHaveBeenCalled();
});
});
});

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { Account } from "./account";
import { GlobalState } from "./globalState";
@@ -14,4 +16,30 @@ export class State<
constructor(globals: TGlobalState) {
this.globals = globals;
}
// TODO, make Jsonify<State,TGlobalState,TAccount> work. It currently doesn't because Globals doesn't implement Jsonify.
static fromJSON<TGlobalState extends GlobalState, TAccount extends Account>(
obj: any
): State<TGlobalState, TAccount> {
if (obj == null) {
return null;
}
return Object.assign(new State(null), obj, {
accounts: State.buildAccountMapFromJSON(obj?.accounts),
});
}
private static buildAccountMapFromJSON(
jsonAccounts: Jsonify<{ [userId: string]: Jsonify<Account> }>
) {
if (!jsonAccounts) {
return {};
}
const accounts: { [userId: string]: Account } = {};
for (const userId in jsonAccounts) {
accounts[userId] = Account.fromJSON(jsonAccounts[userId]);
}
return accounts;
}
}

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { HtmlStorageLocation } from "../../enums/htmlStorageLocation";
import { StorageLocation } from "../../enums/storageLocation";
@@ -8,3 +10,5 @@ export type StorageOptions = {
htmlStorageLocation?: HtmlStorageLocation;
keySuffix?: string;
};
export type MemoryStorageOptions<T> = StorageOptions & { deserializer?: (obj: Jsonify<T>) => T };

View File

@@ -4,6 +4,7 @@ import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
export abstract class TokenRequest {
protected device?: DeviceRequest;
protected passwordlessAuthRequest: string;
constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
this.device = device != null ? device : null;
@@ -18,6 +19,10 @@ export abstract class TokenRequest {
this.twoFactor = twoFactor;
}
setPasswordlessAccessCode(accessCode: string) {
this.passwordlessAuthRequest = accessCode;
}
protected toIdentityToken(clientId: string) {
const obj: any = {
scope: "api offline_access",
@@ -32,6 +37,11 @@ export abstract class TokenRequest {
// obj.devicePushToken = this.device.pushToken;
}
//passswordless login
if (this.passwordlessAuthRequest) {
obj.authRequest = this.passwordlessAuthRequest;
}
if (this.twoFactor.token && this.twoFactor.provider != null) {
obj.twoFactorToken = this.twoFactor.token;
obj.twoFactorProvider = this.twoFactor.provider;

View File

@@ -2,5 +2,6 @@ import { SsoConfigApi } from "../../api/ssoConfigApi";
export class OrganizationSsoRequest {
enabled = false;
identifier: string;
data: SsoConfigApi;
}

View File

@@ -2,6 +2,10 @@ import { OrganizationKeysRequest } from "./organizationKeysRequest";
export class OrganizationUpdateRequest {
name: string;
/**
* @deprecated 2022-08-03 Moved to OrganizationSsoRequest, left for backwards compatability.
* https://bitwarden.atlassian.net/browse/EC-489
*/
identifier: string;
businessName: string;
billingEmail: string;

View File

@@ -0,0 +1,12 @@
import { AuthRequestType } from "../../enums/authRequestType";
export class PasswordlessCreateAuthRequest {
constructor(
readonly email: string,
readonly deviceIdentifier: string,
readonly publicKey: string,
readonly type: AuthRequestType,
readonly accessCode: string,
readonly fingerprintPhrase: string
) {}
}

View File

@@ -0,0 +1,26 @@
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { BaseResponse } from "./baseResponse";
export class AuthRequestResponse extends BaseResponse {
id: string;
publicKey: string;
requestDeviceType: DeviceType;
requestIpAddress: string;
key: string;
masterPasswordHash: string;
creationDate: string;
requestApproved: boolean;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.publicKey = this.getResponseProperty("PublicKey");
this.requestDeviceType = this.getResponseProperty("RequestDeviceType");
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
this.key = this.getResponseProperty("Key");
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
this.creationDate = this.getResponseProperty("CreationDate");
this.requestApproved = this.getResponseProperty("RequestApproved");
}
}

View File

@@ -0,0 +1,3 @@
export interface ICaptchaProtectedResponse {
captchaBypassToken: string;
}

View File

@@ -0,0 +1,12 @@
import { BaseResponse } from "../baseResponse";
import { ICaptchaProtectedResponse } from "./ICaptchaProtectedResponse";
export class RegisterResponse extends BaseResponse implements ICaptchaProtectedResponse {
captchaBypassToken: string;
constructor(response: any) {
super(response);
this.captchaBypassToken = this.getResponseProperty("CaptchaBypassToken");
}
}

View File

@@ -37,6 +37,10 @@ export class NotificationResponse extends BaseResponse {
case NotificationType.SyncSendDelete:
this.payload = new SyncSendNotification(payload);
break;
case NotificationType.AuthRequest:
case NotificationType.AuthRequestResponse:
this.payload = new AuthRequestPushNotification(payload);
break;
default:
break;
}
@@ -96,3 +100,14 @@ export class SyncSendNotification extends BaseResponse {
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
}
}
export class AuthRequestPushNotification extends BaseResponse {
id: string;
userId: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
}
}

View File

@@ -3,12 +3,14 @@ import { BaseResponse } from "../baseResponse";
export class OrganizationSsoResponse extends BaseResponse {
enabled: boolean;
identifier: string;
data: SsoConfigApi;
urls: SsoUrls;
constructor(response: any) {
super(response);
this.enabled = this.getResponseProperty("Enabled");
this.identifier = this.getResponseProperty("Identifier");
this.data =
this.getResponseProperty("Data") != null
? new SsoConfigApi(this.getResponseProperty("Data"))

View File

@@ -34,7 +34,7 @@ export class SsoConfigView extends View {
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spMinIncomingSigningAlgorithm: string;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;

View File

@@ -0,0 +1,60 @@
import { Injectable } from "@angular/core";
import {
HttpTransportType,
HubConnection,
HubConnectionBuilder,
IHubProtocol,
} from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "../abstractions/anonymousHub.service";
import { AuthService } from "../abstractions/auth.service";
import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service";
import {
AuthRequestPushNotification,
NotificationResponse,
} from "./../models/response/notificationResponse";
@Injectable()
export class AnonymousHubService implements AnonymousHubServiceAbstraction {
private anonHubConnection: HubConnection;
private url: string;
constructor(
private environmentService: EnvironmentService,
private authService: AuthService,
private logService: LogService
) {}
async createHubConnection(token: string) {
this.url = this.environmentService.getNotificationsUrl();
this.anonHubConnection = new HubConnectionBuilder()
.withUrl(this.url + "/anonymousHub?Token=" + token, {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
})
.withHubProtocol(new MessagePackHubProtocol() as IHubProtocol)
.build();
this.anonHubConnection.start().catch((error) => this.logService.error(error));
this.anonHubConnection.on("AuthRequestResponseRecieved", (data: any) => {
this.ProcessNotification(new NotificationResponse(data));
});
}
stopHubConnection() {
if (this.anonHubConnection) {
this.anonHubConnection.stop();
}
}
private async ProcessNotification(notification: NotificationResponse) {
await this.authService.authResponsePushNotifiction(
notification.payload as AuthRequestPushNotification
);
}
}

View File

@@ -56,6 +56,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
import { PasswordRequest } from "../models/request/passwordRequest";
import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
import { PaymentRequest } from "../models/request/paymentRequest";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
@@ -94,6 +95,8 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
import { AttachmentResponse } from "../models/response/attachmentResponse";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { AuthRequestResponse } from "../models/response/authRequestResponse";
import { RegisterResponse } from "../models/response/authentication/registerResponse";
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
@@ -266,6 +269,17 @@ export class ApiService implements ApiServiceAbstraction {
}
}
async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/", request, false, true);
return new AuthRequestResponse(r);
}
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
const path = `/auth-requests/${id}/response?code=${accessCode}`;
const r = await this.send("GET", path, null, false, true);
return new AuthRequestResponse(r);
}
// Account APIs
async getProfile(): Promise<ProfileResponse> {
@@ -339,17 +353,18 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("POST", "/accounts/password-hint", request, false, false);
}
postRegister(request: RegisterRequest): Promise<any> {
return this.send(
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
const r = await this.send(
"POST",
"/accounts/register",
request,
false,
false,
true,
this.platformUtilsService.isDev()
? this.environmentService.getIdentityUrl()
: this.environmentService.getApiUrl()
);
return new RegisterResponse(r);
}
async postPremium(data: FormData): Promise<PaymentResponse> {
@@ -2337,7 +2352,9 @@ export class ApiService implements ApiServiceAbstraction {
requestInit.headers = headers;
const response = await this.fetch(new Request(requestUrl, requestInit));
if (hasResponse && response.status === 200) {
const responseType = response.headers.get("content-type");
const responseIsJson = responseType != null && responseType.indexOf("application/json") !== -1;
if (hasResponse && response.status === 200 && responseIsJson) {
const responseJson = await response.json();
return responseJson;
} else if (response.status !== 200) {

View File

@@ -1,3 +1,5 @@
import { Observable, Subject } from "rxjs";
import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
@@ -17,17 +19,20 @@ import { KdfType } from "../enums/kdfType";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ErrorResponse } from "../models/response/errorResponse";
import { AuthRequestPushNotification } from "../models/response/notificationResponse";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
@@ -42,9 +47,15 @@ export class AuthService implements AuthServiceAbstraction {
: null;
}
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
private logInStrategy:
| ApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy;
private sessionTimeout: any;
private pushNotificationSubject = new Subject<string>();
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
@@ -61,52 +72,78 @@ export class AuthService implements AuthServiceAbstraction {
) {}
async logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
credentials:
| ApiLogInCredentials
| PasswordLogInCredentials
| SsoLogInCredentials
| PasswordlessLogInCredentials
): Promise<AuthResult> {
this.clearState();
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
let strategy:
| ApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy;
if (credentials.type === AuthenticationType.Password) {
strategy = new PasswordLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this
);
} else if (credentials.type === AuthenticationType.Sso) {
strategy = new SsoLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService
);
} else if (credentials.type === AuthenticationType.Api) {
strategy = new ApiLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService
);
switch (credentials.type) {
case AuthenticationType.Password:
strategy = new PasswordLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this
);
break;
case AuthenticationType.Sso:
strategy = new SsoLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService
);
break;
case AuthenticationType.Api:
strategy = new ApiLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService
);
break;
case AuthenticationType.Passwordless:
strategy = new PasswordlessLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this
);
break;
}
const result = await strategy.logIn(credentials as any);
@@ -202,7 +239,21 @@ export class AuthService implements AuthServiceAbstraction {
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
}
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise<any> {
this.pushNotificationSubject.next(notification.id);
}
getPushNotifcationObs$(): Observable<any> {
return this.pushNotificationSubject.asObservable();
}
private saveState(
strategy:
| ApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy
) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}

View File

@@ -393,7 +393,7 @@ export class CipherService implements CipherServiceAbstraction {
: firstValueFrom(this.settingsService.settings$).then(
(settings: AccountSettingsSettings) => {
let matches: any[] = [];
settings.equivalentDomains.forEach((eqDomain: any) => {
settings.equivalentDomains?.forEach((eqDomain: any) => {
if (eqDomain.length && eqDomain.indexOf(domain) >= 0) {
matches = matches.concat(eqDomain);
}

View File

@@ -48,14 +48,16 @@ export class ConfigService implements ConfigServiceAbstraction {
}
private async fetchServerConfig(): Promise<ServerConfig> {
const response = await this.configApiService.get();
const data = new ServerConfigData(response);
try {
const response = await this.configApiService.get();
if (data != null) {
await this.stateService.setServerConfig(data);
return new ServerConfig(data);
if (response != null) {
const data = new ServerConfigData(response);
await this.stateService.setServerConfig(data);
return new ServerConfig(data);
}
} catch {
return null;
}
return null;
}
}

View File

@@ -98,7 +98,7 @@ export class EncryptService implements AbstractEncryptService {
}
}
return this.cryptoFunctionService.aesDecryptFast(fastParams);
return await this.cryptoFunctionService.aesDecryptFast(fastParams);
}
async decryptToBytes(encThing: IEncrypted, key: SymmetricCryptoKey): Promise<ArrayBuffer> {

View File

@@ -2,7 +2,7 @@ import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
import { StateService } from "../abstractions/state.service";
import { EventType } from "../enums/eventType";
import { EventData } from "../models/data/eventData";

View File

@@ -3,7 +3,7 @@ import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service";
import { OrganizationUserType } from "../enums/organizationUserType";

View File

@@ -1,6 +1,12 @@
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import {
AbstractStorageService,
MemoryStorageServiceInterface,
} from "@bitwarden/common/abstractions/storage.service";
export class MemoryStorageService implements AbstractStorageService {
export class MemoryStorageService
extends AbstractStorageService
implements MemoryStorageServiceInterface
{
private store = new Map<string, any>();
get<T>(key: string): Promise<T> {

View File

@@ -1,56 +0,0 @@
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export class OrganizationService implements OrganizationServiceAbstraction {
constructor(private stateService: StateService) {}
async get(id: string): Promise<Organization> {
const organizations = await this.stateService.getOrganizations();
// eslint-disable-next-line
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
return new Organization(organizations[id]);
}
async getByIdentifier(identifier: string): Promise<Organization> {
const organizations = await this.getAll();
if (organizations == null || organizations.length === 0) {
return null;
}
return organizations.find((o) => o.identifier === identifier);
}
async getAll(userId?: string): Promise<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
// eslint-disable-next-line
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
response.push(new Organization(organizations[id]));
}
}
const sortedResponse = response.sort((a, b) => a.name.localeCompare(b.name));
return sortedResponse;
}
async save(organizations: { [id: string]: OrganizationData }) {
return await this.stateService.setOrganizations(organizations);
}
async canManageSponsorships(): Promise<boolean> {
const orgs = await this.getAll();
return orgs.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
);
}
async hasOrganizations(userId?: string): Promise<boolean> {
const organizations = await this.getAll(userId);
return organizations.length > 0;
}
}

View File

@@ -1,5 +1,6 @@
import { ApiService } from "../../abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
import { SyncService } from "../../abstractions/sync/sync.service.abstraction";
import { OrganizationApiKeyType } from "../../enums/organizationApiKeyType";
import { ImportDirectoryRequest } from "../../models/request/importDirectoryRequest";
import { OrganizationSsoRequest } from "../../models/request/organization/organizationSsoRequest";
@@ -28,7 +29,7 @@ import { PaymentResponse } from "../../models/response/paymentResponse";
import { TaxInfoResponse } from "../../models/response/taxInfoResponse";
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
constructor(private apiService: ApiService) {}
constructor(private apiService: ApiService, private syncService: SyncService) {}
async get(id: string): Promise<OrganizationResponse> {
const r = await this.apiService.send("GET", "/organizations/" + id, null, true, true);
@@ -80,6 +81,8 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async create(request: OrganizationCreateRequest): Promise<OrganizationResponse> {
const r = await this.apiService.send("POST", "/organizations", request, true, true);
// Forcing a sync will notify organization service that they need to repull
await this.syncService.fullSync(true);
return new OrganizationResponse(r);
}
@@ -90,7 +93,9 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse> {
const r = await this.apiService.send("PUT", "/organizations/" + id, request, true, true);
return new OrganizationResponse(r);
const data = new OrganizationResponse(r);
await this.syncService.fullSync(true);
return data;
}
async updatePayment(id: string, request: PaymentRequest): Promise<void> {
@@ -144,7 +149,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
}
async verifyBank(id: string, request: VerifyBankRequest): Promise<void> {
return this.apiService.send(
await this.apiService.send(
"POST",
"/organizations/" + id + "/verify-bank",
request,
@@ -162,15 +167,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
}
async leave(id: string): Promise<void> {
return this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false);
await this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false);
await this.syncService.fullSync(true);
}
async delete(id: string, request: SecretVerificationRequest): Promise<void> {
return this.apiService.send("DELETE", "/organizations/" + id, request, true, false);
await this.apiService.send("DELETE", "/organizations/" + id, request, true, false);
await this.syncService.fullSync(true);
}
async updateLicense(id: string, data: FormData): Promise<void> {
return this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
await this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
}
async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void> {
@@ -223,6 +230,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
}
async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise<void> {
// Can't broadcast anything because the response doesn't have content
return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false);
}
@@ -242,6 +250,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
true,
true
);
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
return new OrganizationKeysResponse(r);
}
@@ -258,6 +267,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
true,
true
);
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
return new OrganizationSsoResponse(r);
}
}

View File

@@ -0,0 +1,119 @@
import { BehaviorSubject, concatMap, filter } from "rxjs";
import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "../../models/data/organizationData";
import { Organization } from "../../models/domain/organization";
import { isSuccessfullyCompleted } from "../../types/syncEventArgs";
export class OrganizationService implements OrganizationServiceAbstraction {
private _organizations = new BehaviorSubject<Organization[]>([]);
organizations$ = this._organizations.asObservable();
constructor(
private stateService: StateService,
private syncNotifierService: SyncNotifierService
) {
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {
if (!unlocked) {
this._organizations.next([]);
return;
}
const data = await this.stateService.getOrganizations();
this.updateObservables(data);
})
)
.subscribe();
this.syncNotifierService.sync$
.pipe(
filter(isSuccessfullyCompleted),
concatMap(async ({ data }) => {
const { profile } = data;
const organizations: { [id: string]: OrganizationData } = {};
profile.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
profile.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.updateStateAndObservables(organizations);
})
)
.subscribe();
}
async getAll(userId?: string): Promise<Organization[]> {
const organizationsMap = await this.stateService.getOrganizations({ userId: userId });
return Object.values(organizationsMap || {}).map((o) => new Organization(o));
}
async canManageSponsorships(): Promise<boolean> {
const organizations = this._organizations.getValue();
return organizations.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
);
}
hasOrganizations(): boolean {
const organizations = this._organizations.getValue();
return organizations.length > 0;
}
async upsert(organization: OrganizationData): Promise<void> {
let organizations = await this.stateService.getOrganizations();
if (organizations == null) {
organizations = {};
}
organizations[organization.id] = organization;
await this.updateStateAndObservables(organizations);
}
async delete(id: string): Promise<void> {
const organizations = await this.stateService.getOrganizations();
if (organizations == null) {
return;
}
if (organizations[id] == null) {
return;
}
delete organizations[id];
await this.updateStateAndObservables(organizations);
}
get(id: string): Organization {
const organizations = this._organizations.getValue();
return organizations.find((organization) => organization.id === id);
}
getByIdentifier(identifier: string): Organization {
const organizations = this._organizations.getValue();
return organizations.find((organization) => organization.identifier === identifier);
}
private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) {
await this.stateService.setOrganizations(organizationsMap);
this.updateObservables(organizationsMap);
}
private updateObservables(organizationsMap: { [id: string]: OrganizationData }) {
const organizations = Object.values(organizationsMap || {}).map((o) => new Organization(o));
this._organizations.next(organizations);
}
}

View File

@@ -1,5 +1,5 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";

View File

@@ -1,4 +1,4 @@
import { OrganizationService } from "../../abstractions/organization.service";
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";

View File

@@ -3,14 +3,16 @@ import { BehaviorSubject, concatMap } from "rxjs";
import { LogService } from "../abstractions/log.service";
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
import { StateMigrationService } from "../abstractions/stateMigration.service";
import { AbstractStorageService } from "../abstractions/storage.service";
import {
MemoryStorageServiceInterface,
AbstractStorageService,
} from "../abstractions/storage.service";
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
import { KdfType } from "../enums/kdfType";
import { StorageLocation } from "../enums/storageLocation";
import { ThemeType } from "../enums/themeType";
import { UriMatchType } from "../enums/uriMatchType";
import { StateFactory } from "../factories/stateFactory";
import { Utils } from "../misc/utils";
import { CipherData } from "../models/data/cipherData";
import { CollectionData } from "../models/data/collectionData";
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
@@ -56,6 +58,8 @@ const partialKeys = {
masterKey: "_masterkey",
};
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
export class StateService<
TGlobalState extends GlobalState = GlobalState,
TAccount extends Account = Account
@@ -76,7 +80,7 @@ export class StateService<
constructor(
protected storageService: AbstractStorageService,
protected secureStorageService: AbstractStorageService,
protected memoryStorageService: AbstractStorageService,
protected memoryStorageService: AbstractStorageService & MemoryStorageServiceInterface,
protected logService: LogService,
protected stateMigrationService: StateMigrationService,
protected stateFactory: StateFactory<TGlobalState, TAccount>,
@@ -150,6 +154,9 @@ export class StateService<
return;
}
await this.updateState(async (state) => {
if (state.accounts == null) {
state.accounts = {};
}
state.accounts[userId] = this.createAccount();
const diskAccount = await this.getAccountFromDisk({ userId: userId });
state.accounts[userId].profile = diskAccount.profile;
@@ -494,11 +501,11 @@ export class StateService<
);
}
@withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys?.cryptoMasterKey;
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.cryptoMasterKey;
}
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
@@ -604,23 +611,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
}
async getDecodedToken(options?: StorageOptions): Promise<any> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.tokens?.decodedToken;
}
async setDecodedToken(value: any, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.tokens.decodedToken = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
return (
@@ -657,11 +647,11 @@ export class StateService<
);
}
@withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys?.cryptoSymmetricKey?.decrypted;
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.cryptoSymmetricKey?.decrypted;
}
async setDecryptedCryptoSymmetricKey(
@@ -678,14 +668,13 @@ export class StateService<
);
}
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
async getDecryptedOrganizationKeys(
options?: StorageOptions
): Promise<Map<string, SymmetricCryptoKey>> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.organizationKeys?.decrypted;
return this.recordToMap(account?.keys?.organizationKeys?.decrypted);
}
async setDecryptedOrganizationKeys(
@@ -695,7 +684,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.organizationKeys.decrypted = value;
account.keys.organizationKeys.decrypted = this.mapToRecord(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -725,7 +714,6 @@ export class StateService<
);
}
@withPrototype(EncString)
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -762,14 +750,9 @@ export class StateService<
}
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
const privateKey = (
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys?.privateKey;
let result = privateKey?.decrypted;
if (result == null && privateKey?.decryptedSerialized != null) {
result = Utils.fromByteStringToArray(privateKey.decryptedSerialized);
}
return result;
)?.keys?.privateKey.decrypted;
}
async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
@@ -777,21 +760,19 @@ export class StateService<
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.privateKey.decrypted = value;
account.keys.privateKey.decryptedSerialized =
value == null ? null : Utils.fromBufferToByteString(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
async getDecryptedProviderKeys(
options?: StorageOptions
): Promise<Map<string, SymmetricCryptoKey>> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys?.providerKeys?.decrypted;
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return this.recordToMap(account?.keys?.providerKeys?.decrypted);
}
async setDecryptedProviderKeys(
@@ -801,7 +782,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.providerKeys.decrypted = value;
account.keys.providerKeys.decrypted = this.mapToRecord(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -1029,6 +1010,24 @@ export class StateService<
);
}
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(DDG_SHARED_KEY, options);
}
async setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
value == null
? await this.secureStorageService.remove(DDG_SHARED_KEY, options)
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
}
async getEmail(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@@ -1187,6 +1186,27 @@ export class StateService<
);
}
async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableDuckDuckGoBrowserIntegration ?? false
);
}
async setEnableDuckDuckGoBrowserIntegration(
value: boolean,
options?: StorageOptions
): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.enableDuckDuckGoBrowserIntegration = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEnableFullWidth(options?: StorageOptions): Promise<boolean> {
return (
(
@@ -1538,7 +1558,6 @@ export class StateService<
);
}
@withPrototype(EnvironmentUrls)
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
if ((await this.state())?.activeUserId == null) {
return await this.getGlobalEnvironmentUrls(options);
@@ -1908,12 +1927,18 @@ export class StateService<
);
}
/**
* @deprecated Do not call this directly, use OrganizationService
*/
async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.data?.organizations;
}
/**
* @deprecated Do not call this directly, use OrganizationService
*/
async setOrganizations(
value: { [id: string]: OrganizationData },
options?: StorageOptions
@@ -2021,11 +2046,7 @@ export class StateService<
const keys = (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys;
let result = keys?.publicKey;
if (result == null && keys?.publicKeySerialized != null) {
result = Utils.fromByteStringToArray(keys.publicKeySerialized);
}
return result;
return keys?.publicKey;
}
async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
@@ -2033,7 +2054,6 @@ export class StateService<
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.publicKey = value;
account.keys.publicKeySerialized = value == null ? null : Utils.fromBufferToByteString(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -2741,8 +2761,11 @@ export class StateService<
: await this.secureStorageService.save(`${options.userId}${key}`, value, options);
}
protected state(): Promise<State<TGlobalState, TAccount>> {
return this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state);
protected async state(): Promise<State<TGlobalState, TAccount>> {
const state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state, {
deserializer: (s) => State.fromJSON(s),
});
return state;
}
private async setState(state: State<TGlobalState, TAccount>): Promise<void> {
@@ -2761,6 +2784,14 @@ export class StateService<
await this.setState(updatedState);
});
}
private mapToRecord<V>(map: Map<string, V>): Record<string, V> {
return map == null ? null : Object.fromEntries(map);
}
private recordToMap<V>(record: Record<string, V>): Map<string, V> {
return record == null ? null : new Map(Object.entries(record));
}
}
export function withPrototype<T>(
@@ -2893,52 +2924,3 @@ function withPrototypeForObjectValues<T>(
};
};
}
function withPrototypeForMap<T>(
valuesConstructor: new (...args: any[]) => T,
valuesConverter: (input: any) => T = (i) => i
): (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => { value: (...args: any[]) => Promise<Map<string, T>> } {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
return {
value: function (...args: any[]) {
const originalResult: Promise<any> = originalMethod.apply(this, args);
if (!(originalResult instanceof Promise)) {
throw new Error(
`Error applying prototype to stored value -- result is not a promise for method ${String(
propertyKey
)}`
);
}
return originalResult.then((result) => {
if (result == null) {
return null;
} else if (result instanceof Map) {
return result;
} else {
for (const key in Object.keys(result)) {
result[key] =
result[key] == null ||
result[key].constructor.name === valuesConstructor.prototype.constructor.name
? valuesConverter(result[key])
: valuesConverter(
Object.create(
valuesConstructor.prototype,
Object.getOwnPropertyDescriptors(result[key])
)
);
}
return new Map<string, T>(Object.entries(result));
}
});
},
};
};
}

View File

@@ -12,7 +12,13 @@ import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { Account, AccountSettings, AccountSettingsSettings } from "../models/domain/account";
import {
Account,
AccountSettings,
AccountSettingsSettings,
EncryptionPair,
} from "../models/domain/account";
import { EncString } from "../models/domain/encString";
import { EnvironmentUrls } from "../models/domain/environmentUrls";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { GlobalState } from "../models/domain/globalState";
@@ -314,10 +320,10 @@ export class StateMigrationService<
passwordGenerationOptions:
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
defaultAccount.settings.passwordGenerationOptions,
pinProtected: {
pinProtected: Object.assign(new EncryptionPair<string, EncString>(), {
decrypted: null,
encrypted: await this.get<string>(v1Keys.pinProtected),
},
}),
protectedPin: await this.get<string>(v1Keys.protectedPin),
settings:
userId == null

View File

@@ -1,5 +1,3 @@
import { Subject } from "rxjs";
import { ApiService } from "../../abstractions/api.service";
import { CipherService } from "../../abstractions/cipher.service";
import { CollectionService } from "../../abstractions/collection.service";
@@ -9,18 +7,17 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service.
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { OrganizationService } from "../../abstractions/organization.service";
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../abstractions/provider.service";
import { SendService } from "../../abstractions/send.service";
import { SettingsService } from "../../abstractions/settings.service";
import { StateService } from "../../abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { sequentialize } from "../../misc/sequentialize";
import { CipherData } from "../../models/data/cipherData";
import { CollectionData } from "../../models/data/collectionData";
import { FolderData } from "../../models/data/folderData";
import { OrganizationData } from "../../models/data/organizationData";
import { PolicyData } from "../../models/data/policyData";
import { ProviderData } from "../../models/data/providerData";
import { SendData } from "../../models/data/sendData";
@@ -36,15 +33,10 @@ import {
import { PolicyResponse } from "../../models/response/policyResponse";
import { ProfileResponse } from "../../models/response/profileResponse";
import { SendResponse } from "../../models/response/sendResponse";
import { SyncEventArgs } from "../../types/syncEventArgs";
export class SyncService implements SyncServiceAbstraction {
syncInProgress = false;
private _sync = new Subject<SyncEventArgs>();
sync$ = this._sync.asObservable();
constructor(
private apiService: ApiService,
private settingsService: SettingsService,
@@ -58,9 +50,9 @@ export class SyncService implements SyncServiceAbstraction {
private logService: LogService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private organizationService: OrganizationService,
private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction,
private syncNotifierService: SyncNotifierService,
private logoutCallback: (expired: boolean) => Promise<void>
) {}
@@ -84,8 +76,10 @@ export class SyncService implements SyncServiceAbstraction {
@sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
this.syncNotifierService.next({ status: "Started" });
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
@@ -101,6 +95,7 @@ export class SyncService implements SyncServiceAbstraction {
if (!needsSync) {
await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
@@ -117,11 +112,13 @@ export class SyncService implements SyncServiceAbstraction {
await this.syncPolicies(response.policies);
await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: true, data: response });
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
} else {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
}
@@ -272,13 +269,11 @@ export class SyncService implements SyncServiceAbstraction {
private syncStarted() {
this.syncInProgress = true;
this.messagingService.send("syncStarted");
this._sync.next({ status: "Started" });
}
private syncCompleted(successfully: boolean): boolean {
this.syncInProgress = false;
this.messagingService.send("syncCompleted", { successfully: successfully });
this._sync.next({ status: successfully ? "SuccessfullyCompleted" : "UnsuccessfullyCompleted" });
return successfully;
}
@@ -320,24 +315,11 @@ export class SyncService implements SyncServiceAbstraction {
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
const providers: { [id: string]: ProviderData } = {};
response.providers.forEach((p) => {
providers[p.id] = new ProviderData(p);
});
response.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.organizationService.save(organizations);
await this.providerService.save(providers);
if (await this.keyConnectorService.userNeedsMigration()) {

View File

@@ -0,0 +1,18 @@
import { Subject } from "rxjs";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { SyncEventArgs } from "../../types/syncEventArgs";
/**
* This class should most likely have 0 dependencies because it will hopefully
* be rolled into SyncService once upon a time.
*/
export class SyncNotifierService implements SyncNotifierServiceAbstraction {
private _sync = new Subject<SyncEventArgs>();
sync$ = this._sync.asObservable();
next(event: SyncEventArgs): void {
this._sync.next(event);
}
}

View File

@@ -93,11 +93,6 @@ export class TokenService implements TokenServiceAbstraction {
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
async decodeToken(token?: string): Promise<any> {
const storedToken = await this.stateService.getDecodedToken();
if (token === null && storedToken != null) {
return storedToken;
}
token = token ?? (await this.stateService.getAccessToken());
if (token == null) {

View File

@@ -81,7 +81,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t("verificationCodeRequired"));
} else {
throw new Error(this.i18nService.t("masterPassRequired"));
throw new Error(this.i18nService.t("masterPasswordRequired"));
}
}
}

View File

@@ -0,0 +1,9 @@
type CheckableBase = {
checked?: boolean;
};
export type Checkable<T> = T & CheckableBase;
export function isChecked(item: CheckableBase): boolean {
return !!item.checked;
}

View File

@@ -0,0 +1,44 @@
import {
PositiveInfinity,
NegativeInfinity,
JsonPrimitive,
TypedArray,
JsonValue,
} from "type-fest";
import { NotJsonable } from "type-fest/source/jsonify";
/**
* Extracted from type-fest and extended with Jsonification of objects returned from `toJSON` methods.
*/
export type DeepJsonify<T> =
// Check if there are any non-JSONable types represented in the union.
// Note: The use of tuples in this first condition side-steps distributive conditional types
// (see https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532)
[Extract<T, NotJsonable | bigint>] extends [never]
? T extends PositiveInfinity | NegativeInfinity ? null
: T extends JsonPrimitive
? T // Primitive is acceptable
: T extends number ? number
: T extends string ? string
: T extends boolean ? boolean
: T extends Map<any, any> | Set<any> ? Record<string, unknown> // {}
: T extends TypedArray ? Record<string, number>
: T extends Array<infer U>
? Array<DeepJsonify<U extends NotJsonable ? null : U>> // It's an array: recursive call for its children
: T extends object
? T extends { toJSON(): infer J }
? (() => J) extends () => JsonValue // Is J assignable to JsonValue?
? J // Then T is Jsonable and its Jsonable value is J
: {[P in keyof J as P extends symbol
? never
: J[P] extends NotJsonable
? never
: P]: DeepJsonify<Required<J>[P]>;
} // Not Jsonable because its toJSON() method does not return JsonValue
: {[P in keyof T as P extends symbol
? never
: T[P] extends NotJsonable
? never
: P]: DeepJsonify<Required<T>[P]>} // It's an object: recursive call for its children
: never // Otherwise any other non-object is removed
: never; // Otherwise non-JSONable type union was found not empty

View File

@@ -1,15 +1,38 @@
import { filter } from "rxjs";
import { SyncResponse } from "../models/response/syncResponse";
export type SyncStatus = "Started" | "SuccessfullyCompleted" | "UnsuccessfullyCompleted";
type SyncStatus = "Started" | "Completed";
export type SyncEventArgs = {
status: SyncStatus;
type SyncEventArgsBase<T extends SyncStatus> = {
status: T;
};
type SyncCompletedEventArgsBase<T extends boolean> = SyncEventArgsBase<"Completed"> & {
successfully: T;
};
type SyncSuccessfullyCompletedEventArgs = SyncCompletedEventArgsBase<true> & {
data: SyncResponse;
};
export type SyncEventArgs =
| SyncSuccessfullyCompletedEventArgs
| SyncCompletedEventArgsBase<false>
| SyncEventArgsBase<"Started">;
/**
* Helper function to filter only on successfully completed syncs
* @returns a function that can be used in a `.pipe()` from an observable
* @returns a function that can be used in a `.pipe(filter(...))` from an observable
* @example
* ```
* of<SyncEventArgs>({ status: "Completed", successfully: true, data: new SyncResponse() })
* .pipe(filter(isSuccessfullyCompleted))
* .subscribe(event => {
* console.log(event.data);
* });
* ```
*/
export function onlySuccessfullyCompleted() {
return filter<SyncEventArgs>((syncEvent) => syncEvent.status === "SuccessfullyCompleted");
export function isSuccessfullyCompleted(
syncEvent: SyncEventArgs
): syncEvent is SyncSuccessfullyCompletedEventArgs {
return syncEvent.status === "Completed" && syncEvent.successfully;
}