mirror of
https://github.com/danbulant/jose
synced 2026-05-24 20:41:46 +00:00
feat: encryption with AES_CBC_HMAC_SHA2
This commit is contained in:
parent
b44e15fbb8
commit
b247fc33df
6 changed files with 155 additions and 2 deletions
84
lib/jwa/aes_cbc_hmac_sha2.js
Normal file
84
lib/jwa/aes_cbc_hmac_sha2.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const { createCipheriv, createDecipheriv } = require('crypto')
|
||||
const { timingSafeEqual } = require('crypto')
|
||||
const { strict: assert } = require('assert')
|
||||
|
||||
const { TODO } = require('../errors')
|
||||
|
||||
const MAX_INT32 = Math.pow(2, 32)
|
||||
|
||||
const uint64be = (value, buf = Buffer.alloc(8)) => {
|
||||
const high = Math.floor(value / MAX_INT32)
|
||||
const low = value % MAX_INT32
|
||||
|
||||
buf.writeUInt32BE(high, 0)
|
||||
buf.writeUInt32BE(low, 4)
|
||||
return buf
|
||||
}
|
||||
|
||||
const ivCheck = (iv) => {
|
||||
if (!iv || iv.length !== 16) {
|
||||
throw new TODO('invalid iv')
|
||||
}
|
||||
}
|
||||
const keyCheck = (key, size) => {
|
||||
if ((size << 1) !== (key.length << 3)) {
|
||||
throw new TODO('invalid key size')
|
||||
}
|
||||
}
|
||||
|
||||
const encrypt = (size, sign, { keyObject }, payload, { iv, aad = Buffer.alloc(0) }) => {
|
||||
const key = keyObject.export()
|
||||
ivCheck(iv)
|
||||
keyCheck(key, size)
|
||||
|
||||
const encKey = key.slice(size / 8)
|
||||
const cipher = createCipheriv(`AES-${size}-CBC`, encKey, iv)
|
||||
const ciphertext = Buffer.concat([cipher.update(payload), cipher.final()])
|
||||
const macData = Buffer.concat([aad, iv, ciphertext, uint64be(aad.length * 8)])
|
||||
|
||||
const macKey = key.slice(0, size / 8)
|
||||
const tag = sign({ keyObject: macKey }, macData).slice(0, size / 8)
|
||||
|
||||
return { ciphertext, tag }
|
||||
}
|
||||
|
||||
const verify = (actual, expected) => {
|
||||
if (expected.length !== actual.length) {
|
||||
return timingSafeEqual(actual, Buffer.allocUnsafe(actual.length))
|
||||
}
|
||||
|
||||
return timingSafeEqual(actual, expected)
|
||||
}
|
||||
|
||||
const decrypt = (size, sign, { keyObject }, ciphertext, { iv, tag = Buffer.alloc(0), aad = Buffer.alloc(0) }) => {
|
||||
const key = keyObject.export()
|
||||
ivCheck(iv)
|
||||
keyCheck(key, size)
|
||||
|
||||
const encKey = key.slice(size / 8)
|
||||
const macKey = key.slice(0, size / 8)
|
||||
|
||||
const macData = Buffer.concat([aad, iv, ciphertext, uint64be(aad.length * 8)])
|
||||
|
||||
const expectedTag = sign({ keyObject: macKey }, macData, tag).slice(0, size / 8)
|
||||
|
||||
if (!verify(tag, expectedTag)) {
|
||||
throw new TODO('mac check failed')
|
||||
}
|
||||
|
||||
const cipher = createDecipheriv(`AES-${size}-CBC`, encKey, iv)
|
||||
|
||||
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
|
||||
}
|
||||
|
||||
module.exports = (JWA) => {
|
||||
['A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512'].forEach((jwaAlg) => {
|
||||
const size = parseInt(jwaAlg.substr(1, 3), 10)
|
||||
|
||||
assert(!JWA.encrypt.has(jwaAlg), `encrypt alg ${jwaAlg} already registered`)
|
||||
assert(!JWA.decrypt.has(jwaAlg), `decrypt alg ${jwaAlg} already registered`)
|
||||
|
||||
JWA.encrypt.set(jwaAlg, encrypt.bind(undefined, size, JWA.sign.get(`HS${size * 2}`)))
|
||||
JWA.decrypt.set(jwaAlg, decrypt.bind(undefined, size, JWA.sign.get(`HS${size * 2}`)))
|
||||
})
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ const JWA = {
|
|||
verify: new Map(),
|
||||
wrapKey: new Map(),
|
||||
unwrapKey: new Map(),
|
||||
encrypt: new Map(),
|
||||
decrypt: new Map()
|
||||
}
|
||||
|
||||
// sign, verify
|
||||
|
|
@ -16,6 +18,9 @@ require('./rsassa_pkcs1')(JWA)
|
|||
// wrapKey, unwrapKey
|
||||
require('./rsaes')(JWA)
|
||||
|
||||
// encrypt, decrypt
|
||||
require('./aes_cbc_hmac_sha2')(JWA)
|
||||
|
||||
module.exports = {
|
||||
sign: (alg, key, payload) => {
|
||||
if (!JWA.sign.has(alg)) {
|
||||
|
|
@ -45,7 +50,7 @@ module.exports = {
|
|||
throw new TODO(`the key does not support ${alg} wrapKey algorithm`)
|
||||
}
|
||||
|
||||
return JWA.sign.get(alg)(key, payload)
|
||||
return JWA.wrapKey.get(alg)(key, payload)
|
||||
},
|
||||
unwrapKey: (alg, key, payload) => {
|
||||
if (!JWA.unwrapKey.has(alg)) {
|
||||
|
|
@ -56,5 +61,25 @@ module.exports = {
|
|||
}
|
||||
|
||||
return JWA.unwrapKey.get(alg)(key, payload)
|
||||
},
|
||||
encrypt: (alg, key, cleartext, opts) => {
|
||||
if (!JWA.encrypt.has(alg)) {
|
||||
throw new TODO(`encrypt alg ${alg} not implemented`)
|
||||
}
|
||||
if (!key.algorithms('encrypt').has(alg)) {
|
||||
throw new TODO(`the key does not support ${alg} encrypt algorithm`)
|
||||
}
|
||||
|
||||
return JWA.encrypt.get(alg)(key, cleartext, opts)
|
||||
},
|
||||
decrypt: (alg, key, ciphertext, opts) => {
|
||||
if (!JWA.decrypt.has(alg)) {
|
||||
throw new TODO(`decrypt alg ${alg} not implemented`)
|
||||
}
|
||||
if (!key.algorithms('decrypt').has(alg)) {
|
||||
throw new TODO(`the key does not support ${alg} decrypt algorithm`)
|
||||
}
|
||||
|
||||
return JWA.decrypt.get(alg)(key, ciphertext, opts)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ module.exports = (alg) => {
|
|||
throw new TypeError('unsupported intended content encryption key alg')
|
||||
}
|
||||
|
||||
return new OctKey(createSecretKey(randomBytes(byteLength)))
|
||||
return new OctKey(createSecretKey(randomBytes(byteLength)), { use: 'enc', alg })
|
||||
}
|
||||
|
|
|
|||
20
lib/jwe/generate_iv.js
Normal file
20
lib/jwe/generate_iv.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const { randomBytes } = require('crypto')
|
||||
|
||||
const IVLENGTHS = {
|
||||
'A128CBC-HS256': 128 / 8,
|
||||
'A192CBC-HS384': 128 / 8,
|
||||
'A256CBC-HS512': 128 / 8,
|
||||
'A128GCM': 96 / 8,
|
||||
'A192GCM': 96 / 8,
|
||||
'A256GCM': 96 / 8
|
||||
}
|
||||
|
||||
module.exports = (alg) => {
|
||||
const byteLength = IVLENGTHS[alg]
|
||||
|
||||
if (byteLength === undefined) {
|
||||
throw new TypeError('unsupported intended content encryption key alg')
|
||||
}
|
||||
|
||||
return randomBytes(byteLength)
|
||||
}
|
||||
23
lib/jwe/serializers.js
Normal file
23
lib/jwe/serializers.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const { TODO } = require('../errors')
|
||||
|
||||
function detect (input) {
|
||||
if (typeof input === 'string') {
|
||||
return 'compact'
|
||||
}
|
||||
|
||||
if ('encrypted_key' in input) {
|
||||
return 'flattened'
|
||||
}
|
||||
|
||||
if ('recipients' in input && 'ciphertext' in input && Array.isArray(input.recipients)) {
|
||||
if (input.signatures.every(s => 'encrypted_key' in s)) {
|
||||
return 'general'
|
||||
}
|
||||
}
|
||||
|
||||
throw new TODO('invalid serialization')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detect
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ module.exports = (jws, key) => {
|
|||
if (SINGLE_RECIPIENT.has(serialization)) {
|
||||
if (serialization === 'compact') { // compact serialization format
|
||||
([prot, payload, signature] = jws.split('.'))
|
||||
// TODO: assert prot, payload, signature
|
||||
} else { // flattened serialization format
|
||||
({ protected: prot, payload, signature } = jws)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue