1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

Assign ownership to many libs files (#6928)

Assign ownership to many of the remaining libs/common files.

Criteria for ownership:
* Files used by a single team, is now owned by that team.
* Files related to a domain owned by a team is now owned by that team.
* Where ownership is unclear the "lowest level" service takes ownership.
This commit is contained in:
Oscar Hinton
2023-11-27 21:59:44 +01:00
committed by GitHub
parent 31ca3ea7b0
commit a5e3432f85
336 changed files with 446 additions and 473 deletions

View File

@@ -1,5 +1,5 @@
import { UriMatchType } from "../../enums";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher";

View File

@@ -1,6 +1,6 @@
import { TreeNode } from "../../models/domain/tree-node";
import { CollectionData } from "../models/data/collection.data";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/tree-node";
import { CollectionView } from "../models/view/collection.view";
export abstract class CollectionService {

View File

@@ -0,0 +1,4 @@
export abstract class TotpService {
getCode: (key: string) => Promise<string>;
getTimeInterval: (key: string) => number;
}

View File

@@ -0,0 +1,6 @@
export enum FieldType {
Text = 0,
Hidden = 1,
Boolean = 2,
Linked = 3,
}

View File

@@ -0,0 +1,6 @@
export * from "./cipher-reprompt-type";
export * from "./cipher-type";
export * from "./field-type.enum";
export * from "./linked-id-type.enum";
export * from "./secure-note-type.enum";
export * from "./uri-match-type.enum";

View File

@@ -0,0 +1,40 @@
export type LinkedIdType = LoginLinkedId | CardLinkedId | IdentityLinkedId;
// LoginView
export enum LoginLinkedId {
Username = 100,
Password = 101,
}
// CardView
export enum CardLinkedId {
CardholderName = 300,
ExpMonth = 301,
ExpYear = 302,
Code = 303,
Brand = 304,
Number = 305,
}
// IdentityView
export enum IdentityLinkedId {
Title = 400,
MiddleName = 401,
Address1 = 402,
Address2 = 403,
Address3 = 404,
City = 405,
State = 406,
PostalCode = 407,
Country = 408,
Company = 409,
Email = 410,
Phone = 411,
Ssn = 412,
Username = 413,
PassportNumber = 414,
LicenseNumber = 415,
FirstName = 416,
LastName = 417,
FullName = 418,
}

View File

@@ -0,0 +1,3 @@
export enum SecureNoteType {
Generic = 0,
}

View File

@@ -0,0 +1,8 @@
export enum UriMatchType {
Domain = 0,
Host = 1,
StartsWith = 2,
Exact = 3,
RegularExpression = 4,
Never = 5,
}

View File

@@ -0,0 +1,27 @@
import { LinkedIdType } from "./enums";
import { ItemView } from "./models/view/item.view";
export class LinkedMetadata {
constructor(readonly propertyKey: string, private readonly _i18nKey?: string) {}
get i18nKey() {
return this._i18nKey ?? this.propertyKey;
}
}
/**
* A decorator used to set metadata used by Linked custom fields. Apply it to a class property or getter to make it
* available as a Linked custom field option.
* @param id - A unique value that is saved in the Field model. It is used to look up the decorated class property.
* @param i18nKey - The i18n key used to describe the decorated class property in the UI. If it is null, then the name
* of the class property will be used as the i18n key.
*/
export function linkedFieldOption(id: LinkedIdType, i18nKey?: string) {
return (prototype: ItemView, propertyKey: string) => {
if (prototype.linkedFieldOptions == null) {
prototype.linkedFieldOptions = new Map<LinkedIdType, LinkedMetadata>();
}
prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, i18nKey));
};
}

View File

