Skip to content

Commit c24a2ee

Browse files
committed
feat: dumb key rotation system
1 parent 4c4ad23 commit c24a2ee

11 files changed

Lines changed: 224 additions & 211 deletions

src/drivers/aes_256_cbc.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
4848
/**
4949
* Creating chiper
5050
*/
51-
const cipher = createCipheriv('aes-256-cbc', this.cryptoKey, iv)
51+
const cipher = createCipheriv('aes-256-cbc', this.getFirstKey().key, iv)
5252

5353
/**
5454
* Encoding value to a string so that we can set it on the cipher
@@ -70,7 +70,7 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
7070
/**
7171
* Returns the id + result + hmac
7272
*/
73-
const hmac = new Hmac(this.cryptoKey).generate(result)
73+
const hmac = new Hmac(this.getFirstKey().key).generate(result)
7474
return this.computeReturns([this.#config.id, result, hmac])
7575
}
7676

@@ -118,25 +118,27 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
118118
* Make sure the hash is correct, it means the first 2 parts of the
119119
* string are not tampered.
120120
*/
121-
const isValidHmac = new Hmac(this.cryptoKey).compare(
122-
`${encryptedEncoded}${this.separator}${ivEncoded}`,
123-
hash
124-
)
125-
126-
if (!isValidHmac) {
127-
return null
121+
for (const { key } of this.cryptoKeys) {
122+
const isValidHmac = new Hmac(key).compare(
123+
`${encryptedEncoded}${this.separator}${ivEncoded}`,
124+
hash
125+
)
126+
127+
if (!isValidHmac) {
128+
continue
129+
}
130+
131+
/**
132+
* The Decipher can raise exceptions with malformed input, so we wrap it
133+
* to avoid leaking sensitive information
134+
*/
135+
try {
136+
const decipher = createDecipheriv('aes-256-cbc', key, iv)
137+
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
138+
return new MessageBuilder().verify(decrypted, purpose)
139+
} catch {}
128140
}
129141

130-
/**
131-
* The Decipher can raise exceptions with malformed input, so we wrap it
132-
* to avoid leaking sensitive information
133-
*/
134-
try {
135-
const decipher = createDecipheriv('aes-256-cbc', this.cryptoKey, iv)
136-
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
137-
return new MessageBuilder().verify(decrypted, purpose)
138-
} catch {
139-
return null
140-
}
142+
return null
141143
}
142144
}

src/drivers/aes_256_gcm.ts

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
4848
/**
4949
* Creating chiper
5050
*/
51-
const cipher = createCipheriv('aes-256-gcm', this.cryptoKey, iv)
51+
const cipher = createCipheriv('aes-256-gcm', this.getFirstKey().key, iv)
5252

