Skip to content

Commit b3b6b45

Browse files
committed
improvement(security): detect sealed envelopes by their full shape
1 parent 9543e87 commit b3b6b45

3 files changed

Lines changed: 11 additions & 11 deletions

File tree

‎packages/db/script-migrations/0037_seal_account_tokens.integration.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ describe('sealing stored account tokens in PostgreSQL', () => {
3636
})
3737

3838
afterAll(async () => {
39-
process.env.ENCRYPTION_KEY = previousKey
39+
if (previousKey === undefined) Reflect.deleteProperty(process.env, 'ENCRYPTION_KEY')
40+
else process.env.ENCRYPTION_KEY = previousKey
4041
await sql?.end()
4142
if (admin) {
4243
await admin.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`)

‎packages/security/src/encryption.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ describe('unseal', () => {
8383
}
8484
})
8585

86-
it('rejects a malformed envelope', () => {
87-
expect(() => unseal(`${seal('token', KEY)}:extra`, KEY)).toThrow(/Malformed/)
86+
it('treats a raw token that only starts like an envelope as unsealed', () => {
87+
for (const value of ['enc:v1:raw-provider-token', `${seal('token', KEY)}:extra`]) {
88+
expect(isSealed(value)).toBe(false)
89+
expect(() => unseal(value, KEY)).toThrow(/not a sealed envelope/)
90+
}
8891
})
8992
})

‎packages/security/src/encryption.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ function assertKey(key: Buffer): void {
7070
const SEALED_PREFIX = 'enc:v1:'
7171
const SEALED_IV_BYTES = 12
7272
const SEALED_KEY_ID_BYTES = 4
73+
const SEALED_ENVELOPE = /^enc:v1:[0-9a-f]{8}:[0-9a-f]{24}:(?:[0-9a-f]{2})*:[0-9a-f]{32}$/
7374

7475
interface SealingKeys {
7576
keyId: string
@@ -90,7 +91,7 @@ function deriveSealingKeys(key: Buffer): SealingKeys {
9091

9192
/** Whether `value` is an envelope produced by {@link seal}. */
9293
export function isSealed(value: string): boolean {
93-
return value.startsWith(SEALED_PREFIX)
94+
return SEALED_ENVELOPE.test(value)
9495
}
9596

9697
/**
@@ -112,17 +113,12 @@ export function seal(plaintext: string, key: Buffer): string {
112113
}
113114

114115
/**
115-
* Opens an envelope produced by {@link seal}. Throws when the value is not an envelope, was sealed
116+
* Opens an envelope produced by {@link seal}. Throws when the value is not a well-formed envelope, was sealed
116117
* under a different key, or fails GCM authentication.
117118
*/
118119
export function unseal(value: string, key: Buffer): string {
119120
if (!isSealed(value)) throw new Error('Value is not a sealed envelope')
120-
const [keyId, ivHex, ciphertextHex, authTagHex, ...rest] = value
121-
.slice(SEALED_PREFIX.length)
122-
.split(':')
123-
if (!keyId || !ivHex || ciphertextHex === undefined || !authTagHex || rest.length > 0) {
124-
throw new Error('Malformed sealed envelope')
125-
}
121+
const [keyId, ivHex, ciphertextHex, authTagHex] = value.slice(SEALED_PREFIX.length).split(':')
126122
const keys = deriveSealingKeys(key)
127123
if (keyId !== keys.keyId) throw new Error('Sealed envelope was sealed under a different key')
128124
const decipher = createDecipheriv('aes-256-gcm', keys.cipherKey, Buffer.from(ivHex, 'hex'), {

0 commit comments

Comments
 (0)