-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[TDL-11] Proposal - item decryption refactor (alternative proposal) #3738
Changes from all commits
cdb26d6
cfe2339
6013538
0adf42c
ea818a1
b7e3d86
4ac5152
dbd744b
165267f
b609826
51ce66a
433094a
c1ae51a
ef1954c
1712606
dd3b040
1698dec
d79c8b6
d01adc8
aebd065
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { encrypted, getEncryptedProperties } from "@bitwarden/common/misc/encrypted.decorator"; | ||
|
||
class TestClass { | ||
@encrypted | ||
encryptedString: string; | ||
@encrypted | ||
anotherEncryptedString: string; | ||
|
||
someOtherProperty: Date; | ||
} | ||
|
||
describe("encrypted decorator", () => { | ||
it("adds property name to list", () => { | ||
const testClass = new TestClass(); | ||
const result = getEncryptedProperties(testClass); | ||
expect(result).toEqual(["encryptedString", "anotherEncryptedString"]); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Decryptable } from "../../src/interfaces/decryptable.interface"; | ||
import { encrypted } from "../../src/misc/encrypted.decorator"; | ||
import { EncString } from "../../src/models/domain/encString"; | ||
|
||
export class SimpleEncryptedObjectView { | ||
id: number; | ||
username: string; | ||
password: string; | ||
} | ||
|
||
/** | ||
* An object with EncStrings and non-encrypted fields. | ||
*/ | ||
export class SimpleEncryptedObject implements Decryptable<SimpleEncryptedObjectView> { | ||
@encrypted username = new EncString("3.myUsername" + this.id + "_Encrypted"); | ||
@encrypted password = new EncString("3.myPassword" + this.id + "_Encrypted"); | ||
|
||
constructor(public id: number) {} | ||
|
||
toView(decryptedProperties: any) { | ||
return Object.assign( | ||
new SimpleEncryptedObjectView(), | ||
{ | ||
// Manually copy over unencrypted values | ||
id: this.id, | ||
}, | ||
decryptedProperties // Assign everything else from the decryptedProperties object | ||
); | ||
} | ||
} | ||
|
||
export class NestedEncryptedObjectView {} | ||
|
||
/** | ||
* An object with nested encrypted objects (i.e. objects containing EncStrings) | ||
*/ | ||
export class NestedEncryptedObject implements Decryptable<NestedEncryptedObjectView> { | ||
@encrypted nestedLogin1 = new SimpleEncryptedObject(1); | ||
@encrypted nestedLogin2 = new SimpleEncryptedObject(2); | ||
collectionId = "myCollectionId"; | ||
|
||
toView(decryptedProperties: any) { | ||
return Object.assign( | ||
new NestedEncryptedObjectView(), | ||
{ | ||
// Manually copy over unencrypted values | ||
collectionId: this.collectionId, | ||
}, | ||
decryptedProperties // Assign everything else from the decryptedProperties object | ||
); | ||
} | ||
} | ||
|
||
export class NestedArrayEncryptedObjectView {} | ||
|
||
/** | ||
* An object with nested encrypted objects (i.e. objects containing EncStrings) in an array | ||
*/ | ||
export class NestedArrayEncryptedObject implements Decryptable<NestedArrayEncryptedObjectView> { | ||
@encrypted logins = [new SimpleEncryptedObject(1), new SimpleEncryptedObject(2)]; | ||
collectionId = "myCollectionId"; | ||
|
||
toView(decryptedProperties: any) { | ||
return Object.assign( | ||
new NestedArrayEncryptedObjectView(), | ||
{ | ||
// Manually copy over unencrypted values | ||
collectionId: this.collectionId, | ||
}, | ||
decryptedProperties // Assign everything else from the decryptedProperties object | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface Decryptable<T> { | ||
toView: (decryptedProperties: any) => T; | ||
} | ||
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a good idea to convert this to a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with that, but decryptItem<TEncrypted, TDecrypted>(item: TEncrypted, key: SymmetricCryptoKey, viewFactory: (props: DecryptedProperties<TEncrypted>) => TDecrypted): TDecrypted assuming that |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import "reflect-metadata"; // TODO: this can only be imported in one place, so shouldn't be here | ||
|
||
const metadataKey = Symbol("encryptedPropertiesKey"); | ||
|
||
export function encrypted(prototype: any, propertyKey: string) { | ||
const encStringList: string[] = Reflect.getMetadata(metadataKey, prototype); | ||
if (encStringList == null) { | ||
Reflect.defineMetadata(metadataKey, [propertyKey], prototype); | ||
} else { | ||
encStringList.push(propertyKey); | ||
} | ||
} | ||
|
||
export function getEncryptedProperties(target: any): string[] { | ||
return Reflect.getMetadata(metadataKey, target); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,14 @@ import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; | |
import { LogService } from "../abstractions/log.service"; | ||
import { EncryptionType } from "../enums/encryptionType"; | ||
import { IEncrypted } from "../interfaces/IEncrypted"; | ||
import { Decryptable } from "../interfaces/decryptable.interface"; | ||
import { getEncryptedProperties } from "../misc/encrypted.decorator"; | ||
import { Utils } from "../misc/utils"; | ||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; | ||
import { EncString } from "../models/domain/encString"; | ||
import { EncryptedObject } from "../models/domain/encryptedObject"; | ||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; | ||
import { View } from "../models/view/view"; | ||
|
||
export class EncryptService implements AbstractEncryptService { | ||
constructor( | ||
|
@@ -164,6 +167,61 @@ export class EncryptService implements AbstractEncryptService { | |
return obj; | ||
} | ||
|
||
async decryptItem<T>(item: Decryptable<T>, key: SymmetricCryptoKey): Promise<T> { | ||
if (item == null) { | ||
throw new Error("Cannot decrypt a null item"); | ||
} | ||
|
||
const promises: Promise<any>[] = []; | ||
const decryptedValues: Record<string, string | View | Array<View>> = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably BufferArray is needed here, too. |
||
|
||
const encryptedProperties = getEncryptedProperties(item); | ||
encryptedProperties?.forEach((prop) => { | ||
const propValue = (item as any)[prop]; | ||
if (propValue == null) { | ||
return; | ||
} | ||
|
||
// Case 1: the property is an EncString | ||
if (propValue instanceof EncString) { | ||
promises.push( | ||
this.decryptToUtf8(propValue, key) | ||
.then((decryptedValue) => (decryptedValues[prop] = decryptedValue)) | ||
.catch((e) => { | ||
this.logService.error(e); | ||
decryptedValues[prop] = "[error: cannot decrypt]"; | ||
}) | ||
); | ||
return; | ||
} | ||
|
||
// Case 2: the property is an array of nested decryptables | ||
// NB: arrays of EncStrings are not supported at this time (no current use cases) | ||
if (propValue instanceof Array) { | ||
decryptedValues[prop] = []; | ||
|
||
propValue.forEach((subItem) => | ||
promises.push( | ||
this.decryptItem(subItem, key).then((decryptedValue) => | ||
(decryptedValues[prop] as Array<View>).push(decryptedValue) | ||
) | ||
) | ||
); | ||
} else { | ||
// Case 3: the property is a single nested decryptable | ||
promises.push( | ||
this.decryptItem(propValue, key).then( | ||
(decryptedValue) => (decryptedValues[prop] = decryptedValue) | ||
) | ||
); | ||
} | ||
}); | ||
|
||
await Promise.all(promises); | ||
|
||
return item.toView(decryptedValues); | ||
} | ||
|
||
private logMacFailed(msg: string) { | ||
if (this.logMacFailures) { | ||
this.logService.error(msg); | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't particularly like how this turned out of any of the examples.
For one, the decrypted properties is an any rather than some inferred type (think
Jsonify
magic).Second, it assumes that decrypted properties are going to be named the same in the View layer.
I think some work needs to be done to improve this interface a bit before it's ready to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was a bit of a hack to illustrate the idea of an intermediate "decrypted properties" object, as a way to get decrypted values to the View without caching them on Domain. It can be improved for sure.
I wasn't sure whether mapping property values to different properties on the View object was actually required. We have that capability at the moment and I don't think we actually use it anywhere. But it's probably best to keep it for flexibility, especially if we're considering having multiple Views.
Typing this correctly was a
TODO
, in theory we should be able to have a mapped type like:EncStrings
becomestrings
Domain
becomes the correspondingView
(?)So I can definitely experiment with that further. As you say, jsonify magic.