1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 22:44:11 +00:00

Merge branch 'master' into PS55-6-22

This commit is contained in:
CarleyDiaz-Bitwarden
2022-07-21 17:10:45 -04:00
431 changed files with 13919 additions and 5118 deletions

View File

@@ -1,3 +1,4 @@
import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
import { PolicyType } from "../enums/policyType";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
@@ -23,7 +24,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergencyAccessI
import { EmergencyAccessPasswordRequest } from "../models/request/emergencyAccessPasswordRequest";
import { EmergencyAccessUpdateRequest } from "../models/request/emergencyAccessUpdateRequest";
import { EventRequest } from "../models/request/eventRequest";
import { FolderRequest } from "../models/request/folderRequest";
import { GroupRequest } from "../models/request/groupRequest";
import { IapCheckRequest } from "../models/request/iapCheckRequest";
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
@@ -117,7 +117,6 @@ import {
EmergencyAccessViewResponse,
} from "../models/response/emergencyAccessResponse";
import { EventResponse } from "../models/response/eventResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { GroupDetailsResponse, GroupResponse } from "../models/response/groupResponse";
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
@@ -182,6 +181,16 @@ import { UserKeyResponse } from "../models/response/userKeyResponse";
import { SendAccessView } from "../models/view/sendAccessView";
export abstract class ApiService {
send: (
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
body: any,
authed: boolean,
hasResponse: boolean,
apiUrl?: string,
alterHeaders?: (headers: Headers) => void
) => Promise<any>;
postIdentityToken: (
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
@@ -228,11 +237,6 @@ export abstract class ApiService {
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
getFolder: (id: string) => Promise<FolderResponse>;
postFolder: (request: FolderRequest) => Promise<FolderResponse>;
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;
deleteFolder: (id: string) => Promise<any>;
getSend: (id: string) => Promise<SendResponse>;
postSendAccess: (
id: string,
@@ -259,6 +263,7 @@ export abstract class ApiService {
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
getCipher: (id: string) => Promise<CipherResponse>;
getFullCipherDetails: (id: string) => Promise<CipherResponse>;
getCipherAdmin: (id: string) => Promise<CipherResponse>;
getAttachmentData: (
cipherId: string,
@@ -569,7 +574,8 @@ export abstract class ApiService {
request: OrganizationApiKeyRequest
) => Promise<ApiKeyResponse>;
getOrganizationApiKeyInformation: (
id: string
id: string,
type?: OrganizationApiKeyType
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
postOrganizationRotateApiKey: (
id: string,

View File

@@ -9,6 +9,7 @@ export type Urls = {
notifications?: string;
events?: string;
keyConnector?: string;
scim?: string;
};
export type PayPalConfig = {
@@ -28,6 +29,7 @@ export abstract class EnvironmentService {
getIdentityUrl: () => string;
getEventsUrl: () => string;
getKeyConnectorUrl: () => string;
getScimUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
getUrls: () => Urls;

View File

@@ -1,21 +0,0 @@
import { FolderData } from "../models/data/folderData";
import { Folder } from "../models/domain/folder";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TreeNode } from "../models/domain/treeNode";
import { FolderView } from "../models/view/folderView";
export abstract class FolderService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>;
getAll: () => Promise<Folder[]>;
getAllDecrypted: () => Promise<FolderView[]>;
getAllNested: (folders?: FolderView[]) => Promise<TreeNode<FolderView>[]>;
getNested: (id: string) => Promise<TreeNode<FolderView>>;
saveWithServer: (folder: Folder) => Promise<any>;
upsert: (folder: FolderData | FolderData[]) => Promise<any>;
replace: (folders: { [id: string]: FolderData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
}

View File

@@ -0,0 +1,8 @@
import { Folder } from "@bitwarden/common/models/domain/folder";
import { FolderResponse } from "@bitwarden/common/models/response/folderResponse";
export class FolderApiServiceAbstraction {
save: (folder: Folder) => Promise<any>;
delete: (id: string) => Promise<any>;
get: (id: string) => Promise<FolderResponse>;
}

View File

@@ -0,0 +1,26 @@
import { Observable } from "rxjs";
import { FolderData } from "../../models/data/folderData";
import { Folder } from "../../models/domain/folder";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { FolderView } from "../../models/view/folderView";
export abstract class FolderService {
folders$: Observable<Folder[]>;
folderViews$: Observable<FolderView[]>;
clearCache: () => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>;
/**
* @deprecated Only use in CLI!
*/
getAllDecryptedFromState: () => Promise<FolderView[]>;
}
export abstract class InternalFolderService extends FolderService {
upsert: (folder: FolderData | FolderData[]) => Promise<void>;
replace: (folders: { [id: string]: FolderData }) => Promise<void>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -0,0 +1,13 @@
import { AbstractControl } from "@angular/forms";
export interface AllValidationErrors {
controlName: string;
errorName: string;
}
export interface FormGroupControls {
[key: string]: AbstractControl;
}
export abstract class FormValidationErrorsService {
getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[];
}

View File

@@ -1,5 +1,7 @@
import { Observable } from "rxjs";
export abstract class I18nService {
locale: string;
locale$: Observable<string>;
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;

View File

@@ -1,10 +1,11 @@
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, Observable } from "rxjs";
import { KdfType } from "../enums/kdfType";
import { ThemeType } from "../enums/themeType";
import { UriMatchType } from "../enums/uriMatchType";
import { CipherData } from "../models/data/cipherData";
import { CollectionData } from "../models/data/collectionData";
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
import { EventData } from "../models/data/eventData";
import { FolderData } from "../models/data/folderData";
import { OrganizationData } from "../models/data/organizationData";
@@ -21,13 +22,14 @@ import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { WindowState } from "../models/domain/windowState";
import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView";
import { FolderView } from "../models/view/folderView";
import { SendView } from "../models/view/sendView";
export abstract class StateService<T extends Account = Account> {
accounts: BehaviorSubject<{ [userId: string]: T }>;
activeAccount: BehaviorSubject<string>;
activeAccountUnlocked: Observable<boolean>;
addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>;
@@ -88,8 +90,6 @@ export abstract class StateService<T extends Account = Account> {
value: SymmetricCryptoKey,
options?: StorageOptions
) => Promise<void>;
getDecryptedFolders: (options?: StorageOptions) => Promise<FolderView[]>;
setDecryptedFolders: (value: FolderView[], options?: StorageOptions) => Promise<void>;
getDecryptedOrganizationKeys: (
options?: StorageOptions
) => Promise<Map<string, SymmetricCryptoKey>>;
@@ -183,14 +183,22 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>;
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use FolderService
*/
getEncryptedFolders: (options?: StorageOptions) => Promise<{ [id: string]: FolderData }>;
/**
* @deprecated Do not call this directly, use FolderService
*/
setEncryptedFolders: (
value: { [id: string]: FolderData },
options?: StorageOptions
) => Promise<void>;
getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise<any>;
getEncryptedOrganizationKeys: (
options?: StorageOptions
) => Promise<{ [orgId: string]: EncryptedOrganizationKeyData }>;
setEncryptedOrganizationKeys: (
value: Map<string, SymmetricCryptoKey>,
value: { [orgId: string]: EncryptedOrganizationKeyData },
options?: StorageOptions
) => Promise<void>;
getEncryptedPasswordGenerationHistory: (

View File

@@ -48,6 +48,8 @@ export enum EventType {
OrganizationUser_AdminResetPassword = 1508,
OrganizationUser_ResetSsoLink = 1509,
OrganizationUser_FirstSsoLogin = 1510,
OrganizationUser_Deactivated = 1511,
OrganizationUser_Activated = 1512,
Organization_Updated = 1600,
Organization_PurgedVault = 1601,

View File

@@ -1,4 +1,5 @@
export enum OrganizationApiKeyType {
Default = 0,
BillingSync = 1,
Scim = 2,
}

View File

@@ -1,3 +1,4 @@
export enum OrganizationConnectionType {
CloudBillingSync = 1,
Scim = 2,
}

View File

@@ -25,4 +25,5 @@ export enum Permissions {
DeleteAssignedCollections,
ManageSso,
ManageBilling,
ManageScim,
}

View File

@@ -0,0 +1,9 @@
export enum ScimProviderType {
Default = 0,
AzureAd = 1,
Okta = 2,
OneLogin = 3,
JumpCloud = 4,
GoogleWorkspace = 5,
Rippling = 6,
}

View File

@@ -3,5 +3,6 @@ export enum StateVersion {
Two = 2, // Move to a typed State object
Three = 3, // Fix migration of users' premium status
Four = 4, // Fix 'Never Lock' option by removing stale data
Latest = Four,
Five = 5, // Migrate to new storage of encrypted organization keys
Latest = Five,
}

View File

@@ -301,6 +301,12 @@ export abstract class BaseImporter {
return "Visa";
}
// Mir
re = new RegExp("^220[0-4]");
if (cardNum.match(re) != null) {
return "Mir";
}
return null;
}

View File

@@ -25,6 +25,7 @@ export class PermissionsApi extends BaseResponse {
managePolicies: boolean;
manageUsers: boolean;
manageResetPassword: boolean;
manageScim: boolean;
constructor(data: any = null) {
super(data);
@@ -51,5 +52,6 @@ export class PermissionsApi extends BaseResponse {
this.managePolicies = this.getResponseProperty("ManagePolicies");
this.manageUsers = this.getResponseProperty("ManageUsers");
this.manageResetPassword = this.getResponseProperty("ManageResetPassword");
this.manageScim = this.getResponseProperty("ManageScim");
}
}

View File

@@ -0,0 +1,17 @@
import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType";
import { BaseResponse } from "../response/baseResponse";
export class ScimConfigApi extends BaseResponse {
enabled: boolean;
scimProvider: ScimProviderType;
constructor(data: any) {
super(data);
if (data == null) {
return;
}
this.enabled = this.getResponseProperty("Enabled");
this.scimProvider = this.getResponseProperty("ScimProvider");
}
}

View File

@@ -0,0 +1,14 @@
export type EncryptedOrganizationKeyData =
| OrganizationEncryptedOrganizationKeyData
| ProviderEncryptedOrganizationKeyData;
type OrganizationEncryptedOrganizationKeyData = {
type: "organization";
key: string;
};
type ProviderEncryptedOrganizationKeyData = {
type: "provider";
key: string;
providerId: string;
};

View File

@@ -19,6 +19,7 @@ export class OrganizationData {
useApi: boolean;
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useResetPassword: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -58,6 +59,7 @@ export class OrganizationData {
this.useApi = response.useApi;
this.useSso = response.useSso;
this.useKeyConnector = response.useKeyConnector;
this.useScim = response.useScim;
this.useResetPassword = response.useResetPassword;
this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium;

View File

@@ -3,6 +3,7 @@ import { KdfType } from "../../enums/kdfType";
import { UriMatchType } from "../../enums/uriMatchType";
import { CipherData } from "../data/cipherData";
import { CollectionData } from "../data/collectionData";
import { EncryptedOrganizationKeyData } from "../data/encryptedOrganizationKeyData";
import { EventData } from "../data/eventData";
import { FolderData } from "../data/folderData";
import { OrganizationData } from "../data/organizationData";
@@ -11,7 +12,6 @@ import { ProviderData } from "../data/providerData";
import { SendData } from "../data/sendData";
import { CipherView } from "../view/cipherView";
import { CollectionView } from "../view/collectionView";
import { FolderView } from "../view/folderView";
import { SendView } from "../view/sendView";
import { EncString } from "./encString";
@@ -31,15 +31,19 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
decrypted?: TDecrypted[];
}
// This is a temporary structure to handle migrated `DataEncryptionPair` to
// avoid needing a data migration at this stage. It should be replaced with
// proper data migrations when `DataEncryptionPair` is deprecated.
export class TemporaryDataEncryption<TEncrypted> {
encrypted?: { [id: string]: TEncrypted };
}
export class AccountData {
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<
CipherData,
CipherView
>();
folders?: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<
FolderData,
FolderView
>();
folders? = new TemporaryDataEncryption<FolderData>();
localData?: any;
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
collections?: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<
@@ -66,8 +70,11 @@ export class AccountKeys {
string,
SymmetricCryptoKey
>();
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
any,
organizationKeys?: EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
> = new EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
>();
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<

View File

@@ -0,0 +1,56 @@
import { CryptoService } from "../../abstractions/crypto.service";
import { EncryptedOrganizationKeyData } from "../../models/data/encryptedOrganizationKeyData";
import { EncString } from "./encString";
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export abstract class BaseEncryptedOrganizationKey {
decrypt: (cryptoService: CryptoService) => Promise<SymmetricCryptoKey>;
static fromData(data: EncryptedOrganizationKeyData) {
switch (data.type) {
case "organization":
return new EncryptedOrganizationKey(data.key);
case "provider":
return new ProviderEncryptedOrganizationKey(data.key, data.providerId);
default:
return null;
}
}
}
export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
constructor(private key: string) {}
async decrypt(cryptoService: CryptoService) {
const decValue = await cryptoService.rsaDecrypt(this.key);
return new SymmetricCryptoKey(decValue);
}
toData(): EncryptedOrganizationKeyData {
return {
type: "organization",
key: this.key,
};
}
}
export class ProviderEncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
constructor(private key: string, private providerId: string) {}
async decrypt(cryptoService: CryptoService) {
const providerKey = await cryptoService.getProviderKey(this.providerId);
const decValue = await cryptoService.decryptToBytes(new EncString(this.key), providerKey);
return new SymmetricCryptoKey(decValue);
}
toData(): EncryptedOrganizationKeyData {
return {
type: "provider",
key: this.key,
providerId: this.providerId,
};
}
}

View File

@@ -20,6 +20,7 @@ export class Organization {
useApi: boolean;
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useResetPassword: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -63,6 +64,7 @@ export class Organization {
this.useApi = obj.useApi;
this.useSso = obj.useSso;
this.useKeyConnector = obj.useKeyConnector;
this.useScim = obj.useScim;
this.useResetPassword = obj.useResetPassword;
this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium;
@@ -173,6 +175,10 @@ export class Organization {
return this.isAdmin || this.permissions.manageSso;
}
get canManageScim() {
return this.isAdmin || this.permissions.manageScim;
}
get canManagePolicies() {
return this.isAdmin || this.permissions.managePolicies;
}
@@ -207,6 +213,7 @@ export class Organization {
(permissions.includes(Permissions.ManageUsers) && this.canManageUsers) ||
(permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) ||
(permissions.includes(Permissions.ManageSso) && this.canManageSso) ||
(permissions.includes(Permissions.ManageScim) && this.canManageScim) ||
(permissions.includes(Permissions.ManageBilling) && this.canManageBilling);
return specifiedPermissions && (this.enabled || this.isOwner);

View File

@@ -1,9 +1,10 @@
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
import { BillingSyncConfigRequest } from "./billingSyncConfigRequest";
import { ScimConfigRequest } from "./scimConfigRequest";
/**API request config types for OrganizationConnectionRequest */
export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest;
export type OrganizationConnectionRequestConfigs = BillingSyncConfigRequest | ScimConfigRequest;
export class OrganizationConnectionRequest {
constructor(

View File

@@ -2,5 +2,6 @@ import { SecretVerificationRequest } from "./secretVerificationRequest";
export class PasswordRequest extends SecretVerificationRequest {
newMasterPasswordHash: string;
masterPasswordHint: string;
key: string;
}

View File

@@ -0,0 +1,5 @@
import { ScimProviderType } from "@bitwarden/common/enums/scimProviderType";
export class ScimConfigRequest {
constructor(private enabled: boolean, private scimProvider: ScimProviderType = null) {}
}

View File

@@ -21,15 +21,13 @@ export class ErrorResponse extends BaseResponse {
}
}
if (errorModel) {
if (status === 429) {
this.message = "Rate limit exceeded. Try again later.";
} else if (errorModel) {
this.message = this.getResponseProperty("Message", errorModel);
this.validationErrors = this.getResponseProperty("ValidationErrors", errorModel);
this.captchaSiteKey = this.validationErrors?.HCaptcha_SiteKey?.[0];
this.captchaRequired = !Utils.isNullOrWhitespace(this.captchaSiteKey);
} else {
if (status === 429) {
this.message = "Rate limit exceeded. Try again later.";
}
}
this.statusCode = status;
}

View File

@@ -1,10 +1,11 @@
import { OrganizationConnectionType } from "../../enums/organizationConnectionType";
import { BillingSyncConfigApi } from "../api/billingSyncConfigApi";
import { ScimConfigApi } from "../api/scimConfigApi";
import { BaseResponse } from "./baseResponse";
/**API response config types for OrganizationConnectionResponse */
export type OrganizationConnectionConfigApis = BillingSyncConfigApi;
export type OrganizationConnectionConfigApis = BillingSyncConfigApi | ScimConfigApi;
export class OrganizationConnectionResponse<
TConfig extends OrganizationConnectionConfigApis

View File

@@ -17,6 +17,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
useApi: boolean;
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useResetPassword: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -57,6 +58,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.useApi = this.getResponseProperty("UseApi");
this.useSso = this.getResponseProperty("UseSso");
this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false;
this.useScim = this.getResponseProperty("UseScim") ?? false;
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.selfHost = this.getResponseProperty("SelfHost");
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");

View File

@@ -4,6 +4,7 @@ import { EnvironmentService } from "../abstractions/environment.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { TokenService } from "../abstractions/token.service";
import { DeviceType } from "../enums/deviceType";
import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
import { PolicyType } from "../enums/policyType";
import { Utils } from "../misc/utils";
@@ -30,7 +31,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergencyAccessI
import { EmergencyAccessPasswordRequest } from "../models/request/emergencyAccessPasswordRequest";
import { EmergencyAccessUpdateRequest } from "../models/request/emergencyAccessUpdateRequest";
import { EventRequest } from "../models/request/eventRequest";
import { FolderRequest } from "../models/request/folderRequest";
import { GroupRequest } from "../models/request/groupRequest";
import { IapCheckRequest } from "../models/request/iapCheckRequest";
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
@@ -126,7 +126,6 @@ import {
} from "../models/response/emergencyAccessResponse";
import { ErrorResponse } from "../models/response/errorResponse";
import { EventResponse } from "../models/response/eventResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { GroupDetailsResponse, GroupResponse } from "../models/response/groupResponse";
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
@@ -487,27 +486,6 @@ export class ApiService implements ApiServiceAbstraction {
return new BillingPaymentResponse(r);
}
// Folder APIs
async getFolder(id: string): Promise<FolderResponse> {
const r = await this.send("GET", "/folders/" + id, null, true, true);
return new FolderResponse(r);
}
async postFolder(request: FolderRequest): Promise<FolderResponse> {
const r = await this.send("POST", "/folders", request, true, true);
return new FolderResponse(r);
}
async putFolder(id: string, request: FolderRequest): Promise<FolderResponse> {
const r = await this.send("PUT", "/folders/" + id, request, true, true);
return new FolderResponse(r);
}
deleteFolder(id: string): Promise<any> {
return this.send("DELETE", "/folders/" + id, null, true, false);
}
// Send APIs
async getSend(id: string): Promise<SendResponse> {
@@ -612,6 +590,11 @@ export class ApiService implements ApiServiceAbstraction {
return new CipherResponse(r);
}
async getFullCipherDetails(id: string): Promise<CipherResponse> {
const r = await this.send("GET", "/ciphers/" + id + "/details", null, true, true);
return new CipherResponse(r);
}
async getCipherAdmin(id: string): Promise<CipherResponse> {
const r = await this.send("GET", "/ciphers/" + id + "/admin", null, true, true);
return new CipherResponse(r);
@@ -1412,7 +1395,7 @@ export class ApiService implements ApiServiceAbstraction {
// Plan APIs
async getPlans(): Promise<ListResponse<PlanResponse>> {
const r = await this.send("GET", "/plans/", null, true, true);
const r = await this.send("GET", "/plans/", null, false, true);
return new ListResponse(r, PlanResponse);
}
@@ -1840,15 +1823,14 @@ export class ApiService implements ApiServiceAbstraction {
}
async getOrganizationApiKeyInformation(
id: string
id: string,
type: OrganizationApiKeyType = null
): Promise<ListResponse<OrganizationApiKeyInformationResponse>> {
const r = await this.send(
"GET",
"/organizations/" + id + "/api-key-information",
null,
true,
true
);
const uri =
type === null
? "/organizations/" + id + "/api-key-information"
: "/organizations/" + id + "/api-key-information/" + type;
const r = await this.send("GET", uri, null, true, true);
return new ListResponse(r, OrganizationApiKeyInformationResponse);
}
@@ -2566,7 +2548,7 @@ export class ApiService implements ApiServiceAbstraction {
await this.tokenService.setToken(response.accessToken);
}
private async send(
async send(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
body: any,

View File

@@ -86,6 +86,7 @@ export class CollectionService implements CollectionServiceAbstraction {
const collections = await this.getAll();
decryptedCollections = await this.decryptMany(collections);
await this.stateService.setDecryptedCollections(decryptedCollections);
return decryptedCollections;
}

View File

@@ -13,9 +13,11 @@ import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { sequentialize } from "../misc/sequentialize";
import { Utils } from "../misc/utils";
import { EEFLongWordList } from "../misc/wordlist";
import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { EncryptedObject } from "../models/domain/encryptedObject";
import { BaseEncryptedOrganizationKey } from "../models/domain/encryptedOrganizationKey";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse";
@@ -58,23 +60,28 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[]
orgs: ProfileOrganizationResponse[] = [],
providerOrgs: ProfileProviderOrganizationResponse[] = []
): Promise<void> {
const orgKeys: any = {};
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
orgKeys[org.id] = org.key;
encOrgKeyData[org.id] = {
type: "organization",
key: org.key,
};
});
for (const providerOrg of providerOrgs) {
// Convert provider encrypted keys to user encrypted.
const providerKey = await this.getProviderKey(providerOrg.providerId);
const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey);
orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString;
}
providerOrgs.forEach((org) => {
encOrgKeyData[org.id] = {
type: "provider",
providerId: org.providerId,
key: org.key,
};
});
await this.stateService.setDecryptedOrganizationKeys(null);
return await this.stateService.setEncryptedOrganizationKeys(orgKeys);
return await this.stateService.setEncryptedOrganizationKeys(encOrgKeyData);
}
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
@@ -211,35 +218,36 @@ export class CryptoService implements CryptoServiceAbstraction {
@sequentialize(() => "getOrgKeys")
async getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>> {
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
const result: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys();
if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) {
return decryptedOrganizationKeys;
}
const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys();
if (encOrgKeys == null) {
const encOrgKeyData = await this.stateService.getEncryptedOrganizationKeys();
if (encOrgKeyData == null) {
return null;
}
let setKey = false;
for (const orgId in encOrgKeys) {
// eslint-disable-next-line
if (!encOrgKeys.hasOwnProperty(orgId)) {
for (const orgId of Object.keys(encOrgKeyData)) {
if (result.has(orgId)) {
continue;
}
const decValue = await this.rsaDecrypt(encOrgKeys[orgId]);
orgKeys.set(orgId, new SymmetricCryptoKey(decValue));
const encOrgKey = BaseEncryptedOrganizationKey.fromData(encOrgKeyData[orgId]);
const decOrgKey = await encOrgKey.decrypt(this);
result.set(orgId, decOrgKey);
setKey = true;
}
if (setKey) {
await this.stateService.setDecryptedOrganizationKeys(orgKeys);
await this.stateService.setDecryptedOrganizationKeys(result);
}
return orgKeys;
return result;
}
async getOrgKey(orgId: string): Promise<SymmetricCryptoKey> {

View File

@@ -19,6 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private notificationsUrl: string;
private eventsUrl: string;
private keyConnectorUrl: string;
private scimUrl: string = null;
constructor(private stateService: StateService) {
this.stateService.activeAccount.subscribe(async () => {
@@ -111,6 +112,16 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return this.keyConnectorUrl;
}
getScimUrl() {
if (this.scimUrl != null) {
return this.scimUrl + "/v2";
}
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
? "https://scim.bitwarden.com/v2"
: this.getWebVaultUrl() + "/scim/v2";
}
async setUrlsFromStorage(): Promise<void> {
const urls: any = await this.stateService.getEnvironmentUrls();
const envUrls = new EnvironmentUrls();
@@ -123,6 +134,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
// scimUrl is not saved to storage
}
async setUrls(urls: Urls): Promise<Urls> {
@@ -135,6 +147,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
urls.events = this.formatUrl(urls.events);
urls.keyConnector = this.formatUrl(urls.keyConnector);
// scimUrl cannot be cleared
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
await this.stateService.setEnvironmentUrls({
base: urls.base,
api: urls.api,
@@ -144,6 +159,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
notifications: urls.notifications,
events: urls.events,
keyConnector: urls.keyConnector,
// scimUrl is not saved to storage
});
this.baseUrl = urls.base;
@@ -154,6 +170,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.notificationsUrl = urls.notifications;
this.eventsUrl = urls.events;
this.keyConnectorUrl = urls.keyConnector;
this.scimUrl = urls.scim;
this.urlsSubject.next(urls);
@@ -170,6 +187,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
notifications: this.notificationsUrl,
events: this.eventsUrl,
keyConnector: this.keyConnectorUrl,
scim: this.scimUrl,
};
}

View File

@@ -1,4 +1,5 @@
import * as papa from "papaparse";
import { firstValueFrom } from "rxjs";
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
@@ -8,7 +9,7 @@ import {
ExportFormat,
ExportService as ExportServiceAbstraction,
} from "../abstractions/export.service";
import { FolderService } from "../abstractions/folder.service";
import { FolderService } from "../abstractions/folder/folder.service.abstraction";
import { CipherType } from "../enums/cipherType";
import { DEFAULT_KDF_ITERATIONS, KdfType } from "../enums/kdfType";
import { Utils } from "../misc/utils";
@@ -115,7 +116,7 @@ export class ExportService implements ExportServiceAbstraction {
const promises = [];
promises.push(
this.folderService.getAllDecrypted().then((folders) => {
firstValueFrom(this.folderService.folderViews$).then((folders) => {
decFolders = folders;
})
);
@@ -191,7 +192,7 @@ export class ExportService implements ExportServiceAbstraction {
const promises = [];
promises.push(
this.folderService.getAll().then((f) => {
firstValueFrom(this.folderService.folders$).then((f) => {
folders = f;
})
);

View File

@@ -1,193 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService as FolderServiceAbstraction } from "../abstractions/folder.service";
import { I18nService } from "../abstractions/i18n.service";
import { StateService } from "../abstractions/state.service";
import { ServiceUtils } from "../misc/serviceUtils";
import { Utils } from "../misc/utils";
import { CipherData } from "../models/data/cipherData";
import { FolderData } from "../models/data/folderData";
import { Folder } from "../models/domain/folder";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TreeNode } from "../models/domain/treeNode";
import { FolderRequest } from "../models/request/folderRequest";
import { FolderResponse } from "../models/response/folderResponse";
import { FolderView } from "../models/view/folderView";
const NestingDelimiter = "/";
export class FolderService implements FolderServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private apiService: ApiService,
private i18nService: I18nService,
private cipherService: CipherService,
private stateService: StateService
) {}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
}
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
const folder = new Folder();
folder.id = model.id;
folder.name = await this.cryptoService.encrypt(model.name, key);
return folder;
}
async get(id: string): Promise<Folder> {
const folders = await this.stateService.getEncryptedFolders();
// eslint-disable-next-line
if (folders == null || !folders.hasOwnProperty(id)) {
return null;
}
return new Folder(folders[id]);
}
async getAll(): Promise<Folder[]> {
const folders = await this.stateService.getEncryptedFolders();
const response: Folder[] = [];
for (const id in folders) {
// eslint-disable-next-line
if (folders.hasOwnProperty(id)) {
response.push(new Folder(folders[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<FolderView[]> {
const decryptedFolders = await this.stateService.getDecryptedFolders();
if (decryptedFolders != null) {
return decryptedFolders;
}
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
const decFolders: FolderView[] = [];
const promises: Promise<any>[] = [];
const folders = await this.getAll();
folders.forEach((folder) => {
promises.push(folder.decrypt().then((f) => decFolders.push(f)));
});
await Promise.all(promises);
decFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
const noneFolder = new FolderView();
noneFolder.name = this.i18nService.t("noneFolder");
decFolders.push(noneFolder);
await this.stateService.setDecryptedFolders(decFolders);
return decFolders;
}
async getAllNested(folders?: FolderView[]): Promise<TreeNode<FolderView>[]> {
folders = folders ?? (await this.getAllDecrypted());
const nodes: TreeNode<FolderView>[] = [];
folders.forEach((f) => {
const folderCopy = new FolderView();
folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate;
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
});
return nodes;
}
async getNested(id: string): Promise<TreeNode<FolderView>> {
const folders = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
}
async saveWithServer(folder: Folder): Promise<any> {
const request = new FolderRequest(folder);
let response: FolderResponse;
if (folder.id == null) {
response = await this.apiService.postFolder(request);
folder.id = response.id;
} else {
response = await this.apiService.putFolder(folder.id, request);
}
const data = new FolderData(response);
await this.upsert(data);
}
async upsert(folder: FolderData | FolderData[]): Promise<any> {
let folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
folders = {};
}
if (folder instanceof FolderData) {
const f = folder as FolderData;
folders[f.id] = f;
} else {
(folder as FolderData[]).forEach((f) => {
folders[f.id] = f;
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async replace(folders: { [id: string]: FolderData }): Promise<any> {
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
await this.stateService.setEncryptedFolders(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
return;
}
if (typeof id === "string") {
if (folders[id] == null) {
return;
}
delete folders[id];
} else {
(id as string[]).forEach((i) => {
delete folders[i];
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
// Items in a deleted folder are re-assigned to "No Folder"
const ciphers = await this.stateService.getEncryptedCiphers();
if (ciphers != null) {
const updates: CipherData[] = [];
for (const cId in ciphers) {
if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null;
updates.push(ciphers[cId]);
}
}
if (updates.length > 0) {
this.cipherService.upsert(updates);
}
}
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteFolder(id);
await this.delete(id);
}
}

View File

@@ -0,0 +1,50 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { FolderData } from "@bitwarden/common/models/data/folderData";
import { Folder } from "@bitwarden/common/models/domain/folder";
import { FolderRequest } from "@bitwarden/common/models/request/folderRequest";
import { FolderResponse } from "@bitwarden/common/models/response/folderResponse";
export class FolderApiService implements FolderApiServiceAbstraction {
constructor(private folderService: InternalFolderService, private apiService: ApiService) {}
async save(folder: Folder): Promise<any> {
const request = new FolderRequest(folder);
let response: FolderResponse;
if (folder.id == null) {
response = await this.postFolder(request);
folder.id = response.id;
} else {
response = await this.putFolder(folder.id, request);
}
const data = new FolderData(response);
await this.folderService.upsert(data);
}
async delete(id: string): Promise<any> {
await this.deleteFolder(id);
await this.folderService.delete(id);
}
async get(id: string): Promise<FolderResponse> {
const r = await this.apiService.send("GET", "/folders/" + id, null, true, true);
return new FolderResponse(r);
}
private async postFolder(request: FolderRequest): Promise<FolderResponse> {
const r = await this.apiService.send("POST", "/folders", request, true, true);
return new FolderResponse(r);
}
async putFolder(id: string, request: FolderRequest): Promise<FolderResponse> {
const r = await this.apiService.send("PUT", "/folders/" + id, request, true, true);
return new FolderResponse(r);
}
private deleteFolder(id: string): Promise<any> {
return this.apiService.send("DELETE", "/folders/" + id, null, true, false);
}
}

View File

@@ -0,0 +1,163 @@
import { BehaviorSubject } from "rxjs";
import { CipherService } from "../../abstractions/cipher.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../abstractions/folder/folder.service.abstraction";
import { I18nService } from "../../abstractions/i18n.service";
import { StateService } from "../../abstractions/state.service";
import { Utils } from "../../misc/utils";
import { CipherData } from "../../models/data/cipherData";
import { FolderData } from "../../models/data/folderData";
import { Folder } from "../../models/domain/folder";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { FolderView } from "../../models/view/folderView";
export class FolderService implements InternalFolderServiceAbstraction {
private _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
private _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]);
folders$ = this._folders.asObservable();
folderViews$ = this._folderViews.asObservable();
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private cipherService: CipherService,
private stateService: StateService
) {
this.stateService.activeAccountUnlocked.subscribe(async (unlocked) => {
if ((Utils.global as any).bitwardenContainerService == null) {
return;
}
if (!unlocked) {
this._folders.next([]);
this._folderViews.next([]);
return;
}
const data = await this.stateService.getEncryptedFolders();
await this.updateObservables(data);
});
}
async clearCache(): Promise<void> {
this._folderViews.next([]);
}
// TODO: This should be moved to EncryptService or something
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
const folder = new Folder();
folder.id = model.id;
folder.name = await this.cryptoService.encrypt(model.name, key);
return folder;
}
async get(id: string): Promise<Folder> {
const folders = this._folders.getValue();
return folders.find((folder) => folder.id === id);
}
/**
* @deprecated Only use in CLI!
*/
async getAllDecryptedFromState(): Promise<FolderView[]> {
const data = await this.stateService.getEncryptedFolders();
const folders = Object.values(data || {}).map((f) => new Folder(f));
return this.decryptFolders(folders);
}
async upsert(folder: FolderData | FolderData[]): Promise<void> {
let folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
folders = {};
}
if (folder instanceof FolderData) {
const f = folder as FolderData;
folders[f.id] = f;
} else {
(folder as FolderData[]).forEach((f) => {
folders[f.id] = f;
});
}
await this.updateObservables(folders);
await this.stateService.setEncryptedFolders(folders);
}
async replace(folders: { [id: string]: FolderData }): Promise<void> {
await this.updateObservables(folders);
await this.stateService.setEncryptedFolders(folders);
}
async clear(userId?: string): Promise<any> {
if (userId == null || userId == (await this.stateService.getUserId())) {
this._folders.next([]);
this._folderViews.next([]);
}
await this.stateService.setEncryptedFolders(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
return;
}
if (typeof id === "string") {
if (folders[id] == null) {
return;
}
delete folders[id];
} else {
(id as string[]).forEach((i) => {
delete folders[i];
});
}
await this.updateObservables(folders);
await this.stateService.setEncryptedFolders(folders);
// Items in a deleted folder are re-assigned to "No Folder"
const ciphers = await this.stateService.getEncryptedCiphers();
if (ciphers != null) {
const updates: CipherData[] = [];
for (const cId in ciphers) {
if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null;
updates.push(ciphers[cId]);
}
}
if (updates.length > 0) {
this.cipherService.upsert(updates);
}
}
}
private async updateObservables(foldersMap: { [id: string]: FolderData }) {
const folders = Object.values(foldersMap || {}).map((f) => new Folder(f));
this._folders.next(folders);
if (await this.cryptoService.hasKey()) {
this._folderViews.next(await this.decryptFolders(folders));
}
}
private async decryptFolders(folders: Folder[]) {
const decryptFolderPromises = folders.map((f) => f.decrypt());
const decryptedFolders = await Promise.all(decryptFolderPromises);
decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
const noneFolder = new FolderView();
noneFolder.name = this.i18nService.t("noneFolder");
decryptedFolders.push(noneFolder);
return decryptedFolders;
}
}

View File

@@ -0,0 +1,31 @@
import { FormGroup, ValidationErrors } from "@angular/forms";
import {
FormGroupControls,
FormValidationErrorsService as FormValidationErrorsAbstraction,
AllValidationErrors,
} from "../abstractions/formValidationErrors.service";
export class FormValidationErrorsService implements FormValidationErrorsAbstraction {
getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[] {
let errors: AllValidationErrors[] = [];
Object.keys(controls).forEach((key) => {
const control = controls[key];
if (control instanceof FormGroup) {
errors = errors.concat(this.getFormValidationErrors(control.controls));
}
const controlErrors: ValidationErrors = controls[key].errors;
if (controlErrors !== null) {
Object.keys(controlErrors).forEach((keyError) => {
errors.push({
controlName: key,
errorName: keyError,
});
});
}
});
return errors;
}
}

View File

@@ -1,7 +1,10 @@
import { Observable, ReplaySubject } from "rxjs";
import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service";
export class I18nService implements I18nServiceAbstraction {
locale: string;
private _locale = new ReplaySubject<string>(1);
locale$: Observable<string> = this._locale.asObservable();
// First locale is the default (English)
supportedTranslationLocales: string[] = ["en"];
translationLocale: string;
@@ -85,10 +88,14 @@ export class I18nService implements I18nServiceAbstraction {
}
this.inited = true;
this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage;
this.translationLocale = locale != null ? locale : this.systemLanguage;
this._locale.next(this.translationLocale);
try {
this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: "base" });
this.collator = new Intl.Collator(this.translationLocale, {
numeric: true,
sensitivity: "base",
});
} catch {
this.collator = null;
}

View File

@@ -2,7 +2,7 @@ import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { FolderService } from "../abstractions/folder/folder.service.abstraction";
import { I18nService } from "../abstractions/i18n.service";
import { ImportService as ImportServiceAbstraction } from "../abstractions/import.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";

View File

@@ -14,16 +14,23 @@ export class SearchService implements SearchServiceAbstraction {
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
private searchableMinLength = 2;
private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"];
private readonly defaultSearchableMinLength: number = 2;
private searchableMinLength: number = this.defaultSearchableMinLength;
constructor(
private cipherService: CipherService,
private logService: LogService,
private i18nService: I18nService
) {
if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) {
this.searchableMinLength = 1;
}
this.i18nService.locale$.subscribe((locale) => {
if (this.immediateSearchLocales.indexOf(locale) !== -1) {
this.searchableMinLength = 1;
} else {
this.searchableMinLength = this.defaultSearchableMinLength;
}
});
//register lunr pipeline function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
}

View File

@@ -13,6 +13,7 @@ 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";
import { EventData } from "../models/data/eventData";
import { FolderData } from "../models/data/folderData";
import { OrganizationData } from "../models/data/organizationData";
@@ -31,7 +32,6 @@ import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { WindowState } from "../models/domain/windowState";
import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView";
import { FolderView } from "../models/view/folderView";
import { SendView } from "../models/view/sendView";
const keys = {
@@ -56,6 +56,7 @@ export class StateService<
{
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
activeAccount = new BehaviorSubject<string>(null);
activeAccountUnlocked = new BehaviorSubject<boolean>(false);
private hasBeenInited = false;
private isRecoveredSession = false;
@@ -70,7 +71,21 @@ export class StateService<
protected stateMigrationService: StateMigrationService,
protected stateFactory: StateFactory<TGlobalState, TAccount>,
protected useAccountCache: boolean = true
) {}
) {
// If the account gets changed, verify the new account is unlocked
this.activeAccount.subscribe(async (userId) => {
if (userId == null && this.activeAccountUnlocked.getValue() == false) {
return;
} else if (userId == null) {
this.activeAccountUnlocked.next(false);
}
// FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existance of the crypto key is a low level
// implementation detail.
this.activeAccountUnlocked.next((await this.getCryptoMasterKey()) != null);
});
}
async init(): Promise<void> {
if (this.hasBeenInited) {
@@ -499,6 +514,15 @@ export class StateService<
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
if (options.userId == this.activeAccount.getValue()) {
const nextValue = value != null;
// Avoid emitting if we are already unlocked
if (this.activeAccountUnlocked.getValue() != nextValue) {
this.activeAccountUnlocked.next(nextValue);
}
}
}
async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> {
@@ -658,24 +682,6 @@ export class StateService<
);
}
@withPrototypeForArrayMembers(FolderView)
async getDecryptedFolders(options?: StorageOptions): Promise<FolderView[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.data?.folders?.decrypted;
}
async setDecryptedFolders(value: FolderView[], options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.data.folders.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson)
async getDecryptedOrganizationKeys(
options?: StorageOptions
@@ -1363,14 +1369,16 @@ export class StateService<
);
}
async getEncryptedOrganizationKeys(options?: StorageOptions): Promise<any> {
async getEncryptedOrganizationKeys(
options?: StorageOptions
): Promise<{ [orgId: string]: EncryptedOrganizationKeyData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.keys?.organizationKeys.encrypted;
}
async setEncryptedOrganizationKeys(
value: Map<string, SymmetricCryptoKey>,
value: { [orgId: string]: EncryptedOrganizationKeyData },
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
@@ -1940,52 +1948,52 @@ export class StateService<
async getPasswordGenerationOptions(options?: StorageOptions): Promise<any> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.passwordGenerationOptions;
}
async setPasswordGenerationOptions(value: any, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
account.settings.passwordGenerationOptions = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
}
async getUsernameGenerationOptions(options?: StorageOptions): Promise<any> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.usernameGenerationOptions;
}
async setUsernameGenerationOptions(value: any, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
account.settings.usernameGenerationOptions = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
}
async getGeneratorOptions(options?: StorageOptions): Promise<any> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.generatorOptions;
}
async setGeneratorOptions(value: any, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
account.settings.generatorOptions = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
}

View File

@@ -155,6 +155,15 @@ export class StateMigrationService<
case StateVersion.Three:
await this.migrateStateFrom3To4();
break;
case StateVersion.Four: {
const authenticatedAccounts = await this.getAuthenticatedAccounts();
for (const account of authenticatedAccounts) {
const migratedAccount = await this.migrateAccountFrom4To5(account);
await this.set(account.profile.userId, migratedAccount);
}
await this.setCurrentStateVersion(StateVersion.Five);
break;
}
}
currentStateVersion += 1;
@@ -488,6 +497,20 @@ export class StateMigrationService<
await this.set(keys.global, globals);
}
protected async migrateAccountFrom4To5(account: TAccount): Promise<TAccount> {
const encryptedOrgKeys = account.keys?.organizationKeys?.encrypted;
if (encryptedOrgKeys != null) {
for (const [orgId, encKey] of Object.entries(encryptedOrgKeys)) {
encryptedOrgKeys[orgId] = {
type: "organization",
key: encKey as unknown as string, // Account v4 does not reflect the current account model so we have to cast
};
}
}
return account;
}
protected get options(): StorageOptions {
return { htmlStorageLocation: HtmlStorageLocation.Local };
}
@@ -510,4 +533,15 @@ export class StateMigrationService<
protected async getCurrentStateVersion(): Promise<StateVersion> {
return (await this.getGlobals())?.stateVersion ?? StateVersion.One;
}
protected async setCurrentStateVersion(newVersion: StateVersion): Promise<void> {
const globals = await this.getGlobals();
globals.stateVersion = newVersion;
await this.set(keys.global, globals);
}
protected async getAuthenticatedAccounts(): Promise<TAccount[]> {
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
return Promise.all(authenticatedUserIds.map((id) => this.get<TAccount>(id)));
}
}

View File

@@ -2,7 +2,8 @@ import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { FolderApiServiceAbstraction } from "../abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "../abstractions/folder/folder.service.abstraction";
import { KeyConnectorService } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { MessagingService } from "../abstractions/messaging.service";
@@ -40,7 +41,7 @@ export class SyncService implements SyncServiceAbstraction {
constructor(
private apiService: ApiService,
private settingsService: SettingsService,
private folderService: FolderService,
private folderService: InternalFolderService,
private cipherService: CipherService,
private cryptoService: CryptoService,
private collectionService: CollectionService,
@@ -52,6 +53,7 @@ export class SyncService implements SyncServiceAbstraction {
private stateService: StateService,
private organizationService: OrganizationService,
private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction,
private logoutCallback: (expired: boolean) => Promise<void>
) {}
@@ -127,7 +129,7 @@ export class SyncService implements SyncServiceAbstraction {
(!isEdit && localFolder == null) ||
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)
) {
const remoteFolder = await this.apiService.getFolder(notification.id);
const remoteFolder = await this.folderApiService.get(notification.id);
if (remoteFolder != null) {
await this.folderService.upsert(new FolderData(remoteFolder));
this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id });
@@ -196,7 +198,7 @@ export class SyncService implements SyncServiceAbstraction {
}
if (shouldUpdate) {
const remoteCipher = await this.apiService.getCipher(notification.id);
const remoteCipher = await this.apiService.getFullCipherDetails(notification.id);
if (remoteCipher != null) {
await this.cipherService.upsert(new CipherData(remoteCipher));
this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id });

View File

@@ -2,7 +2,7 @@ import { AuthService } from "../abstractions/auth.service";
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { FolderService } from "../abstractions/folder/folder.service.abstraction";
import { KeyConnectorService } from "../abstractions/keyConnector.service";
import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
@@ -80,6 +80,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
if (userId == null || userId === (await this.stateService.getUserId())) {
this.searchService.clearIndex();
await this.folderService.clearCache();
}
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
@@ -91,7 +92,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.cryptoService.clearKeyPair(true, userId);
await this.cryptoService.clearEncKey(true, userId);
await this.folderService.clearCache(userId);
await this.cipherService.clearCache(userId);
await this.collectionService.clearCache(userId);