5353
if (purpose) {
5454
cipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
@@ -76,7 +76,7 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
7676
/**
7777
* Returns the id + result + nounce + hmac
7878
*/
79-
const hmac = new Hmac(this.cryptoKey).generate(result)
79+
const hmac = new Hmac(this.getFirstKey().key).generate(result)
8080
return this.computeReturns([this.#config.id, result, nounce, hmac])
8181
}
8282

@@ -132,38 +132,40 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
132132
* Make sure the hash is correct, it means the first 2 parts of the
133133
* string are not tampered.
134134
*/
135-
const isValidHmac = new Hmac(this.cryptoKey).compare(
136-
`${encryptedEncoded}${this.separator}${ivEncoded}`,
137-
hash
138-
)
139-
140-
if (!isValidHmac) {
141-
return null
142-
}
143-
144-
/**
145-
* The Decipher can raise exceptions with malformed input, so we wrap it
146-
* to avoid leaking sensitive information
147-
*/
148-
try {
149-
const decipher = createDecipheriv('aes-256-gcm', this.cryptoKey, iv)
150-
151-
/**
152-
* Set the purpose to decipher
153-
*/
154-
if (purpose) {
155-
decipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
135+
for (const { key } of this.cryptoKeys) {
136+
const isValidHmac = new Hmac(key).compare(
137+
`${encryptedEncoded}${this.separator}${ivEncoded}`,
138+
hash
139+
)
140+
141+
if (!isValidHmac) {
142+
continue
156143
}
157144

158145
/**
159-
* Set the nounce
146+
* The Decipher can raise exceptions with malformed input, so we wrap it
147+
* to avoid leaking sensitive information
160148
*/
161-
decipher.setAuthTag(nounce)
162-
163-
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
164-
return new MessageBuilder().verify(decrypted)
165-
} catch {
166-
return null
149+
try {
150+
const decipher = createDecipheriv('aes-256-gcm', key, iv)
151+
152+
/**
153+
* Set the purpose to decipher
154+
*/
155+
if (purpose) {
156+
decipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
157+
}
158+
159+
/**
160+
* Set the nounce
161+
*/
162+
decipher.setAuthTag(nounce)
163+
164+
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
165+
return new MessageBuilder().verify(decrypted)
166+
} catch {}
167167
}
168+
169+
return null
168170
}
169171
}

src/drivers/base_driver.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,32 @@ export abstract class BaseDriver {
1515
* The key for signing and encrypting values. It is derived
1616
* from the user provided secret.
1717
*/
18-
cryptoKey: Buffer
18+
cryptoKeys = new Set<{ key: Buffer; verifier: MessageVerifier }>()
1919

2020
/**
2121
* Use `dot` as a separator for joining encrypted value, iv and the
2222
* hmac hash. The idea is borrowed from JWTs.
2323
*/
2424
separator = '.'
2525

26-
/**
27-
* Reference to the instance of message verifier for signing
28-
* and verifying values.
29-
*/
30-
verifier: MessageVerifier
31-
3226
protected constructor(config: BaseConfig) {
33-
this.#validateSecret(config.key)
34-
this.cryptoKey = createHash('sha256').update(config.key).digest()
35-
this.verifier = new MessageVerifier(config.key)
27+
if (!config.keys || !Array.isArray(config.keys)) {
28+
throw new errors.E_MISSING_ENCRYPTER_KEY()
29+
}
30+
31+
for (const key of config.keys) {
32+
this.#validateSecret(key)
33+
34+
const cryptoKey = createHash('sha256').update(key).digest()
35+
this.cryptoKeys.add({ key: cryptoKey, verifier: new MessageVerifier(key) })
36+
}
3637
}
3738

3839
/**
3940
* Validates the app secret
4041
*/
41-
#validateSecret(secret?: string) {
42-
if (typeof secret !== 'string') {
42+
#validateSecret(secret: string) {
43+
if (!secret) {
4344
throw new errors.E_MISSING_ENCRYPTER_KEY()
4445
}
4546

@@ -52,11 +53,9 @@ export abstract class BaseDriver {
5253
return values.join(this.separator)
5354
}
5455

55-
/**
56-
* Returns the message verifier instance
57-
*/
58-
getMessageVerifier() {
59-
return this.verifier
56+
getFirstKey() {
57+
const [firstKey] = this.cryptoKeys
58+
return firstKey
6059
}
6160

6261
/**

src/drivers/chacha20_poly1305.ts

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export class ChaCha20Poly1305 extends BaseDriver implements EncryptionDriverCont
4848
/**
4949
* Creating cipher
5050
*/
51-
const cipher = createCipheriv('chacha20-poly1305', this.cryptoKey, iv, { authTagLength: 16 })
51+
const cipher = createCipheriv('chacha20-poly1305', this.getFirstKey().key, iv, {
52+
authTagLength: 16,
53+
})
5254

5355
if (purpose) {
5456
cipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
@@ -76,7 +78,7 @@ export class ChaCha20Poly1305 extends BaseDriver implements EncryptionDriverCont
7678
/**
7779
* Returns the id + result + nounce + hmac
7880
*/
79-
const hmac = new Hmac(this.cryptoKey).generate(result)
81+
const hmac = new Hmac(this.getFirstKey().key).generate(result)
8082
return this.computeReturns([this.#config.id, result, nounce, hmac])
8183
}
8284

@@ -90,10 +92,10 @@ export class ChaCha20Poly1305 extends BaseDriver implements EncryptionDriverCont
9092

9193
/**
9294
* Make sure the encrypted value is in correct format. ie
93-
* [id].[encrypted value].[iv].[nounce].[hash]
95+
* [id].[encrypted value].[iv].[nounce].[hmac]
9496
*/
95-
const [id, encryptedEncoded, ivEncoded, nounceEncoded, hash] = value.split(this.separator)
96-
if (!id || !encryptedEncoded || !ivEncoded || !nounceEncoded || !hash) {
97+
const [id, encryptedEncoded, ivEncoded, nounceEncoded, hmac] = value.split(this.separator)
98+
if (!id || !encryptedEncoded || !ivEncoded || !nounceEncoded || !hmac) {
9799
return null
98100
}
99101

@@ -132,39 +134,42 @@ export class ChaCha20Poly1305 extends BaseDriver implements EncryptionDriverCont
132134
* Make sure the hash is correct, it means the first 2 parts of the
133135
* string are not tampered.
134136
*/
135-
const isValidHmac = new Hmac(this.cryptoKey).compare(
136-
`${encryptedEncoded}${this.separator}${ivEncoded}`,
137-
hash
138-
)
139-
if (!isValidHmac) {
140-
return null
141-
}
142-
143-
/**
144-
* The Decipher can raise exceptions with malformed input, so we wrap it
145-
* to avoid leaking sensitive information
146-
*/
147-
try {
148-
const decipher = createDecipheriv('chacha20-poly1305', this.cryptoKey, iv, {
149-
authTagLength: 16,
150-
})
151-
152-
/**
153-
* Set the purpose to decipher
154-
*/
155-
if (purpose) {
156-
decipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
137+
for (const { key } of this.cryptoKeys) {
138+
const isValidHmac = new Hmac(key).compare(
139+
`${encryptedEncoded}${this.separator}${ivEncoded}`,
140+
hmac
141+
)
142+
143+
if (!isValidHmac) {
144+
continue
157145
}
158146

159147
/**
160-
* Set the nounce
148+
* The Decipher can raise exceptions with malformed input, so we wrap it
149+
* to avoid leaking sensitive information
161150
*/
162-
decipher.setAuthTag(nounce)
163-
164-
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
165-
return new MessageBuilder().verify(decrypted)
166-
} catch {
167-
return null
151+
try {
152+
const decipher = createDecipheriv('chacha20-poly1305', key, iv, {
153+
authTagLength: 16,
154+
})
155+
156+
/**
157+
* Set the purpose to decipher
158+
*/
159+
if (purpose) {
160+
decipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
161+
}
162+
163+
/**
164+
* Set the nounce
165+
*/
166+
decipher.setAuthTag(nounce)
167+
168+
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
169+
return new MessageBuilder().verify(decrypted)
170+
} catch {}
168171
}
172+
173+
return null
169174
}
170175
}

src/drivers/legacy.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class Legacy extends BaseDriver implements EncryptionDriverContract {
4949
/**
5050
* Creating chiper
5151
*/
52-
const cipher = createCipheriv('aes-256-cbc', this.cryptoKey, iv)
52+
const cipher = createCipheriv('aes-256-cbc', this.getFirstKey().key, iv)
5353

5454
/**
5555
* Encoding value to a string so that we can set it on the cipher
@@ -73,7 +73,7 @@ export class Legacy extends BaseDriver implements EncryptionDriverContract {
7373
/**
7474
* Returns the result + hmac
7575
*/
76-
const hmac = new Hmac(this.cryptoKey).generate(result)
76+
const hmac = new Hmac(this.getFirstKey().key).generate(result)
7777
return this.computeReturns([result, hmac])
7878
}
7979

@@ -114,25 +114,28 @@ export class Legacy extends BaseDriver implements EncryptionDriverContract {
114114
* Make sure the hash is correct, it means the first 2 parts of the
115115
* string are not tampered.
116116
*/
117-
const isValidHmac = new Hmac(this.cryptoKey).compare(
118-
`${encryptedEncoded}${this.separator}${ivEncoded}`,
119-
hash
120-
)
121-
122-
if (!isValidHmac) {
123-
return null
117+
for (const { key } of this.cryptoKeys) {
118+
const isValidHmac = new Hmac(key).compare(
119+
`${encryptedEncoded}${this.separator}${ivEncoded}`,
120+
hash
121+
)
122+
123+
if (!isValidHmac) {
124+
continue
125+
}
126+
127+
/**
128+
* The Decipher can raise exceptions with malformed input, so we wrap it
129+
* to avoid leaking sensitive information
130+
*/
131+
try {
132+
const decipher = createDecipheriv('aes-256-cbc', key, iv)
133+
const decrypted = decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8')
134+
135+
return new MessageBuilder().verify(decrypted, purpose)
136+
} catch {}
124137
}
125138

126-
/**
127-
* The Decipher can raise exceptions with malformed input, so we wrap it
128-
* to avoid leaking sensitive information
129-
*/
130-
try {
131-
const decipher = createDecipheriv('aes-256-cbc', this.cryptoKey, iv)
132-
const decrypted = decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8')
133-
return new MessageBuilder().verify(decrypted, purpose)
134-
} catch {
135-
return null
136-
}
139+
return null
137140
}
138141
}

0 commit comments

Comments
 (0)