Skip to content

Commit ce11974

Browse files
committed
refactor: remove useless hmac check, derive key for cbc and rename some variables
1 parent 38e18de commit ce11974

15 files changed

Lines changed: 250 additions & 142 deletions

src/base64.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* @boringnode/encryption
3+
*
4+
* @license MIT
5+
* @copyright Boring Node
6+
*/
7+
8+
export function base64UrlEncode(
9+
data: ArrayBuffer | SharedArrayBuffer | Uint8Array | Buffer
10+
): string {
11+
const buffer = Buffer.from(data as any)
12+
13+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
14+
}
15+
16+
export function base64UrlDecode(encoded: string): Buffer
17+
export function base64UrlDecode(encoded: string, encoding: BufferEncoding): string
18+
export function base64UrlDecode(
19+
encoded: string,
20+
encoding?: BufferEncoding
21+
): Buffer | string | null {
22+
const padded = encoded
23+
.replace(/-/g, '+')
24+
.replace(/_/g, '/')
25+
.padEnd(Math.ceil(encoded.length / 4) * 4, '=')
26+
27+
try {
28+
const buffer = Buffer.from(padded, 'base64')
29+
return encoding ? buffer.toString(encoding) : buffer
30+
} catch {
31+
return null
32+
}
33+
}

src/drivers/aes_256_cbc.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
* @copyright Boring Node
66
*/
77

8-
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
8+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto'
99
import { MessageBuilder } from '@poppinss/utils'
1010
import { BaseDriver } from './base_driver.js'
1111
import { Hmac } from '../hmac.js'
1212
import * as errors from '../exceptions.js'
13-
import type { AES256CBCConfig, EncryptionDriverContract } from '../types/main.js'
13+
import type { AES256CBCConfig, CypherText, EncryptionDriverContract } from '../types/main.js'
14+
import { base64UrlDecode, base64UrlEncode } from '../base64.js'
1415