@@ -0,0 +1,23 @@
import { BaseResponse } from "../../../models/response/base.response";
export class CardApi extends BaseResponse {
cardholderName: string;
brand: string;
number: string;
expMonth: string;
expYear: string;
code: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.cardholderName = this.getResponseProperty("CardholderName");
this.brand = this.getResponseProperty("Brand");
this.number = this.getResponseProperty("Number");
this.expMonth = this.getResponseProperty("ExpMonth");
this.expYear = this.getResponseProperty("ExpYear");
this.code = this.getResponseProperty("Code");
}
}

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "../../models/response/base.response";
import { BaseResponse } from "../../../models/response/base.response";
export class Fido2CredentialApi extends BaseResponse {
credentialId: string;

View File

@@ -0,0 +1,20 @@
import { BaseResponse } from "../../../models/response/base.response";
import { FieldType, LinkedIdType } from "../../enums";
export class FieldApi extends BaseResponse {
name: string;
value: string;
type: FieldType;
linkedId: LinkedIdType;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.type = this.getResponseProperty("Type");
this.name = this.getResponseProperty("Name");
this.value = this.getResponseProperty("Value");
this.linkedId = this.getResponseProperty("linkedId");
}
}

View File

@@ -0,0 +1,47 @@
import { BaseResponse } from "../../../models/response/base.response";
export class IdentityApi extends BaseResponse {
title: string;
firstName: string;
middleName: string;
lastName: string;
address1: string;
address2: string;
address3: string;
city: string;
state: string;
postalCode: string;
country: string;
company: string;
email: string;
phone: string;
ssn: string;
username: string;
passportNumber: string;
licenseNumber: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.title = this.getResponseProperty("Title");
this.firstName = this.getResponseProperty("FirstName");
this.middleName = this.getResponseProperty("MiddleName");
this.lastName = this.getResponseProperty("LastName");
this.address1 = this.getResponseProperty("Address1");
this.address2 = this.getResponseProperty("Address2");
this.address3 = this.getResponseProperty("Address3");
this.city = this.getResponseProperty("City");
this.state = this.getResponseProperty("State");
this.postalCode = this.getResponseProperty("PostalCode");
this.country = this.getResponseProperty("Country");
this.company = this.getResponseProperty("Company");
this.email = this.getResponseProperty("Email");
this.phone = this.getResponseProperty("Phone");
this.ssn = this.getResponseProperty("SSN");
this.username = this.getResponseProperty("Username");
this.passportNumber = this.getResponseProperty("PassportNumber");
this.licenseNumber = this.getResponseProperty("LicenseNumber");
}
}

View File

@@ -0,0 +1,17 @@
import { BaseResponse } from "../../../models/response/base.response";
import { UriMatchType } from "../../enums";
export class LoginUriApi extends BaseResponse {
uri: string;
match: UriMatchType = null;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.uri = this.getResponseProperty("Uri");
const match = this.getResponseProperty("Match");
this.match = match != null ? match : null;
}
}

View File

@@ -0,0 +1,40 @@
import { JsonObject } from "type-fest";
import { BaseResponse } from "../../../models/response/base.response";
import { Fido2CredentialApi } from "./fido2-credential.api";
import { LoginUriApi } from "./login-uri.api";
export class LoginApi extends BaseResponse {
uris: LoginUriApi[];
username: string;
password: string;
passwordRevisionDate: string;
totp: string;
autofillOnPageLoad: boolean;
fido2Credentials?: Fido2CredentialApi[];
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.username = this.getResponseProperty("Username");
this.password = this.getResponseProperty("Password");
this.passwordRevisionDate = this.getResponseProperty("PasswordRevisionDate");
this.totp = this.getResponseProperty("Totp");
this.autofillOnPageLoad = this.getResponseProperty("AutofillOnPageLoad");
const uris = this.getResponseProperty("Uris");
if (uris != null) {
this.uris = uris.map((u: any) => new LoginUriApi(u));
}
const fido2Credentials = this.getResponseProperty("Fido2Credentials");
if (fido2Credentials != null) {
this.fido2Credentials = fido2Credentials.map(
(key: JsonObject) => new Fido2CredentialApi(key)
);
}
}
}

View File

@@ -0,0 +1,14 @@
import { BaseResponse } from "../../../models/response/base.response";
import { SecureNoteType } from "../../enums";
export class SecureNoteApi extends BaseResponse {
type: SecureNoteType;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.type = this.getResponseProperty("Type");
}
}

View File

