diff --git a/lib/jwa/aes_cbc_hmac_sha2.js b/lib/jwa/aes_cbc_hmac_sha2.js new file mode 100644 index 00000000..63b58622 --- /dev/null +++ b/lib/jwa/aes_cbc_hmac_sha2.js @@ -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}`))) + }) +} diff --git a/lib/jwa/index.js b/lib/jwa/index.js index bace3dbd..e857e9fa 100644 --- a/lib/jwa/index.js +++ b/lib/jwa/index.js @@ -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) } } diff --git a/lib/jwe/generate_cek.js b/lib/jwe/generate_cek.js index 636f8a1e..41454a1c 100644 --- a/lib/jwe/generate_cek.js +++ b/lib/jwe/generate_cek.js @@ -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 }) } diff --git a/lib/jwe/generate_iv.js b/lib/jwe/generate_iv.js new file mode 100644 index 00000000..fe475ad5 --- /dev/null +++ b/lib/jwe/generate_iv.js @@ -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) +} diff --git a/lib/jwe/serializers.js b/lib/jwe/serializers.js new file mode 100644 index 00000000..271919d8 --- /dev/null +++ b/lib/jwe/serializers.js @@ -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 +} diff --git a/lib/jws/verify.js b/lib/jws/verify.js index aa2c52ba..5d48baa2 100644 --- a/lib/jws/verify.js +++ b/lib/jws/verify.js @@ -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) }