Skip to content

Commit e4e0f6f

Browse files
authored
fix: Ensures minimum encryption version when decrypting payload (#3045)
* fix: Ensures items key version as minimum when decrypting payload * chore: add more tests
1 parent b8ccb0f commit e4e0f6f

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ContentType } from '@standardnotes/domain-core'
2+
import { EncryptedPayloadInterface, ItemsKeyInterface, ProtocolVersion } from '@standardnotes/models'
3+
import { EncryptionOperatorsInterface } from './EncryptionOperatorsInterface'
4+
import { decryptPayload } from './OperatorWrapper'
5+
6+
describe('decryptPayload', () => {
7+
const createPayload = (version: ProtocolVersion): EncryptedPayloadInterface =>
8+
({
9+
uuid: 'item-1',
10+
version,
11+
content: 'encrypted',
12+
content_type: ContentType.TYPES.Note,
13+
enc_item_key: 'enc-item-key',
14+
}) as EncryptedPayloadInterface
15+
16+
const createKey = (keyVersion: ProtocolVersion): ItemsKeyInterface =>
17+
({
18+
keyVersion,
19+
itemsKey: 'key',
20+
}) as ItemsKeyInterface
21+
22+
describe('version guard', () => {
23+
it('rejects payloads claiming a protocol version below the items key version', async () => {
24+
const payload = createPayload(ProtocolVersion.V001)
25+
const key = createKey(ProtocolVersion.V004)
26+
27+
const operatorManager = {
28+
operatorForVersion: jest.fn(),
29+
} as unknown as EncryptionOperatorsInterface
30+
31+
const result = await decryptPayload(payload, key, operatorManager)
32+
33+
expect(result).toEqual({ uuid: 'item-1', errorDecrypting: true })
34+
expect(operatorManager.operatorForVersion).not.toHaveBeenCalled()
35+
})
36+
37+
it('proceeds when payload version equals items key version', async () => {
38+
const payload = createPayload(ProtocolVersion.V004)
39+
const key = createKey(ProtocolVersion.V004)
40+
const syncOperator = {
41+
generateDecryptedParameters: jest.fn().mockReturnValue({ uuid: 'item-1', content: {} }),
42+
}
43+
const operatorManager = {
44+
operatorForVersion: jest.fn().mockReturnValue(syncOperator),
45+
} as unknown as EncryptionOperatorsInterface
46+
47+
await decryptPayload(payload, key, operatorManager)
48+
49+
expect(operatorManager.operatorForVersion).toHaveBeenCalledWith(ProtocolVersion.V004)
50+
})
51+
52+
it('proceeds when payload version is above items key version', async () => {
53+
const payload = createPayload(ProtocolVersion.V004)
54+
const key = createKey(ProtocolVersion.V003)
55+
const syncOperator = {
56+
generateDecryptedParameters: jest.fn().mockReturnValue({ uuid: 'item-1', content: {} }),
57+
}
58+
const operatorManager = {
59+
operatorForVersion: jest.fn().mockReturnValue(syncOperator),
60+
} as unknown as EncryptionOperatorsInterface
61+
62+
await decryptPayload(payload, key, operatorManager)
63+
64+
expect(operatorManager.operatorForVersion).toHaveBeenCalledWith(ProtocolVersion.V004)
65+
})
66+
})
67+
68+
describe('operator delegation', () => {
69+
it('uses sync operator when operator is not async', async () => {
70+
const payload = createPayload(ProtocolVersion.V004)
71+
const key = createKey(ProtocolVersion.V004)
72+
const decrypted = { uuid: 'item-1', content: { text: 'hello' } }
73+
const syncOperator = {
74+
generateDecryptedParameters: jest.fn().mockReturnValue(decrypted),
75+
}
76+
const operatorManager = {
77+
operatorForVersion: jest.fn().mockReturnValue(syncOperator),
78+
} as unknown as EncryptionOperatorsInterface
79+
80+
const result = await decryptPayload(payload, key, operatorManager)
81+
82+
expect(result).toEqual(decrypted)
83+
expect(syncOperator.generateDecryptedParameters).toHaveBeenCalledWith(
84+
expect.objectContaining({ uuid: 'item-1', version: ProtocolVersion.V004 }),
85+
key,
86+
)
87+
})
88+
89+
it('uses async operator when operator is async', async () => {
90+
const payload = createPayload(ProtocolVersion.V004)
91+
const key = createKey(ProtocolVersion.V004)
92+
const decrypted = { uuid: 'item-1', content: { text: 'hello' } }
93+
const asyncOperator = {
94+
generateEncryptedParametersAsync: jest.fn(),
95+
generateDecryptedParametersAsync: jest.fn().mockResolvedValue(decrypted),
96+
}
97+
const operatorManager = {
98+
operatorForVersion: jest.fn().mockReturnValue(asyncOperator),
99+
} as unknown as EncryptionOperatorsInterface
100+
101+
const result = await decryptPayload(payload, key, operatorManager)
102+
103+
expect(result).toEqual(decrypted)
104+
expect(asyncOperator.generateDecryptedParametersAsync).toHaveBeenCalledWith(
105+
expect.objectContaining({ uuid: 'item-1', version: ProtocolVersion.V004 }),
106+
key,
107+
)
108+
})
109+
110+
it('returns error when operator throws', async () => {
111+
const payload = createPayload(ProtocolVersion.V004)
112+
const key = createKey(ProtocolVersion.V004)
113+
const syncOperator = {
114+
generateDecryptedParameters: jest.fn().mockImplementation(() => {
115+
throw new Error('decryption failed')
116+
}),
117+
}
118+
const operatorManager = {
119+
operatorForVersion: jest.fn().mockReturnValue(syncOperator),
120+
} as unknown as EncryptionOperatorsInterface
121+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
122+
123+
const result = await decryptPayload(payload, key, operatorManager)
124+
125+
expect(result).toEqual({ uuid: 'item-1', errorDecrypting: true })
126+
consoleErrorSpy.mockRestore()
127+
})
128+
})
129+
})

‎packages/encryption/src/Domain/Operator/OperatorWrapper.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
compareVersions,
23
DecryptedPayloadInterface,
34
ItemsKeyInterface,
45
RootKeyInterface,
@@ -44,6 +45,13 @@ export async function decryptPayload<C extends ItemContent = ItemContent>(
4445
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface,
4546
operatorManager: EncryptionOperatorsInterface,
4647
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
48+
if (compareVersions(payload.version, key.keyVersion) < 0) {
49+
return {
50+
uuid: payload.uuid,
51+
errorDecrypting: true,
52+
}
53+
}
54+
4755
const operator = operatorManager.operatorForVersion(payload.version)
4856

4957
try {

0 commit comments

Comments
 (0)