1516
export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
1617
#config: AES256CBCConfig
@@ -39,39 +40,41 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
3940
* You can optionally define a purpose for which the value was encrypted and
4041
* mentioning a different purpose/no purpose during decrypt will fail.
4142
*/
42-
encrypt(payload: any, expiresIn?: string | number, purpose?: string): string {
43+
encrypt(payload: any, expiresIn?: string | number, purpose?: string): CypherText {
4344
/**
4445
* Using a random string as the iv for generating unpredictable values
4546
*/
4647
const iv = randomBytes(16)
4748

49+
const { encryptionKey, authenticationKey } = this.#deriveKey(this.getFirstKey().key, iv)
50+
4851
/**
4952
* Creating chiper
5053
*/
51-
const cipher = createCipheriv('aes-256-cbc', this.getFirstKey().key, iv)
54+
const cipher = createCipheriv('aes-256-cbc', encryptionKey, iv)
5255

5356
/**
5457
* Encoding value to a string so that we can set it on the cipher
5558
*/
56-
const encodedValue = new MessageBuilder().build(payload, expiresIn, purpose)
59+
const plainText = new MessageBuilder().build(payload, expiresIn, purpose)
5760

5861
/**
5962
* Set final to the cipher instance and encrypt it
6063
*/
61-
const encrypted = Buffer.concat([cipher.update(encodedValue, 'utf-8'), cipher.final()])
64+
const cipherText = Buffer.concat([cipher.update(plainText), cipher.final()])
6265

6366
/**
6467
* Concatenate `encrypted value` and `iv` by urlEncoding them. The concatenation is required
6568
* to generate the HMAC, so that HMAC checks for integrity of both the `encrypted value`
6669
* and the `iv`.
6770
*/
68-
const result = `${encrypted.toString('hex')}${this.separator}${iv.toString('hex')}`
71+
const macPayload = `${base64UrlEncode(cipherText)}${this.separator}${base64UrlEncode(iv)}`
6972

7073
/**
7174
* Returns the id + result + hmac
7275
*/
73-
const hmac = new Hmac(this.getFirstKey().key).generate(result)
74-
return this.computeReturns([this.#config.id, result, hmac])
76+
const hmac = new Hmac(authenticationKey).generate(macPayload)
77+
return this.computeReturns([this.#config.id, macPayload, hmac])
7578
}
7679

7780
/**
@@ -83,11 +86,11 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
8386
}
8487

8588
/**
86-
* Make sure the encrypted value is in correct format. ie
87-
* [id].[encrypted value].[iv].[hash]
89+
* Make sure the encrypted value is in the correct format.
90+
* i.e.: [id].[encrypted value].[iv].[mac]
8891
*/
89-
const [id, encryptedEncoded, ivEncoded, hash] = value.split(this.separator)
90-
if (!id || !encryptedEncoded || !ivEncoded || !hash) {
92+
const [id, cipherEncoded, ivEncoded, macEncoded] = value.split(this.separator)
93+
if (!id || !cipherEncoded || !ivEncoded || !macEncoded) {
9194
return null
9295
}
9396

@@ -101,15 +104,15 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
101104
/**
102105
* Make sure we are able to decode the encrypted value
103106
*/
104-
const encrypted = Buffer.from(encryptedEncoded, 'hex')
105-
if (!encrypted) {
107+
const cipherText = base64UrlDecode(cipherEncoded)
108+
if (!cipherText) {
106109
return null
107110
}
108111

109112
/**
110113
* Make sure we are able to decode the iv
111114
*/
112-
const iv = Buffer.from(ivEncoded, 'hex')
115+
const iv = base64UrlDecode(ivEncoded)
113116
if (!iv) {
114117
return null
115118
}
@@ -119,9 +122,11 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
119122
* string are not tampered.
120123
*/
121124
for (const { key } of this.cryptoKeys) {
122-
const isValidHmac = new Hmac(key).compare(
123-
`${encryptedEncoded}${this.separator}${ivEncoded}`,
124-
hash
125+
const { encryptionKey, authenticationKey } = this.#deriveKey(key, iv)
126+
127+
const isValidHmac = new Hmac(authenticationKey).compare(
128+
`${cipherEncoded}${this.separator}${ivEncoded}`,
129+
macEncoded
125130
)
126131

127132
if (!isValidHmac) {
@@ -133,12 +138,24 @@ export class AES256CBC extends BaseDriver implements EncryptionDriverContract {
133138
* to avoid leaking sensitive information
134139
*/
135140
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)
141+
const decipher = createDecipheriv('aes-256-cbc', encryptionKey, iv)
142+
const plainTextBuffer = Buffer.concat([decipher.update(cipherText), decipher.final()])
143+
return new MessageBuilder().verify(plainTextBuffer.toString('utf-8'), purpose)
139144
} catch {}
140145
}
141146

142147
return null
143148
}
149+
150+
#deriveKey(masterKey: Buffer, iv: Buffer) {
151+
const info = Buffer.from(this.#config.id)
152+
const rawDerivedKey = hkdfSync('sha256', masterKey, iv, info, 64)
153+
154+
const derivedKey = Buffer.isBuffer(rawDerivedKey) ? rawDerivedKey : Buffer.from(rawDerivedKey)
155+
156+
return {
157+
encryptionKey: derivedKey.subarray(0, 32),
158+
authenticationKey: derivedKey.subarray(32),
159+
}
160+
}
144161
}

src/drivers/aes_256_gcm.ts

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
99
import { MessageBuilder } from '@poppinss/utils'
1010
import { BaseDriver } from './base_driver.js'
11-
import { Hmac } from '../hmac.js'
1211
import * as errors from '../exceptions.js'
13-
import type { AES256GCMConfig, EncryptionDriverContract } from '../types/main.js'
12+
import { base64UrlDecode, base64UrlEncode } from '../base64.js'
13+
import type { AES256GCMConfig, CypherText, EncryptionDriverContract } from '../types/main.js'
1414

1515
export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
1616
#config: AES256GCMConfig
@@ -39,11 +39,11 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
3939
* You can optionally define a purpose for which the value was encrypted and
4040
* mentioning a different purpose/no purpose during decrypt will fail.
4141
*/
42-
encrypt(payload: any, expiresIn?: string | number, purpose?: string): string {
42+
encrypt(payload: any, expiresIn?: string | number, purpose?: string): CypherText {
4343
/**
4444
* Using a random string as the iv for generating unpredictable values
4545
*/
46-
const iv = randomBytes(16)
46+
const iv = randomBytes(12)
4747

4848
/**
4949
* Creating chiper
@@ -57,27 +57,21 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
5757
/**
5858
* Encoding value to a string so that we can set it on the cipher
5959
*/
60-
const encodedValue = new MessageBuilder().build(payload, expiresIn)
60+
const plainText = new MessageBuilder().build(payload, expiresIn)
6161

6262
/**
6363
* Set final to the cipher instance and encrypt it
6464
*/
65-
const encrypted = Buffer.concat([cipher.update(encodedValue, 'utf-8'), cipher.final()])
65+
const cipherText = Buffer.concat([cipher.update(plainText), cipher.final()])
6666

67-
/**
68-
* Concatenate `encrypted value` and `iv` by urlEncoding them. The concatenation is required
69-
* to generate the HMAC, so that HMAC checks for integrity of both the `encrypted value`
70-
* and the `iv`.
71-
*/
72-
const result = `${encrypted.toString('hex')}${this.separator}${iv.toString('hex')}`
73-
74-
const nounce = cipher.getAuthTag().toString('hex')
67+
const tag = cipher.getAuthTag()
7568

76-
/**
77-
* Returns the id + result + nounce + hmac
78-
*/
79-
const hmac = new Hmac(this.getFirstKey().key).generate(result)
80-
return this.computeReturns([this.#config.id, result, nounce, hmac])
69+
return this.computeReturns([
70+
this.#config.id,
71+
base64UrlEncode(cipherText),
72+
base64UrlEncode(iv),
73+
base64UrlEncode(tag),
74+
])
8175
}
8276

8377
/**
@@ -89,11 +83,11 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
8983
}
9084

9185
/**
92-
* Make sure the encrypted value is in correct format. ie
93-
* [id].[encrypted value].[iv].[nounce].[hash]
86+
* Make sure the encrypted value is in the correct format.
87+
* i.e.: [id].[encrypted value].[iv].[tag]
9488
*/
95-
const [id, encryptedEncoded, ivEncoded, nounceEncoded, hash] = value.split(this.separator)
96-
if (!id || !encryptedEncoded || !ivEncoded || !nounceEncoded || !hash) {
89+
const [id, cipherEncoded, ivEncoded, tagEncoded] = value.split(this.separator)
90+
if (!id || !cipherEncoded || !ivEncoded || !tagEncoded) {
9791
return null
9892
}
9993

@@ -107,62 +101,43 @@ export class AES256GCM extends BaseDriver implements EncryptionDriverContract {
107101
/**
108102
* Make sure we are able to decode the encrypted value
109103
*/
110-
const encrypted = Buffer.from(encryptedEncoded, 'hex')
111-
if (!encrypted) {
104+
const cipherText = base64UrlDecode(cipherEncoded)
105+
if (!cipherText) {
112106
return null
113107
}
114108

115109
/**
116110
* Make sure we are able to decode the iv
117111
*/
118-
const iv = Buffer.from(ivEncoded, 'hex')
112+
const iv = base64UrlDecode(ivEncoded)
119113
if (!iv) {
120114
return null
121115
}
122116

123117
/**
124-
* Make sure we are able to decode the nounce
118+
* Make sure we are able to decode the tag
125119
*/
126-
const nounce = Buffer.from(nounceEncoded, 'hex')
127-
if (!nounce) {
120+
const tag = base64UrlDecode(tagEncoded)
121+
if (!tag) {
128122
return null
129123
}
130124

131-
/**
132-
* Make sure the hash is correct, it means the first 2 parts of the
133-
* string are not tampered.
134-
*/
135125
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
143-
}
144-
145126
/**
146127
* The Decipher can raise exceptions with malformed input, so we wrap it
147128
* to avoid leaking sensitive information
148129
*/
149130
try {
150131
const decipher = createDecipheriv('aes-256-gcm', key, iv)
151132

152-
/**
153-
* Set the purpose to decipher
154-
*/
155133
if (purpose) {
156134
decipher.setAAD(Buffer.from(purpose), { plaintextLength: Buffer.byteLength(purpose) })
157135
}
158136

159-
/**
160-
* Set the nounce
161-
*/
162-
decipher.setAuthTag(nounce)
137+
decipher.setAuthTag(tag)
163138

164-
const decrypted = decipher.update(encrypted) + decipher.final('utf8')
165-
return new MessageBuilder().verify(decrypted)
139+
const plain = Buffer.concat([decipher.update(cipherText), decipher.final()])
140+
return new MessageBuilder().verify(plain)
166141
} catch {}
167142
}
168143

src/drivers/base_driver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { createHash } from 'node:crypto'
99
import * as errors from '../exceptions.js'
1010
import { MessageVerifier } from '../message_verifier.js'
11-
import type { BaseConfig } from '../types/main.js'
11+
import type { BaseConfig, CypherText } from '../types/main.js'
1212

1313
export abstract class BaseDriver {
1414
/**
@@ -50,7 +50,7 @@ export abstract class BaseDriver {
5050
}
5151

5252
computeReturns(values: string[]) {
53-
return values.join(this.separator)
53+
return values.join(this.separator) as CypherText
5454
}
5555

5656
getFirstKey() {
@@ -72,7 +72,7 @@ export abstract class BaseDriver {
7272
* You can optionally define a purpose for which the value was encrypted and
7373
* mentioning a different purpose/no purpose during decrypt will fail.
7474
*/
75-
abstract encrypt(payload: any, expiresIn?: string | number, purpose?: string): string
75+
abstract encrypt(payload: any, expiresIn?: string | number, purpose?: string): CypherText
7676

7777
/**
7878
* Decrypt value and verify it against a purpose

0 commit comments

Comments
 (0)