Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16699] Improve decrypt failure logging #12681

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,18 @@
let decUserKey: Uint8Array;

if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
decUserKey = await this.encryptService.decryptToBytes(

Check warning on line 183 in libs/common/src/auth/services/master-password/master-password.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/auth/services/master-password/master-password.service.ts#L183

Added line #L183 was not covered by tests
userKey,
masterKey,
"Content: User Key; Encrypting Key: Master Key",
);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.keyGenerationService.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
decUserKey = await this.encryptService.decryptToBytes(

Check warning on line 190 in libs/common/src/auth/services/master-password/master-password.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/auth/services/master-password/master-password.service.ts#L190

Added line #L190 was not covered by tests
userKey,
newKey,
"Content: User Key; Encrypting Key: Stretched Master Key",
);
} else {
throw new Error("Unsupported encryption type.");
}
Expand Down
24 changes: 22 additions & 2 deletions libs/common/src/platform/abstractions/encrypt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class EncryptService {
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptContext?: string,
decryptTrace?: string,
): Promise<string>;
abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array>;
/**
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
* @param key - The key to decrypt the Encrypted object with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted Uint8Array
*/
abstract decryptToBytes(
audreyality marked this conversation as resolved.
Show resolved Hide resolved
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
Expand Down
42 changes: 21 additions & 21 deletions libs/common/src/platform/models/domain/domain-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export default class Domain {
map: any,
orgId: string,
key: SymmetricCryptoKey = null,
objectContext: string = "No Domain Context",
): Promise<T> {
const promises = [];
const self: any = this;

for (const prop in map) {
Expand All @@ -73,23 +73,15 @@ export default class Domain {
continue;
}

(function (theProp) {
const p = Promise.resolve()
.then(() => {
const mapProp = map[theProp] || theProp;
if (self[mapProp]) {
return self[mapProp].decrypt(orgId, key);
}
return null;
})
.then((val: any) => {
(viewModel as any)[theProp] = val;
});
promises.push(p);
})(prop);
const mapProp = map[prop] || prop;
if (self[mapProp]) {
(viewModel as any)[prop] = await self[mapProp].decrypt(
orgId,
key,
`Property: ${prop}; ObjectContext: ${objectContext}`,
);
}
}

await Promise.all(promises);
return viewModel;
}

Expand All @@ -114,15 +106,22 @@ export default class Domain {
key: SymmetricCryptoKey,
encryptService: EncryptService,
_: Constructor<TThis> = this.constructor as Constructor<TThis>,
objectContext: string = "No Domain Context",
): Promise<DecryptedObject<TThis, TEncryptedKeys>> {
const promises = [];
const decryptedObjects = [];

for (const prop of encryptedProperties) {
const value = (this as any)[prop] as EncString;
promises.push(this.decryptProperty(prop, value, key, encryptService));
const decrypted = await this.decryptProperty(
prop,
value,
key,
encryptService,
`Property: ${prop.toString()}; ObjectContext: ${objectContext}`,
);
decryptedObjects.push(decrypted);
}

const decryptedObjects = await Promise.all(promises);
const decryptedObject = decryptedObjects.reduce(
(acc, obj) => {
return { ...acc, ...obj };
Expand All @@ -137,10 +136,11 @@ export default class Domain {
value: EncString,
key: SymmetricCryptoKey,
encryptService: EncryptService,
decryptTrace: string,
) {
let decrypted: string = null;
if (value) {
decrypted = await value.decryptWithKey(key, encryptService);
decrypted = await value.decryptWithKey(key, encryptService, decryptTrace);
} else {
decrypted = null;
}
Expand Down
24 changes: 16 additions & 8 deletions libs/common/src/platform/models/domain/enc-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,21 +156,21 @@ export class EncString implements Encrypted {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
}

async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise<string> {
if (this.decryptedValue != null) {
return this.decryptedValue;
}

let keyContext = "provided-key";
let decryptTrace = "provided-key";
try {
if (key == null) {
key = await this.getKeyForDecryption(orgId);
keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
if (orgId != null) {
keyContext = `domain-orgkey-${orgId}`;
decryptTrace = `domain-orgkey-${orgId}`;
} else {
const cryptoService = Utils.getContainerService().getKeyService();
keyContext =
decryptTrace =
(await cryptoService.getUserKey()) == null
? "domain-withlegacysupport-masterkey"
: "domain-withlegacysupport-userkey";
Expand All @@ -181,20 +181,28 @@ export class EncString implements Encrypted {
}

const encryptService = Utils.getContainerService().getEncryptService();
this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext);
this.decryptedValue = await encryptService.decryptToUtf8(
this,
key,
decryptTrace == null ? context : `${decryptTrace}${context || ""}`,
);
} catch (e) {
this.decryptedValue = DECRYPT_ERROR;
}
return this.decryptedValue;
}

async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) {
async decryptWithKey(
key: SymmetricCryptoKey,
encryptService: EncryptService,
decryptTrace: string = "domain-withkey",
): Promise<string> {
try {
if (key == null) {
throw new Error("No key to decrypt EncString");
}

this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey");
this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace);
} catch (e) {
this.decryptedValue = DECRYPT_ERROR;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService {
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType) +
Expand All @@ -128,7 +128,11 @@ export class EncryptServiceImplementation implements EncryptService {
return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc");
}

async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
async decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<Uint8Array> {
if (key == null) {
throw new Error("No encryption key provided.");
}
Expand All @@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService {
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
Expand All @@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService {
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
Expand All @@ -167,23 +175,27 @@ export class EncryptServiceImplementation implements EncryptService {
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
if (computedMac === null) {
this.logMacFailed(
"[Encrypt service] Failed to compute MAC." +
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}

const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"[Encrypt service] MAC comparison failed. Key or payload has changed." +
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
Expand Down
7 changes: 6 additions & 1 deletion libs/common/src/tools/send/models/domain/send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ describe("Send", () => {
const view = await send.decrypt();

expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey");
expect(send.name.decrypt).toHaveBeenNthCalledWith(1, null, "cryptoKey");
expect(send.name.decrypt).toHaveBeenNthCalledWith(
1,
null,
"cryptoKey",
"Property: name; ObjectContext: No Domain Context",
);

expect(view).toMatchObject({
id: "id",
Expand Down
6 changes: 3 additions & 3 deletions libs/common/src/vault/models/domain/attachment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("Attachment", () => {
it("uses the provided key without depending on KeyService", async () => {
const providedKey = mock<SymmetricCryptoKey>();

await attachment.decrypt(null, providedKey);
await attachment.decrypt(null, "", providedKey);

expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
Expand All @@ -111,7 +111,7 @@ describe("Attachment", () => {
const orgKey = mock<OrgKey>();
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);

await attachment.decrypt("orgId", null);
await attachment.decrypt("orgId", "", null);

expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey);
Expand All @@ -121,7 +121,7 @@ describe("Attachment", () => {
const userKey = mock<UserKey>();
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);

await attachment.decrypt(null, null);
await attachment.decrypt(null, "", null);

expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
Expand Down
7 changes: 6 additions & 1 deletion libs/common/src/vault/models/domain/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,19 @@ export class Attachment extends Domain {
);
}

async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<AttachmentView> {
async decrypt(
orgId: string,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<AttachmentView> {
const view = await this.decryptObj(
new AttachmentView(this),
{
fileName: null,
},
orgId,
encKey,
"DomainType: Attachment; " + context,
);

if (this.key != null) {
Expand Down
7 changes: 6 additions & 1 deletion libs/common/src/vault/models/domain/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export class Card extends Domain {
);
}

decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<CardView> {
async decrypt(
orgId: string,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<CardView> {
return this.decryptObj(
new CardView(),
{
Expand All @@ -50,6 +54,7 @@ export class Card extends Domain {
},
orgId,
encKey,
"DomainType: Card; " + context,
);
}

Expand Down
Loading
Loading