@@ -1,4 +1,4 @@
import { CardApi } from "../../../models/api/card.api";
import { CardApi } from "../api/card.api";
export class CardData {
cardholderName: string;

View File

@@ -1,4 +1,4 @@
import { Fido2CredentialApi } from "../../api/fido2-credential.api";
import { Fido2CredentialApi } from "../api/fido2-credential.api";
export class Fido2CredentialData {
credentialId: string;

View File

@@ -1,5 +1,5 @@
import { FieldType, LinkedIdType } from "../../../enums";
import { FieldApi } from "../../../models/api/field.api";
import { FieldType, LinkedIdType } from "../../enums";
import { FieldApi } from "../api/field.api";
export class FieldData {
type: FieldType;

View File

@@ -1,4 +1,4 @@
import { IdentityApi } from "../../../models/api/identity.api";
import { IdentityApi } from "../api/identity.api";
export class IdentityData {
title: string;

View File

@@ -1,5 +1,5 @@
import { UriMatchType } from "../../../enums";
import { LoginUriApi } from "../../../models/api/login-uri.api";
import { UriMatchType } from "../../enums";
import { LoginUriApi } from "../api/login-uri.api";
export class LoginUriData {
uri: string;

View File

@@ -1,4 +1,4 @@
import { LoginApi } from "../../../models/api/login.api";
import { LoginApi } from "../api/login.api";
import { Fido2CredentialData } from "./fido2-credential.data";
import { LoginUriData } from "./login-uri.data";

View File

@@ -1,5 +1,5 @@
import { SecureNoteType } from "../../../enums";
import { SecureNoteApi } from "../../../models/api/secure-note.api";
import { SecureNoteType } from "../../enums";
import { SecureNoteApi } from "../api/secure-note.api";
export class SecureNoteData {
type: SecureNoteType;

View File

@@ -2,13 +2,13 @@ import { mock } from "jest-mock-extended";
import { Jsonify } from "type-fest";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { FieldType, SecureNoteType, UriMatchType } from "../../../enums";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherService } from "../../abstractions/cipher.service";
import { FieldType, SecureNoteType, UriMatchType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../../models/data/cipher.data";

View File

@@ -1,5 +1,5 @@
import { mockEnc } from "../../../../spec";
import { EncryptionType } from "../../../enums";
import { EncryptionType } from "../../../platform/enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { Fido2CredentialData } from "../data/fido2-credential.data";

View File

@@ -1,6 +1,6 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { FieldType } from "../../../enums";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FieldType } from "../../enums";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";

View File

@@ -1,9 +1,9 @@
import { Jsonify } from "type-fest";
import { FieldType, LinkedIdType } from "../../../enums";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { FieldType, LinkedIdType } from "../../enums";
import { FieldData } from "../data/field.data";
import { FieldView } from "../view/field.view";

View File

@@ -1,8 +1,8 @@
import { Jsonify } from "type-fest";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchType } from "../../../enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUri } from "./login-uri";

View File

@@ -1,9 +1,9 @@
import { Jsonify } from "type-fest";
import { UriMatchType } from "../../../enums";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUriView } from "../view/login-uri.view";

View File

@@ -1,13 +1,13 @@
import { mock } from "jest-mock-extended";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchType } from "../../../enums";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { Fido2CredentialApi } from "../../api/fido2-credential.api";
import { UriMatchType } from "../../enums";
import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri";
import { LoginUriView } from "../../models/view/login-uri.view";
import { Fido2CredentialApi } from "../api/fido2-credential.api";
import { Fido2CredentialData } from "../data/fido2-credential.data";
import { Fido2CredentialView } from "../view/fido2-credential.view";

View File

@@ -1,4 +1,4 @@
import { SecureNoteType } from "../../../enums";
import { SecureNoteType } from "../../enums";
import { SecureNoteData } from "../data/secure-note.data";
import { SecureNote } from "./secure-note";

View File

@@ -1,8 +1,8 @@
import { Jsonify } from "type-fest";
import { SecureNoteType } from "../../../enums";
import Domain from "../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SecureNoteType } from "../../enums";
import { SecureNoteData } from "../data/secure-note.data";
import { SecureNoteView } from "../view/secure-note.view";

View File

@@ -0,0 +1,21 @@
export class TreeNode<T extends ITreeNodeObject> {
node: T;
parent: TreeNode<T>;
children: TreeNode<T>[] = [];
constructor(node: T, parent: TreeNode<T>, name?: string, id?: string) {
this.parent = parent;
this.node = node;
if (name) {
this.node.name = name;
}
if (id) {
this.node.id = id;
}
}
}
export interface ITreeNodeObject {
id: string;
name: string;
}

View File

@@ -1,12 +1,12 @@
import { CardApi } from "../../../models/api/card.api";
import { FieldApi } from "../../../models/api/field.api";
import { IdentityApi } from "../../../models/api/identity.api";
import { LoginUriApi } from "../../../models/api/login-uri.api";
import { LoginApi } from "../../../models/api/login.api";
import { SecureNoteApi } from "../../../models/api/secure-note.api";
import { Fido2CredentialApi } from "../../api/fido2-credential.api";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CardApi } from "../api/card.api";
import { Fido2CredentialApi } from "../api/fido2-credential.api";
import { FieldApi } from "../api/field.api";
import { IdentityApi } from "../api/identity.api";
import { LoginUriApi } from "../api/login-uri.api";
import { LoginApi } from "../api/login.api";
import { SecureNoteApi } from "../api/secure-note.api";
import { Cipher } from "../domain/cipher";
import { AttachmentRequest } from "./attachment.request";

View File

@@ -1,5 +1,5 @@
import { FileUploadType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { FileUploadType } from "../../../platform/enums";
import { CipherResponse } from "./cipher.response";

View File

@@ -1,10 +1,10 @@
import { CardApi } from "../../../models/api/card.api";
import { FieldApi } from "../../../models/api/field.api";
import { IdentityApi } from "../../../models/api/identity.api";
import { LoginApi } from "../../../models/api/login.api";
import { SecureNoteApi } from "../../../models/api/secure-note.api";
import { BaseResponse } from "../../../models/response/base.response";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CardApi } from "../api/card.api";
import { FieldApi } from "../api/field.api";
import { IdentityApi } from "../api/identity.api";
import { LoginApi } from "../api/login.api";
import { SecureNoteApi } from "../api/secure-note.api";
import { AttachmentResponse } from "./attachment.response";
import { PasswordHistoryResponse } from "./password-history.response";

View File

@@ -1,7 +1,7 @@
import { Jsonify } from "type-fest";
import { CardLinkedId as LinkedId } from "../../../enums";
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view";

View File

@@ -1,9 +1,9 @@
import { Jsonify } from "type-fest";
import { LinkedIdType } from "../../../enums";
import { View } from "../../../models/view/view";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { LinkedIdType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { LocalData } from "../data/local.data";

View File

@@ -1,7 +1,7 @@
import { Organization } from "../../../admin-console/models/domain/organization";
import { ITreeNodeObject } from "../../../models/domain/tree-node";
import { View } from "../../../models/view/view";
import { Collection } from "../domain/collection";
import { ITreeNodeObject } from "../domain/tree-node";
import { CollectionAccessDetailsResponse } from "../response/collection.response";
export const NestingDelimiter = "/";

View File

@@ -1,7 +1,7 @@
import { Jsonify } from "type-fest";
import { FieldType, LinkedIdType } from "../../../enums";
import { View } from "../../../models/view/view";
import { FieldType, LinkedIdType } from "../../enums";
import { Field } from "../domain/field";
export class FieldView implements View {

View File

@@ -1,8 +1,8 @@
import { Jsonify } from "type-fest";
import { ITreeNodeObject } from "../../../models/domain/tree-node";
import { View } from "../../../models/view/view";
import { Folder } from "../domain/folder";
import { ITreeNodeObject } from "../domain/tree-node";
export class FolderView implements View, ITreeNodeObject {
id: string = null;

View File

@@ -1,8 +1,8 @@
import { Jsonify } from "type-fest";
import { IdentityLinkedId as LinkedId } from "../../../enums";
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
import { Utils } from "../../../platform/misc/utils";
import { IdentityLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view";

View File

@@ -1,5 +1,5 @@
import { LinkedMetadata } from "../../../misc/linkedFieldOption.decorator";
import { View } from "../../../models/view/view";
import { LinkedMetadata } from "../../linked-field-option.decorator";
export abstract class ItemView implements View {
linkedFieldOptions: Map<number, LinkedMetadata>;

View File

@@ -1,5 +1,5 @@
import { UriMatchType } from "../../../enums";
import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUriView } from "./login-uri.view";

View File

@@ -1,9 +1,9 @@
import { Jsonify } from "type-fest";
import { UriMatchType } from "../../../enums";
import { View } from "../../../models/view/view";
import { SafeUrls } from "../../../platform/misc/safe-urls";
import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUri } from "../domain/login-uri";
export class LoginUriView implements View {

View File

@@ -1,8 +1,8 @@
import { Jsonify } from "type-fest";
import { LoginLinkedId as LinkedId, UriMatchType } from "../../../enums";
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
import { Utils } from "../../../platform/misc/utils";
import { LoginLinkedId as LinkedId, UriMatchType } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { Login } from "../domain/login";
import { Fido2CredentialView } from "./fido2-credential.view";

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { SecureNoteType } from "../../../enums";
import { SecureNoteType } from "../../enums";
import { SecureNote } from "../domain/secure-note";
import { ItemView } from "./item.view";

View File

@@ -0,0 +1,71 @@
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
import { ServiceUtils } from "./service-utils";
type FakeObject = { id: string; name: string };
describe("serviceUtils", () => {
let nodeTree: TreeNode<FakeObject>[];
beforeEach(() => {
nodeTree = [
createTreeNode({ id: "1", name: "1" }, [
createTreeNode({ id: "1.1", name: "1.1" }, [
createTreeNode({ id: "1.1.1", name: "1.1.1" }),
]),
createTreeNode({ id: "1.2", name: "1.2" }),
])(null),
createTreeNode({ id: "2", name: "2" }, [createTreeNode({ id: "2.1", name: "2.1" })])(null),
createTreeNode({ id: "3", name: "3" }, [])(null),
];
});
describe("nestedTraverse", () => {
it("should traverse a tree and add a node at the correct position given a valid path", () => {
const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" };
const path = ["1", "1.2", "1.2.1"];
ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/");
expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded);
});
it("should combine the path for missing nodes and use as the added node name given an invalid path", () => {
const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" };
const path = ["3", "3.1", "3.1.1"];
ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/");
expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1");
});
});
describe("getTreeNodeObject", () => {
it("should return a matching node given a single tree branch and a valid id", () => {
const id = "1.1.1";
const given = ServiceUtils.getTreeNodeObject(nodeTree[0], id);
expect(given.node.id).toEqual(id);
});
});
describe("getTreeNodeObjectFromList", () => {
it("should return a matching node given a list of branches and a valid id", () => {
const id = "1.1.1";
const given = ServiceUtils.getTreeNodeObjectFromList(nodeTree, id);
expect(given.node.id).toEqual(id);
});
});
});
type TreeNodeFactory<T extends ITreeNodeObject> = (
obj: T,
children?: TreeNodeFactoryWithoutParent<T>[]
) => TreeNodeFactoryWithoutParent<T>;
type TreeNodeFactoryWithoutParent<T extends ITreeNodeObject> = (
parent?: TreeNode<T>
) => TreeNode<T>;
const createTreeNode: TreeNodeFactory<FakeObject> =
(obj, children = []) =>
(parent) => {
const node = new TreeNode<FakeObject>(obj, parent, obj.name, obj.id);
node.children = children.map((childFunc) => childFunc(node));
return node;
};

View File

@@ -0,0 +1,117 @@
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
export class ServiceUtils {
/**
* Recursively adds a node to nodeTree
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
* @param {number} partIndex - Index of the `parts` array that is being processed
* @param {string[]} parts - Array of strings that represent the path to the `obj` node
* @param {ITreeNodeObject} obj - The node to be added to the tree
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
*/
static nestedTraverse(
nodeTree: TreeNode<ITreeNodeObject>[],
partIndex: number,
parts: string[],
obj: ITreeNodeObject,
parent: TreeNode<ITreeNodeObject> | undefined,
delimiter: string
) {
if (parts.length <= partIndex) {
return;
}
const end: boolean = partIndex === parts.length - 1;
const partName: string = parts[partIndex];
for (let i = 0; i < nodeTree.length; i++) {
if (nodeTree[i].node.name !== partName) {
continue;
}
if (end && nodeTree[i].node.id !== obj.id) {
// Another node exists with the same name as the node being added
nodeTree.push(new TreeNode(obj, parent, partName));
return;
}
// Move down the tree to the next level
ServiceUtils.nestedTraverse(
nodeTree[i].children,
partIndex + 1,
parts,
obj,
nodeTree[i],
delimiter
);
return;
}
// If there's no node here with the same name...
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
// And we're at the end of the path given, add the node
if (end) {
nodeTree.push(new TreeNode(obj, parent, partName));
return;
}
// And we're not at the end of the path, combine the current name with the next name
// 1, *1.2, 1.2.1 becomes
// 1, *1.2/1.2.1
const newPartName = partName + delimiter + parts[partIndex + 1];
ServiceUtils.nestedTraverse(
nodeTree,
0,
[newPartName, ...parts.slice(partIndex + 2)],
obj,
parent,
delimiter
);
}
}
/**
* Searches a tree for a node with a matching `id`
* @param {TreeNode<T>} nodeTree - A single TreeNode branch that will be searched
* @param {string} id - The id of the node to be found
* @returns {TreeNode<T>} The node with a matching `id`
*/
static getTreeNodeObject<T extends ITreeNodeObject>(
nodeTree: TreeNode<T>,
id: string
): TreeNode<T> {
if (nodeTree.node.id === id) {
return nodeTree;
}
for (let i = 0; i < nodeTree.children.length; i++) {
if (nodeTree.children[i].children != null) {
const node = ServiceUtils.getTreeNodeObject(nodeTree.children[i], id);
if (node !== null) {
return node;
}
}
}
return null;
}
/**
* Searches an array of tree nodes for a node with a matching `id`
* @param {TreeNode<T>} nodeTree - An array of TreeNode branches that will be searched
* @param {string} id - The id of the node to be found
* @returns {TreeNode<T>} The node with a matching `id`
*/
static getTreeNodeObjectFromList<T extends ITreeNodeObject>(
nodeTree: TreeNode<T>[],
id: string
): TreeNode<T> {
for (let i = 0; i < nodeTree.length; i++) {
if (nodeTree[i].node.id === id) {
return nodeTree[i];
} else if (nodeTree[i].children != null) {
const node = ServiceUtils.getTreeNodeObjectFromList(nodeTree[i].children, id);
if (node !== null) {
return node;
}
}
}
return null;
}
}

View File

@@ -5,7 +5,6 @@ import { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { UriMatchType, FieldType } from "../../enums";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
@@ -20,6 +19,7 @@ import {
} from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { UriMatchType, FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";

View File

@@ -4,7 +4,6 @@ import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { FieldType, UriMatchType } from "../../enums";
import { ErrorResponse } from "../../models/response/error.response";
import { View } from "../../models/view/view";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
@@ -25,6 +24,7 @@ import {
} from "../../platform/models/domain/symmetric-crypto-key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType, UriMatchType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { Attachment } from "../models/domain/attachment";

View File

@@ -1,5 +1,3 @@
import { ServiceUtils } from "../../misc/serviceUtils";
import { TreeNode } from "../../models/domain/tree-node";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
@@ -7,7 +5,9 @@ import { Utils } from "../../platform/misc/utils";
import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service";
import { CollectionData } from "../models/data/collection.data";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/tree-node";
import { CollectionView } from "../models/view/collection.view";
import { ServiceUtils } from "../service-utils";
const NestingDelimiter = "/";

View File

@@ -0,0 +1,168 @@
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import { TotpService as TotpServiceAbstraction } from "../abstractions/totp.service";
const B32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
export class TotpService implements TotpServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private logService: LogService
) {}
async getCode(key: string): Promise<string> {
if (key == null) {
return null;
}
let period = 30;
let alg: "sha1" | "sha256" | "sha512" = "sha1";
let digits = 6;
let keyB32 = key;
const isOtpAuth = key.toLowerCase().indexOf("otpauth://") === 0;
const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf("steam://") === 0;
if (isOtpAuth) {
const params = Utils.getQueryParams(key);
if (params.has("digits") && params.get("digits") != null) {
try {
const digitParams = parseInt(params.get("digits").trim(), null);
if (digitParams > 10) {
digits = 10;
} else if (digitParams > 0) {
digits = digitParams;
}
} catch {
this.logService.error("Invalid digits param.");
}
}
if (params.has("period") && params.get("period") != null) {
try {
const periodParam = parseInt(params.get("period").trim(), null);
if (periodParam > 0) {
period = periodParam;
}
} catch {
this.logService.error("Invalid period param.");
}
}
if (params.has("secret") && params.get("secret") != null) {
keyB32 = params.get("secret");
}
if (params.has("algorithm") && params.get("algorithm") != null) {
const algParam = params.get("algorithm").toLowerCase();
if (algParam === "sha1" || algParam === "sha256" || algParam === "sha512") {
alg = algParam;
}
}
} else if (isSteamAuth) {
keyB32 = key.substr("steam://".length);
digits = 5;
}
const epoch = Math.round(new Date().getTime() / 1000.0);
const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, "0");
const timeBytes = Utils.fromHexToArray(timeHex);
const keyBytes = this.b32ToBytes(keyB32);
if (!keyBytes.length || !timeBytes.length) {
return null;
}
const hash = await this.sign(keyBytes, timeBytes, alg);
if (hash.length === 0) {
return null;
}
const offset = hash[hash.length - 1] & 0xf;
const binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
let otp = "";
if (isSteamAuth) {
let fullCode = binary & 0x7fffffff;
for (let i = 0; i < digits; i++) {
otp += SteamChars[fullCode % SteamChars.length];
fullCode = Math.trunc(fullCode / SteamChars.length);
}
} else {
otp = (binary % Math.pow(10, digits)).toString();
otp = this.leftPad(otp, digits, "0");
}
return otp;
}
getTimeInterval(key: string): number {
let period = 30;
if (key != null && key.toLowerCase().indexOf("otpauth://") === 0) {
const params = Utils.getQueryParams(key);
if (params.has("period") && params.get("period") != null) {
try {
period = parseInt(params.get("period").trim(), null);
} catch {
this.logService.error("Invalid period param.");
}
}
}
return period;
}
// Helpers
private leftPad(s: string, l: number, p: string): string {
if (l + 1 >= s.length) {
s = Array(l + 1 - s.length).join(p) + s;
}
return s;
}
private decToHex(d: number): string {
return (d < 15.5 ? "0" : "") + Math.round(d).toString(16);
}
private b32ToHex(s: string): string {
s = s.toUpperCase();
let cleanedInput = "";
for (let i = 0; i < s.length; i++) {
if (B32Chars.indexOf(s[i]) < 0) {
continue;
}
cleanedInput += s[i];
}
s = cleanedInput;
let bits = "";
let hex = "";
for (let i = 0; i < s.length; i++) {
const byteIndex = B32Chars.indexOf(s.charAt(i));
if (byteIndex < 0) {
continue;
}
bits += this.leftPad(byteIndex.toString(2), 5, "0");
}
for (let i = 0; i + 4 <= bits.length; i += 4) {
const chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16);
}
return hex;
}
private b32ToBytes(s: string): Uint8Array {
return Utils.fromHexToArray(this.b32ToHex(s));
}
private async sign(
keyBytes: Uint8Array,
timeBytes: Uint8Array,
alg: "sha1" | "sha256" | "sha512"
) {
const signature = await this.cryptoFunctionService.hmac(timeBytes, keyBytes, alg);
return new Uint8Array(signature);
}
}