1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-18243] Improve type safety in decryption (#12885)

* Improve decrypt failure logging

* Rename decryptcontext to decrypttrace

* Improve docs

* PM-16984: Improving type safety of decryption

* Improving type safety of decryption

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
Maciej Zieniuk
2025-03-11 14:06:44 +01:00
committed by GitHub
parent 7e6f2fa798
commit 5cd47ac907
18 changed files with 111 additions and 154 deletions

View File

@@ -39,11 +39,10 @@ export class Collection extends Domain {
}
decrypt(orgKey: OrgKey): Promise<CollectionView> {
return this.decryptObj(
return this.decryptObj<Collection, CollectionView>(
this,
new CollectionView(this),
{
name: null,
},
["name"],
this.organizationId,
orgKey,
);

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
@@ -15,6 +13,19 @@ export type DecryptedObject<
TDecryptedKeys extends EncStringKeys<TEncryptedObject>,
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
// extracts shared keys from the domain and view types
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
ConditionalKeys<D, EncString | null>) &
(keyof V & ConditionalKeys<V, string | null>);
type DomainEncryptableKeys<D extends Domain> = {
[key in ConditionalKeys<D, EncString | null>]: EncString | null;
};
type ViewEncryptableKeys<V extends View> = {
[key in ConditionalKeys<V, string | null>]: string | null;
};
// https://contributing.bitwarden.com/architecture/clients/data-model#domain
export default class Domain {
protected buildDomainModel<D extends Domain>(
@@ -37,6 +48,7 @@ export default class Domain {
}
}
}
protected buildDataModel<D extends Domain>(
domain: D,
dataObj: any,
@@ -58,31 +70,24 @@ export default class Domain {
}
}
protected async decryptObj<T extends View>(
viewModel: T,
map: any,
orgId: string,
key: SymmetricCryptoKey = null,
protected async decryptObj<D extends Domain, V extends View>(
domain: DomainEncryptableKeys<D>,
viewModel: ViewEncryptableKeys<V>,
props: EncryptableKeys<D, V>[],
orgId: string | null,
key: SymmetricCryptoKey | null = null,
objectContext: string = "No Domain Context",
): Promise<T> {
const self: any = this;
for (const prop in map) {
// eslint-disable-next-line
if (!map.hasOwnProperty(prop)) {
continue;
}
const mapProp = map[prop] || prop;
if (self[mapProp]) {
(viewModel as any)[prop] = await self[mapProp].decrypt(
): Promise<V> {
for (const prop of props) {
viewModel[prop] =
(await domain[prop]?.decrypt(
orgId,
key,
`Property: ${prop}; ObjectContext: ${objectContext}`,
);
}
`Property: ${prop as string}; ObjectContext: ${objectContext}`,
)) ?? null;
}
return viewModel;
return viewModel as V;
}
/**
@@ -111,7 +116,7 @@ export default class Domain {
const decryptedObjects = [];
for (const prop of encryptedProperties) {
const value = (this as any)[prop] as EncString;
const value = this[prop] as EncString;
const decrypted = await this.decryptProperty(
prop,
value,
@@ -138,11 +143,9 @@ export default class Domain {
encryptService: EncryptService,
decryptTrace: string,
) {
let decrypted: string = null;
let decrypted: string | null = null;
if (value) {
decrypted = await value.decryptWithKey(key, encryptService, decryptTrace);
} else {
decrypted = null;
}
return {
[propertyKey]: decrypted,

View File

@@ -160,7 +160,7 @@ export class EncString implements Encrypted {
async decrypt(
orgId: string | null,
key: SymmetricCryptoKey = null,
key: SymmetricCryptoKey | null = null,
context?: string,
): Promise<string> {
if (this.decryptedValue != null) {

View File

@@ -54,14 +54,7 @@ export class SendAccess extends Domain {
async decrypt(key: SymmetricCryptoKey): Promise<SendAccessView> {
const model = new SendAccessView(this);
await this.decryptObj(
model,
{
name: null,
},
null,
key,
);
await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], null, key);
switch (this.type) {
case SendType.File:

View File

@@ -34,15 +34,13 @@ export class SendFile extends Domain {
}
async decrypt(key: SymmetricCryptoKey): Promise<SendFileView> {
const view = await this.decryptObj(
return await this.decryptObj<SendFile, SendFileView>(
this,
new SendFileView(this),
{
fileName: null,
},
["fileName"],
null,
key,
);
return view;
}
static fromJSON(obj: Jsonify<SendFile>) {

View File

@@ -30,11 +30,10 @@ export class SendText extends Domain {
}
decrypt(key: SymmetricCryptoKey): Promise<SendTextView> {
return this.decryptObj(
return this.decryptObj<SendText, SendTextView>(
this,
new SendTextView(this),
{
text: null,
},
["text"],
null,
key,
);

View File

@@ -87,15 +87,7 @@ export class Send extends Domain {
// TODO: error?
}
await this.decryptObj(
model,
{
name: null,
notes: null,
},
null,
model.cryptoKey,
);
await this.decryptObj<Send, SendView>(this, model, ["name", "notes"], null, model.cryptoKey);
switch (this.type) {
case SendType.File:

View File

@@ -43,11 +43,10 @@ export class Attachment extends Domain {
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<AttachmentView> {
const view = await this.decryptObj(
const view = await this.decryptObj<Attachment, AttachmentView>(
this,
new AttachmentView(this),
{
fileName: null,
},
["fileName"],
orgId,
encKey,
"DomainType: Attachment; " + context,

View File

@@ -42,16 +42,10 @@ export class Card extends Domain {
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<CardView> {
return this.decryptObj(
return this.decryptObj<Card, CardView>(
this,
new CardView(),
{
cardholderName: null,
brand: null,
number: null,
expMonth: null,
expYear: null,
code: null,
},
["cardholderName", "brand", "number", "expMonth", "expYear", "code"],
orgId,
encKey,
"DomainType: Card; " + context,

View File

@@ -154,12 +154,10 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
bypassValidation = false;
}
await this.decryptObj(
await this.decryptObj<Cipher, CipherView>(
this,
model,
{
name: null,
notes: null,
},
["name", "notes"],
this.organizationId,
encKey,
);

View File

@@ -52,41 +52,38 @@ export class Fido2Credential extends Domain {
}
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<Fido2CredentialView> {
const view = await this.decryptObj(
const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>(
this,
new Fido2CredentialView(),
{
credentialId: null,
keyType: null,
keyAlgorithm: null,
keyCurve: null,
keyValue: null,
rpId: null,
userHandle: null,
userName: null,
rpName: null,
userDisplayName: null,
discoverable: null,
},
[
"credentialId",
"keyType",
"keyAlgorithm",
"keyCurve",
"keyValue",
"rpId",
"userHandle",
"userName",
"rpName",
"userDisplayName",
],
orgId,
encKey,
);
const { counter } = await this.decryptObj(
{ counter: "" },
const { counter } = await this.decryptObj<
Fido2Credential,
{
counter: null,
},
orgId,
encKey,
);
counter: string;
}
>(this, { counter: "" }, ["counter"], orgId, encKey);
// Counter will end up as NaN if this fails
view.counter = parseInt(counter);
const { discoverable } = await this.decryptObj(
const { discoverable } = await this.decryptObj<Fido2Credential, { discoverable: string }>(
this,
{ discoverable: "" },
{
discoverable: null,
},
["discoverable"],
orgId,
encKey,
);

View File

@@ -35,12 +35,10 @@ export class Field extends Domain {
}
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
return this.decryptObj(
return this.decryptObj<Field, FieldView>(
this,
new FieldView(this),
{
name: null,
value: null,
},
["name", "value"],
orgId,
encKey,
);

View File

@@ -40,13 +40,7 @@ export class Folder extends Domain {
}
decrypt(): Promise<FolderView> {
return this.decryptObj(
new FolderView(this),
{
name: null,
},
null,
);
return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], null);
}
async decryptWithKey(

View File

@@ -66,28 +66,29 @@ export class Identity extends Domain {
context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<IdentityView> {
return this.decryptObj(
return this.decryptObj<Identity, IdentityView>(
this,
new IdentityView(),
{
title: null,
firstName: null,
middleName: null,
lastName: null,
address1: null,
address2: null,
address3: null,
city: null,
state: null,
postalCode: null,
country: null,
company: null,
email: null,
phone: null,
ssn: null,
username: null,
passportNumber: null,
licenseNumber: null,
},
[
"title",
"firstName",
"middleName",
"lastName",
"address1",
"address2",
"address3",
"city",
"state",
"postalCode",
"country",
"company",
"email",
"phone",
"ssn",
"username",
"passportNumber",
"licenseNumber",
],
orgId,
encKey,
"DomainType: Identity; " + context,

View File

@@ -38,11 +38,10 @@ export class LoginUri extends Domain {
context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<LoginUriView> {
return this.decryptObj(
return this.decryptObj<LoginUri, LoginUriView>(
this,
new LoginUriView(this),
{
uri: null,
},
["uri"],
orgId,
encKey,
context,

View File

@@ -58,13 +58,10 @@ export class Login extends Domain {
context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<LoginView> {
const view = await this.decryptObj(
const view = await this.decryptObj<Login, LoginView>(
this,
new LoginView(this),
{
username: null,
password: null,
totp: null,
},
["username", "password", "totp"],
orgId,
encKey,
`DomainType: Login; ${context}`,

View File

@@ -25,11 +25,10 @@ export class Password extends Domain {
}
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<PasswordHistoryView> {
return this.decryptObj(
return this.decryptObj<Password, PasswordHistoryView>(
this,
new PasswordHistoryView(this),
{
password: null,
},
["password"],
orgId,
encKey,
"DomainType: PasswordHistory",

View File

@@ -36,13 +36,10 @@ export class SshKey extends Domain {
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<SshKeyView> {
return this.decryptObj(
return this.decryptObj<SshKey, SshKeyView>(
this,
new SshKeyView(),
{
privateKey: null,
publicKey: null,
keyFingerprint: null,
},
["privateKey", "publicKey", "keyFingerprint"],
orgId,
encKey,
"DomainType: SshKey; " + context,