refactor: cleanup, TODO chores

This commit is contained in:
Filip Skokan 2019-02-10 21:39:59 +01:00
parent 775ea638b6
commit cc89d4e02b
25 changed files with 296 additions and 318 deletions

View file

@ -1,37 +1,43 @@
const CODES = {
TODO: 'ERR_TODO',
JOSEAlgNotSupported: 'ERR_JOSE_ALG_NOT_SUPPORTED',
JWEDecryptionFailed: 'ERR_JWE_DECRYPTION_FAILED',
JWEInvalid: 'ERR_JWE_INVALID',
JWEInvalidHeader: 'ERR_JWE_INVALID_HEADER',
JWENoRecipients: 'ERR_JWE_NO_RECIPIENTS',
JWKImportFailed: 'ERR_JWK_IMPORT_FAILED',
JWKKeySupport: 'ERR_JWK_KEY_SUPPORT',
JWSInvalidHeader: 'ERR_JWS_INVALID_HEADER',
JWSMissingAlg: 'ERR_JWS_ALG_MISSING',
JWSNoRecipients: 'ERR_JWS_NO_RECIPIENTS',
JWSVerificationFailed: 'ERR_JWS_VERIFICATION_FAILED',
JWTAudienceMismatch: 'ERR_JWT_AUDIENCE_MISMATCH',
JWTInvalidAlgorithm: 'ERR_JWT_INVALID_ALGORITHM',
JWTIssuerMismatch: 'ERR_JWT_ISSUER_MISMATCH',
JWTNonceMismatch: 'ERR_JWT_NONCE_MISMATCH',
JWTSubjectMismatch: 'ERR_JWT_SUBJECT_MISMATCH',
JWTTokenIdMismatch: 'ERR_JWT_TOKEN_ID_MISMATCH'
JWSVerificationFailed: 'ERR_JWS_VERIFICATION_FAILED'
}
const DEFAULT_MESSAGES = {
JWEDecryptionFailed: 'decryption operation failed',
JWSVerificationFailed: 'signature verification failed'
}
class JoseError extends Error {
constructor (message) {
super(message)
if (message === undefined) {
message = DEFAULT_MESSAGES[this.constructor.name]
}
this.name = this.constructor.name
this.code = CODES[this.constructor.name]
Error.captureStackTrace(this, this.constructor)
}
}
module.exports.JOSEAlgNotSupported = class JOSEAlgNotSupported extends JoseError {}
module.exports.JWEDecryptionFailed = class JWEDecryptionFailed extends JoseError {}
module.exports.JWEInvalid = class JWEInvalid extends JoseError {}
module.exports.JWEInvalidHeader = class JWEInvalidHeader extends JoseError {}
module.exports.JWENoRecipients = class JWENoRecipients extends JoseError {}
module.exports.JWKImportFailed = class JWKImportFailed extends JoseError {}
module.exports.JWKKeySupport = class JWKKeySupport extends JoseError {}
module.exports.JWSInvalidHeader = class JWSInvalidHeader extends JoseError {}
module.exports.JWSMissingAlg = class JWSMissingAlg extends JoseError {}
module.exports.JWSNoRecipients = class JWSNoRecipients extends JoseError {}
module.exports.JWSVerificationFailed = class JWSVerificationFailed extends JoseError {}
module.exports.JWTAudienceMismatch = class JWTAudienceMismatch extends JoseError {}
module.exports.JWTInvalidAlgorithm = class JWTInvalidAlgorithm extends JoseError {}
module.exports.JWTIssuerMismatch = class JWTIssuerMismatch extends JoseError {}
module.exports.JWTNonceMismatch = class JWTNonceMismatch extends JoseError {}
module.exports.JWTSubjectMismatch = class JWTSubjectMismatch extends JoseError {}
module.exports.JWTTokenIdMismatch = class JWTTokenIdMismatch extends JoseError {}
module.exports.TODO = class TODO extends JoseError {}

View file

@ -12,12 +12,4 @@ const IVLENGTHS = {
'A256GCMKW': 96 / 8
}
module.exports = (alg) => {
const byteLength = IVLENGTHS[alg]
if (byteLength === undefined) {
throw new TypeError('unsupported intended content encryption key alg')
}
return randomBytes(byteLength)
}
module.exports = alg => randomBytes(IVLENGTHS[alg])

View file

@ -1,8 +1,6 @@
const { generateKeyPairSync, createSecretKey } = require('crypto')
// TODO: reach out to @tniessen to request constructors being exposed for this exact purpose
// (verifying inputs are already KeyObjects)
// TODO: what's the least blocking way to get to the constructors
// TODO: keep an eye out for utils.isCryptoKeyObject() for verifying inputs are already KeyObjects
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
const PrivateKeyObject = privateKey.constructor

View file

@ -1,19 +1,22 @@
const { createCipheriv, createDecipheriv } = require('crypto')
const { strict: assert } = require('assert')
const { TODO } = require('../errors')
const { JWEInvalid, JWEDecryptionFailed } = require('../errors')
const uint64be = require('../help/uint64be')
const timingSafeEqual = require('../help/timing_safe_equal')
const ivCheck = (iv) => {
if (!iv || iv.length !== 16) {
throw new TODO('invalid iv')
const checkInput = (iv) => {
if (!iv) {
throw new JWEInvalid('missing iv')
}
if (iv.length !== 16) {
throw new JWEInvalid('invalid iv')
}
}
const encrypt = (size, sign, { keyObject }, cleartext, { iv, aad = Buffer.alloc(0) }) => {
const key = keyObject.export()
ivCheck(iv)
checkInput(iv)
const keySize = size / 8
const encKey = key.slice(keySize)
@ -28,24 +31,28 @@ const encrypt = (size, sign, { keyObject }, cleartext, { iv, aad = Buffer.alloc(
}
const decrypt = (size, sign, { keyObject }, ciphertext, { iv, tag = Buffer.alloc(0), aad = Buffer.alloc(0) }) => {
const key = keyObject.export()
ivCheck(iv)
checkInput(iv)
const keySize = size / 8
const key = keyObject.export()
const encKey = key.slice(keySize)
const macKey = key.slice(0, keySize)
const macData = Buffer.concat([aad, iv, ciphertext, uint64be(aad.length * 8)])
const expectedTag = sign({ keyObject: macKey }, macData, tag).slice(0, keySize)
const macCheckPassed = timingSafeEqual(tag, expectedTag)
if (!timingSafeEqual(tag, expectedTag)) {
throw new TODO('mac check failed')
let cleartext
try {
const cipher = createDecipheriv(`AES-${size}-CBC`, encKey, iv)
cleartext = Buffer.concat([cipher.update(ciphertext), cipher.final()])
} catch (err) {}
if (!cleartext || !macCheckPassed) {
throw new JWEDecryptionFailed()
}
const cipher = createDecipheriv(`AES-${size}-CBC`, encKey, iv)
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
return cleartext
}
module.exports = (JWA) => {

View file

@ -1,8 +1,24 @@
const { createCipheriv, createDecipheriv } = require('crypto')
const { strict: assert } = require('assert')
const { JWEInvalid, JWEDecryptionFailed } = require('../errors')
const checkInput = (size, keyLen, iv, tag) => {
if (keyLen * 8 !== size) {
throw new JWEInvalid('invalid key length')
}
if (!iv) {
throw new JWEInvalid('missing iv')
}
if (iv.length !== 12) {
throw new JWEInvalid('invalid iv')
}
if (tag !== undefined && tag.length !== 16) {
throw new JWEInvalid('invalid tag length')
}
}
const encrypt = (size, { keyObject }, cleartext, { iv, aad = Buffer.alloc(0) }) => {
// TODO: commonCheck
checkInput(size, keyObject.symmetricKeySize, iv)
const cipher = createCipheriv(`AES-${size}-GCM`, keyObject, iv)
cipher.setAAD(aad)
@ -14,13 +30,17 @@ const encrypt = (size, { keyObject }, cleartext, { iv, aad = Buffer.alloc(0) })
}
const decrypt = (size, { keyObject }, ciphertext, { iv, tag = Buffer.alloc(0), aad = Buffer.alloc(0) }) => {
// TODO: commonCheck
checkInput(size, keyObject.symmetricKeySize, iv, tag, aad)
const cipher = createDecipheriv(`AES-${size}-GCM`, keyObject, iv)
cipher.setAuthTag(tag)
cipher.setAAD(aad)
try {
const cipher = createDecipheriv(`AES-${size}-GCM`, keyObject, iv)
cipher.setAuthTag(tag)
cipher.setAAD(aad)
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
} catch (err) {
throw new JWEDecryptionFailed()
}
}
module.exports = (JWA) => {

View file

@ -1,13 +1,23 @@
const { SecretKeyObject } = require('../help/key_objects')
const { createCipheriv, createDecipheriv } = require('crypto')
const { strict: assert } = require('assert')
const { TODO } = require('../errors')
const { JWEInvalid, JWEDecryptionFailed } = require('../errors')
const uint64be = require('../help/uint64be')
const timingSafeEqual = require('../help/timing_safe_equal')
const checkInput = (size, keyLen, data) => {
if (keyLen * 8 !== size) {
throw new JWEInvalid('invalid key length')
}
if (data !== undefined && data.length % 8 !== 0) {
throw new JWEInvalid('invalid data length')
}
}
const A0 = Buffer.alloc(8, 'a6', 'hex')
function xor (a, b) {
const xor = (a, b) => {
const len = Math.max(a.length, b.length)
const result = Buffer.alloc(len)
for (let idx = 0; len > idx; idx++) {
@ -17,7 +27,7 @@ function xor (a, b) {
return result
}
function split (input, size) {
const split = (input, size) => {
const output = []
for (let idx = 0; input.length > idx; idx += size) {
output.push(input.slice(idx, idx + size))
@ -25,8 +35,20 @@ function split (input, size) {
return output
}
const getKeyLen = (keyObject) => {
if (Buffer.isBuffer(keyObject)) {
return keyObject.length
}
if (keyObject instanceof SecretKeyObject) {
return keyObject.symmetricKeySize
}
throw new TypeError('invalid key object')
}
const wrapKey = (size, { keyObject }, payload) => {
// TODO: commonCheck
checkInput(size, getKeyLen(keyObject), payload)
const iv = Buffer.alloc(16)
let R = split(payload, 8)
@ -51,7 +73,7 @@ const wrapKey = (size, { keyObject }, payload) => {
}
const unwrapKey = (size, { keyObject }, payload) => {
// TODO: commonCheck
checkInput(size, getKeyLen(keyObject), payload)
const iv = Buffer.alloc(16)
@ -75,7 +97,7 @@ const unwrapKey = (size, { keyObject }, payload) => {
}
if (!timingSafeEqual(A0, A)) {
throw new TODO('decryption failed')
throw new JWEDecryptionFailed() // TODO: different error
}
return Buffer.concat(R)

View file

@ -3,20 +3,20 @@ const { strict: assert } = require('assert')
const ECKey = require('../../jwk/key/ec')
const derive = require('./derive')
const wrapKey = (kw, derive, key, payload) => {
const wrapKey = (wrap, derive, key, payload) => {
const epk = ECKey.generateSync(key.crv)
const derivedKey = derive(epk, key, payload)
const result = kw({ keyObject: derivedKey }, payload)
const result = wrap({ keyObject: derivedKey }, payload)
result.header = { epk: { kty: 'EC', crv: key.crv, x: epk.x, y: epk.y } }
return result
}
const unwrapKey = (kw, derive, key, payload, { apu, apv, epk }) => {
const unwrapKey = (unwrap, derive, key, payload, { apu, apv, epk }) => {
const derivedKey = derive(key, epk, { apu, apv })
return kw({ keyObject: derivedKey }, payload)
return unwrap({ keyObject: derivedKey }, payload)
}
module.exports = (JWA) => {

View file

@ -1,4 +1,4 @@
const { TODO } = require('../errors')
const { JWKKeySupport, JOSEAlgNotSupported } = require('../errors')
const JWA = {
sign: new Map(),
@ -26,65 +26,40 @@ require('./pbes2')(JWA)
require('./ecdh/kw')(JWA)
require('./ecdh/dir')(JWA)
module.exports = {
sign: (alg, key, payload) => {
if (!JWA.sign.has(alg)) {
throw new TODO(`sign alg ${alg} not implemented`)
}
if (!key.algorithms('sign').has(alg)) {
throw new TODO(`the key does not support ${alg} sign algorithm`)
const check = (key, op, alg) => {
if (JWA[op].has(alg) || (alg === 'dir' && op === 'unwrapKey')) {
if (!key.algorithms(op).has(alg)) {
throw new JWKKeySupport(`the key does not support ${alg} ${op} algorithm`)
}
} else {
throw new JOSEAlgNotSupported(`${op} alg ${alg} not implemented`)
}
}
module.exports = {
check,
sign: (alg, key, payload) => {
check(key, 'sign', alg)
return JWA.sign.get(alg)(key, payload)
},
verify: (alg, key, payload, signature) => {
if (!JWA.verify.has(alg)) {
throw new TODO(`verify alg ${alg} not implemented`)
}
if (!key.algorithms('verify').has(alg)) {
throw new TODO(`the key does not support ${alg} verify algorithm`)
}
check(key, 'verify', alg)
return JWA.verify.get(alg)(key, payload, signature)
},
wrapKey: (alg, key, payload, opts) => {
if (!JWA.wrapKey.has(alg)) {
throw new TODO(`wrapKey alg ${alg} not implemented`)
}
if (!key.algorithms('wrapKey').has(alg)) {
throw new TODO(`the key does not support ${alg} wrapKey algorithm`)
}
check(key, 'wrapKey', alg)
return JWA.wrapKey.get(alg)(key, payload, opts)
},
unwrapKey: (alg, key, payload, opts) => {
if (!JWA.unwrapKey.has(alg)) {
throw new TODO(`unwrapKey alg ${alg} not implemented`)
}
if (!key.algorithms('unwrapKey').has(alg)) {
throw new TODO(`the key does not support ${alg} unwrapKey algorithm`)
}
check(key, 'unwrapKey', alg)
return JWA.unwrapKey.get(alg)(key, payload, opts)
},
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`)
}
check(key, 'encrypt', alg)
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`)
}
check(key, 'decrypt', alg)
return JWA.decrypt.get(alg)(key, ciphertext, opts)
}
}

View file

@ -4,40 +4,37 @@ const { pbkdf2Sync: pbkdf2, randomBytes } = require('crypto')
const base64url = require('../help/base64url')
const SALT_LENGTH = 16
const ITERATIONS = 8192
const NULL_BUFFER = Buffer.alloc(1, 0)
const concatSalt = (alg, ps2) => {
const concatSalt = (alg, p2s) => {
return Buffer.concat([
Buffer.from(alg, 'utf8'),
NULL_BUFFER,
ps2
p2s
])
}
// TODO:
// Note that if password-based encryption is used for multiple
// recipients, it is expected that each recipient use different values
// for the PBES2 parameters "p2s" and "p2c".
const wrapKey = (keylen, sha, concat, kw, { keyObject }, payload) => {
const wrapKey = (keylen, sha, concat, wrap, { keyObject }, payload) => {
// Note that if password-based encryption is used for multiple
// recipients, it is expected that each recipient use different values
// for the PBES2 parameters "p2s" and "p2c".
// here we generate p2c between 2048 and 4096 and random p2s
const p2c = Math.floor((Math.random() * 2049) + 2048)
const p2s = randomBytes(SALT_LENGTH)
const p2c = ITERATIONS
const salt = concat(p2s)
const derivedKey = pbkdf2(keyObject.export(), salt, p2c, keylen, sha)
const result = kw({ keyObject: derivedKey }, payload)
const result = wrap({ keyObject: derivedKey }, payload)
result.header = { p2c, p2s: base64url.encode(p2s) }
return result
}
const unwrapKey = (keylen, sha, concat, kw, { keyObject }, payload, { p2c, p2s }) => {
// TODO: validate p2c, p2s
const unwrapKey = (keylen, sha, concat, unwrap, { keyObject }, payload, { p2c, p2s }) => {
const salt = concat(p2s)
const derivedKey = pbkdf2(keyObject.export(), salt, p2c, keylen, sha)
return kw({ keyObject: derivedKey }, payload)
return unwrap({ keyObject: derivedKey }, payload)
}
module.exports = (JWA) => {

View file

@ -1,9 +1,10 @@
const { createSecretKey } = require('crypto')
const generateCEK = require('./generate_cek')
const base64url = require('../help/base64url')
const validateHeaders = require('./validate_headers')
const { detect: resolveSerialization } = require('./serializers')
const { TODO } = require('../errors')
const { decrypt, unwrapKey } = require('../jwa')
const { JWEDecryptionFailed } = require('../errors')
const { check, decrypt, unwrapKey } = require('../jwa')
const JWK = require('../jwk')
const SINGLE_RECIPIENT = new Set(['compact', 'flattened'])
@ -41,20 +42,13 @@ const headerParams = (prot = {}, unprotected = {}, header = {}) => {
*/
// TODO: option to return everything not just the payload
// TODO: add kid magic
const jweDecrypt = (skipValidateHeaders, serialization, jwe, key) => {
const jweDecrypt = (skipValidateHeaders = false, serialization, jwe, key) => {
if (!serialization) {
serialization = resolveSerialization(jwe)
}
let alg, ciphertext, enc, encryptedKey, iv, opts, prot, tag, unprotected, cek, aad, header
// TODO: To mitigate the attacks described in RFC 3218 [RFC3218], the
// recipient MUST NOT distinguish between format, padding, and length
// errors of encrypted keys. It is strongly recommended, in the event
// of receiving an improperly formatted key, that the recipient
// substitute a randomly generated CEK and proceed to the next step, to
// mitigate timing attacks.
if (SINGLE_RECIPIENT.has(serialization)) {
if (serialization === 'compact') { // compact serialization format
([prot, encryptedKey, iv, ciphertext, tag] = jwe.split('.'))
@ -72,14 +66,30 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key) => {
;({ alg, enc } = opts)
if (alg === 'dir') {
// TODO: validate its a secret
cek = JWK.importKey(key, { alg: enc, use: 'enc' })
} else if (alg === 'ECDH-ES') {
const unwrapped = unwrapKey(alg, key, undefined, opts)
cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
check(key, 'decrypt', enc)
} else {
const unwrapped = unwrapKey(alg, key, base64url.decodeToBuffer(encryptedKey), opts)
cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
check(key, 'unwrapKey', alg)
}
try {
if (alg === 'dir') {
// TODO: validate its a secret
cek = JWK.importKey(key, { alg: enc, use: 'enc' })
} else if (alg === 'ECDH-ES') {
const unwrapped = unwrapKey(alg, key, undefined, opts)
cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
} else {
const unwrapped = unwrapKey(alg, key, base64url.decodeToBuffer(encryptedKey), opts)
cek = JWK.importKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
}
} catch (err) {
// To mitigate the attacks described in RFC 3218, the
// recipient MUST NOT distinguish between format, padding, and length
// errors of encrypted keys. It is strongly recommended, in the event
// of receiving an improperly formatted key, that the recipient
// substitute a randomly generated CEK and proceed to the next step, to
// mitigate timing attacks.
cek = generateCEK(enc)
}
if (aad) {
@ -104,9 +114,8 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key) => {
validateHeaders(jwe.protected, jwe.unprotected, jwe.recipients.map(({ header }) => ({ header })))
const { recipients, ...root } = jwe
// general serialization format
const { recipients, ...root } = jwe
for (const recipient of recipients) {
try {
return jweDecrypt(true, 'flattened', { ...root, ...recipient }, key)
@ -115,7 +124,7 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key) => {
}
}
throw new TODO('decryption failed')
throw new JWEDecryptionFailed()
}
module.exports = jweDecrypt.bind(undefined, false, undefined)

View file

@ -1,5 +1,5 @@
const serializers = require('./serializers')
const { TODO } = require('../errors')
const { JWENoRecipients } = require('../errors')
const Key = require('../jwk/key/base')
const { createSecretKey } = require('crypto')
@ -7,11 +7,11 @@ const generateCEK = require('./generate_cek')
const generateIV = require('../help/generate_iv')
const base64url = require('../help/base64url')
const { wrapKey, encrypt } = require('../jwa')
const { check, wrapKey, encrypt } = require('../jwa')
const OctKey = require('../jwk/key/oct')
const validateHeaders = require('./validate_headers')
function process (encryptObj, recipient) {
const encryptForRecipient = (encryptObj, recipient) => {
const { protectedHeader, unprotectedHeader } = encryptObj
const jweHeader = {
@ -23,6 +23,12 @@ function process (encryptObj, recipient) {
const { alg, enc } = jweHeader
if (alg === 'dir') {
check(key, 'encrypt', enc)
} else {
check(key, 'wrapKey', alg)
}
let wrapped
let generatedHeader
let direct
@ -76,29 +82,23 @@ class Encrypt {
* @public
*/
encrypt (serialization) {
if (typeof serialization !== 'string') {
throw new TypeError('TODO')
}
if (!this.recipients.length) {
throw new TODO('missing recipients')
throw new JWENoRecipients('missing recipients')
}
const serializer = serializers[serialization]
if (!serializer) {
throw new TODO('invalid serialization')
throw new TypeError('serialization must be one of "compact", "flattened", "general"')
}
serializer.validate(this, this.recipients)
const enc = validateHeaders(this.protectedHeader, this.unprotectedHeader, this.recipients)
const final = {}
this.cek = generateCEK(enc)
this.recipients.forEach(process.bind(undefined, this))
this.recipients.forEach(encryptForRecipient.bind(undefined, this))
const iv = generateIV(enc)
final.iv = base64url.encode(iv)
if (this.recipients.length === 1 && this.recipients[0].generatedHeader) {
@ -120,7 +120,14 @@ class Encrypt {
aad = Buffer.from(final.protected || '')
}
const { ciphertext, tag } = encrypt(enc, this.cek, this.cleartext, { iv, aad })
let cleartext
if (!Buffer.isBuffer(this.cleartext) && !(typeof this.cleartext === 'string')) {
cleartext = base64url.JSON.encode(this.cleartext)
} else {
cleartext = this.cleartext
}
const { ciphertext, tag } = encrypt(enc, this.cek, cleartext, { iv, aad })
final.tag = base64url.encode(tag)
final.ciphertext = base64url.encode(ciphertext)

View file

@ -1,6 +1,6 @@
const { TODO } = require('../errors')
function compactSerializer (enc, [recipient]) {
const compactSerializer = (enc, [recipient]) => {
return `${enc.protected}.${recipient.encrypted_key}.${enc.iv}.${enc.ciphertext}.${enc.tag}`
}
compactSerializer.validate = (jwe, recipients) => {
@ -9,7 +9,7 @@ compactSerializer.validate = (jwe, recipients) => {
}
}
function flattenedSerializer (enc, [recipient]) {
const flattenedSerializer = (enc, [recipient]) => {
const { header, encrypted_key: encryptedKey } = recipient
return {
@ -29,7 +29,7 @@ flattenedSerializer.validate = (jwe, { length }) => {
}
}
function generalSerializer (enc, recipients) {
const generalSerializer = (enc, recipients) => {
const result = {
...(enc.protected ? { protected: enc.protected } : undefined),
...(enc.unprotected ? { unprotected: enc.unprotected } : undefined),
@ -57,7 +57,7 @@ function generalSerializer (enc, recipients) {
}
generalSerializer.validate = () => {}
function detect (input) {
const detect = (input) => {
if (typeof input === 'string') {
return 'compact'
}

View file

@ -2,7 +2,7 @@ const RSAKey = require('./key/rsa')
const ECKey = require('./key/ec')
const OctKey = require('./key/oct')
function generate (kty, ...args) {
const generate = (kty, ...args) => {
switch (kty) {
case 'rsa':
case 'RSA':
@ -17,7 +17,7 @@ function generate (kty, ...args) {
}
}
function generateSync (kty, ...args) {
const generateSync = (kty, ...args) => {
switch (kty) {
case 'rsa':
case 'RSA':

View file

@ -1,39 +1,45 @@
const { createPublicKey, createPrivateKey, createSecretKey } = require('crypto')
const Key = require('./key/base')
const RSAKey = require('./key/rsa')
const ECKey = require('./key/ec')
const OctKey = require('./key/oct')
const base64url = require('../help/base64url')
const { TODO } = require('../errors')
const { JWKImportFailed } = require('../errors')
const { PrivateKeyObject, PublicKeyObject, SecretKeyObject } = require('../help/key_objects')
const { jwkToPem } = require('../help/key_utils')
function importKey (key, opts) {
const importable = new Set(['string', 'buffer', 'object'])
const parametersTypes = new Set(['object', 'undefined'])
const mergedParameters = (target = {}, source = {}) => {
return Object.assign({}, { alg: source.alg, use: source.use }, target)
}
const importKey = (key, parameters) => {
let privateKey, publicKey, secret
if (key instanceof Key) {
// TODO: carry over opts from the Key instance
if (key.private) {
privateKey = key.keyObject
} else if (key.public) {
publicKey = key.keyObject
} else { // secret
secret = key.keyObject
}
} else if (key instanceof PrivateKeyObject) {
if (!importable.has(typeof key)) {
throw new TypeError('key argument must be a string, buffer or an object')
}
if (!parametersTypes.has(typeof parameters)) {
throw new TypeError('parameters argument must be a string, buffer or an object')
}
if (key instanceof PrivateKeyObject) {
privateKey = key
} else if (key instanceof PublicKeyObject) {
publicKey = key
} else if (key instanceof SecretKeyObject) {
secret = createSecretKey(key.export())
} else if (key && key.kty === 'oct') { // symmetric key <Object>
// TODO: carry over opts from the JWK
secret = key
} else if (key.kty === 'oct') { // symmetric key <Object>
// TODO: carry over parameters from the JWK
secret = createSecretKey(base64url.decodeToBuffer(key.k))
} else if (key && key.kty) { // assume JWK formatted asymmetric key <Object>
parameters = mergedParameters(parameters, key)
} else if (key.kty) { // assume JWK formatted asymmetric key <Object>
let parsedJWK
try {
// TODO: carry over opts from the JWK
parsedJWK = jwkToPem(key)
} catch (err) {}
if (parsedJWK && key.d) {
@ -41,6 +47,7 @@ function importKey (key, opts) {
} else if (parsedJWK) {
publicKey = createPublicKey(parsedJWK)
}
parameters = mergedParameters(parameters, key)
} else { // <Object> | <string> | <Buffer> passed to crypto.createPrivateKey or crypto.createPublicKey or <Buffer> passed to crypto.createSecretKey
try {
privateKey = createPrivateKey(key)
@ -54,18 +61,18 @@ function importKey (key, opts) {
}
if (!privateKey && !publicKey && !secret) {
throw new TODO('import failed')
throw new JWKImportFailed('import failed')
}
const keyObject = privateKey || publicKey || secret
switch (keyObject.asymmetricKeyType) {
case 'rsa':
return new RSAKey(keyObject, opts)
return new RSAKey(keyObject, parameters)
case 'ec':
return new ECKey(keyObject, opts)
return new ECKey(keyObject, parameters)
default:
return new OctKey(keyObject, opts)
return new OctKey(keyObject, parameters)
}
}

View file

@ -20,7 +20,7 @@ const props = {
const USES = new Set(['sig', 'enc'])
function defineLazyComponents (obj) {
const defineLazyComponents = (obj) => {
Object.defineProperties(obj, props[obj.keyObject.asymmetricKeyType.toUpperCase()][obj.keyObject.type].reduce((acc, component) => {
acc[component] = {
get () {

View file

@ -18,12 +18,12 @@ const ENC_LEN = new Set([
])
const ENC_ALGS = new Set([
'A128GCM',
'A192GCM',
'A256GCM',
'A128CBC-HS256',
'A128GCM',
'A192CBC-HS384',
'A256CBC-HS512'
'A192GCM',
'A256CBC-HS512',
'A256GCM'
])
const WRAP_LEN = new Set([
@ -84,13 +84,17 @@ class OctKey extends Key {
return new Set()
}
const algs = new Set(['dir', 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'])
const algs = new Set()
if (WRAP_LEN.has(this.length)) {
algs.add(`A${this.length}KW`)
algs.add(`A${this.length}GCMKW`)
}
['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'].forEach(Set.prototype.add.bind(algs))
algs.add('dir')
return algs
case undefined:
return new Set([

View file

@ -6,12 +6,12 @@ const generateKeyPair = promisify(async)
const Key = require('./base')
const SIG_ALGS = new Set([
'RS256',
'RS384',
'RS512',
'PS256',
'RS256',
'PS384',
'PS512'
'RS384',
'PS512',
'RS512'
])
const WRAP_ALGS = new Set([

View file

@ -1,6 +1,6 @@
const { TODO } = require('../errors')
function compactSerializer (payload, [recipient]) {
const compactSerializer = (payload, [recipient]) => {
return `${recipient.protected}.${payload}.${recipient.signature}`
}
compactSerializer.validate = (jws, recipients) => {
@ -9,7 +9,7 @@ compactSerializer.validate = (jws, recipients) => {
}
}
function flattenedSerializer (payload, [recipient]) {
const flattenedSerializer = (payload, [recipient]) => {
const { header, signature, protected: prot } = recipient
return {
@ -25,7 +25,7 @@ flattenedSerializer.validate = (jws, { length }) => {
}
}
function generalSerializer (payload, recipients) {
const generalSerializer = (payload, recipients) => {
return {
payload,
signatures: recipients.map(({ header, signature, protected: prot }) => {
@ -39,7 +39,7 @@ function generalSerializer (payload, recipients) {
}
generalSerializer.validate = () => {}
function detect (input) {
const detect = (input) => {
if (typeof input === 'string') {
return 'compact'
}

View file

@ -2,11 +2,11 @@ const base64url = require('../help/base64url')
const serializers = require('./serializers')
const Key = require('../jwk/key/base')
const { JWSInvalidHeader, JWSMissingAlg, TODO } = require('../errors')
const { sign } = require('../jwa')
const { JWSInvalidHeader, JWSNoRecipients } = require('../errors')
const { check, sign } = require('../jwa')
const isDisjoint = require('../help/is_disjoint')
function process (payload, recipient) {
const signForRecipient = (payload, recipient) => {
const { key, protectedHeader, unprotectedHeader } = recipient
const joseHeader = {
@ -18,9 +18,12 @@ function process (payload, recipient) {
throw new JWSInvalidHeader('JWS Protected and JWS Unprotected Header Parameter names must be disjoint')
}
const { alg } = { ...joseHeader.protected, ...joseHeader.unprotected }
const alg = joseHeader.protected.alg || joseHeader.unprotected.alg
check(key, 'verify', alg)
if (!alg) {
throw new JWSMissingAlg('every JOSE header must contain an "alg" parameter')
throw new JWSInvalidHeader('every JOSE header must contain an "alg" parameter')
}
if (Object.keys(joseHeader.unprotected).length) {
@ -52,19 +55,16 @@ class Sign {
* @public
*/
sign (serialization) {
if (typeof serialization !== 'string') {
throw new TypeError('TODO')
}
if (!this.recipients.length) {
throw new TODO('missing recipients')
}
const serializer = serializers[serialization]
if (!serializer) {
throw new TODO('invalid serialization')
throw new TypeError('serialization must be one of "compact", "flattened", "general"')
}
serializer.validate(this, this.recipients)
if (!this.recipients.length) {
throw new JWSNoRecipients('missing recipients')
}
let payload
if (!Buffer.isBuffer(this.payload) && !(typeof this.payload === 'string')) {
payload = base64url.JSON.encode(this.payload)
@ -72,7 +72,7 @@ class Sign {
payload = base64url.encode(this.payload)
}
this.recipients.forEach(process.bind(undefined, payload))
this.recipients.forEach(signForRecipient.bind(undefined, payload))
return serializer(payload, this.recipients)
}

View file

@ -1,7 +1,8 @@
const base64url = require('../help/base64url')
const { detect: resolveSerialization } = require('./serializers')
const { JWSVerificationFailed, JWSMissingAlg } = require('../errors')
const { verify } = require('../jwa')
const { JWSVerificationFailed, JWSInvalidHeader } = require('../errors')
const { check, verify } = require('../jwa')
const isDisjoint = require('../help/is_disjoint')
const SINGLE_RECIPIENT = new Set(['compact', 'flattened'])
@ -29,22 +30,17 @@ const jwsVerify = (serialization, jws, key) => {
({ protected: prot, payload, signature, header } = jws)
}
// TODO: alg may also be unprotected but must it must be provided disjoint
const { alg: protectedAlg } = prot ? base64url.JSON.decode(prot) : {}
const { alg: unprotectedAlg } = header || {}
if (!protectedAlg ^ !unprotectedAlg) {
alg = unprotectedAlg || protectedAlg
} else {
throw new JWSMissingAlg('missing alg')
const parsedProt = prot ? base64url.JSON.decode(prot) : {}
if (!isDisjoint(parsedProt, header)) {
throw new JWSInvalidHeader('JWS Protected and JWS Unprotected Header Parameter names must be disjoint')
}
try {
if (!verify(alg, key, [prot, payload].join('.'), base64url.decodeToBuffer(signature))) {
throw new JWSVerificationFailed('verification failed')
}
} catch (err) {
throw new JWSVerificationFailed('verification failed')
alg = parsedProt.alg || header.alg
check(key, 'verify', alg)
if (!verify(alg, key, [prot, payload].join('.'), base64url.decodeToBuffer(signature))) {
throw new JWSVerificationFailed()
}
return base64url.JSON.decode.try(payload)
@ -52,8 +48,6 @@ const jwsVerify = (serialization, jws, key) => {
// general serialization format
const { signatures, ...root } = jws
// general serialization format
for (const recipient of signatures) {
try {
return jwsVerify('flattened', { ...root, ...recipient }, key)
@ -62,7 +56,7 @@ const jwsVerify = (serialization, jws, key) => {
}
}
throw new JWSVerificationFailed('verification failed')
throw new JWSVerificationFailed()
}
module.exports = jwsVerify.bind(undefined, undefined)

View file

@ -38,7 +38,7 @@
"@commitlint/config-conventional": "^7.5.0",
"ava": "^1.2.1",
"husky": "^1.3.1",
"nyc": "^13.2.0",
"nyc": "^13.3.0",
"standard": "^12.0.1"
},
"engines": {

View file

@ -1,5 +1,5 @@
const base64url = require('../../lib/help/base64url')
const withoutRandom = ({ p2s, epk, iv, tag, ...rest } = {}) => rest
const withoutRandom = ({ p2s, p2c, epk, iv, tag, ...rest } = {}) => rest
const decodeWithoutRandom = (input) => {
return withoutRandom(base64url.JSON.decode(input))
}

View file

@ -11,27 +11,15 @@ const TAG_SEQ = (0x10 | PRIMITIVE_BIT) | (CLASS_UNIVERSAL << 6)
const TAG_INT = 0x02 | (CLASS_UNIVERSAL << 6)
test('.derToJose no signature', t => {
function fn () {
return derToJose()
}
t.throws(fn, { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
t.throws(() => derToJose(), { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
})
test('.derToJose non buffer or base64 signature', t => {
function fn () {
return derToJose(123)
}
t.throws(fn, { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
t.throws(() => derToJose(123), { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
})
test('.derToJose unknown algorithm', t => {
function fn () {
return derToJose(decodeToBuffer('Zm9vLmJhci5iYXo'), 'foobar')
}
t.throws(fn, { instanceOf: Error, message: 'Unknown algorithm "foobar"' })
t.throws(() => derToJose(decodeToBuffer('Zm9vLmJhci5iYXo'), 'foobar'), { instanceOf: Error, message: 'Unknown algorithm "foobar"' })
})
Object.entries({
@ -43,11 +31,9 @@ Object.entries({
const input = Buffer.alloc(10)
input[0] = TAG_SEQ + 1 // not seq
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /expected "seq"/ })
}, { instanceOf: Error, message: /expected "seq"/ })
})
test(`.derToJose seq length exceeding input (${alg})`, t => {
@ -55,11 +41,9 @@ Object.entries({
input[0] = TAG_SEQ
input[1] = 10
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /length/ })
}, { instanceOf: Error, message: /length/ })
})
test(`.derToJose r is not marked as int (${alg})`, t => {
@ -68,11 +52,9 @@ Object.entries({
input[1] = 8
input[2] = TAG_INT + 1 // not int
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /expected "int".+"r"/ })
}, { instanceOf: Error, message: /expected "int".+"r"/ })
})
test(`.derToJose r length exceeds available input (${alg})`, t => {
@ -82,11 +64,9 @@ Object.entries({
input[2] = TAG_INT
input[3] = 5
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /"r".+length/ })
}, { instanceOf: Error, message: /"r".+length/ })
})
test(`.derToJose r length exceeds sensical param length (${alg})`, t => {
@ -96,11 +76,9 @@ Object.entries({
input[2] = TAG_INT
input[3] = len + 2
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /"r".+length.+acceptable/ })
}, { instanceOf: Error, message: /"r".+length.+acceptable/ })
})
test(`.derToJose s is not marked as int (${alg})`, t => {
@ -113,11 +91,9 @@ Object.entries({
input[5] = 0
input[6] = TAG_INT + 1 // not int
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /expected "int".+"s"/ })
}, { instanceOf: Error, message: /expected "int".+"s"/ })
})
test(`.derToJose s length exceeds available input (${alg})`, t => {
@ -131,11 +107,9 @@ Object.entries({
input[6] = TAG_INT
input[7] = 3
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /"s".+length/ })
}, { instanceOf: Error, message: /"s".+length/ })
})
test(`.derToJose s length does not consume available input (${alg})`, t => {
@ -149,11 +123,9 @@ Object.entries({
input[6] = TAG_INT
input[7] = 1
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /"s".+length/ })
}, { instanceOf: Error, message: /"s".+length/ })
})
test(`.derToJose s length exceeds sensical param length (${alg})`, t => {
@ -167,60 +139,34 @@ Object.entries({
input[6] = TAG_INT
input[7] = len + 2
function fn () {
t.throws(() => {
derToJose(input, alg)
}
t.throws(fn, { instanceOf: Error, message: /"s".+length.+acceptable/ })
}, { instanceOf: Error, message: /"s".+length.+acceptable/ })
})
})
test('.joseToDer no signature', t => {
function fn () {
return joseToDer()
}
t.throws(fn, { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
t.throws(() => joseToDer(), { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
})
test('.joseToDer non buffer or base64 signature', t => {
function fn () {
return joseToDer(123)
}
t.throws(fn, { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
t.throws(() => joseToDer(123), { instanceOf: TypeError, message: 'ECDSA signature must be a Buffer' })
})
test('.joseToDer unknown algorithm', t => {
function fn () {
return joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo='), 'foobar')
}
t.throws(fn, { instanceOf: Error, message: /"foobar"/ })
t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo='), 'foobar'), { instanceOf: Error, message: /"foobar"/ })
})
test('.joseToDer incorrect signature length (ES256)', t => {
function fn () {
return joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES256')
}
t.throws(fn, { instanceOf: Error, message: /"64"/ })
t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES256'), { instanceOf: Error, message: /"64"/ })
})
test('.joseToDer incorrect signature length (ES384)', t => {
function fn () {
return joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES384')
}
t.throws(fn, { instanceOf: Error, message: /"96"/ })
t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES384'), { instanceOf: Error, message: /"96"/ })
})
test('.joseToDer incorrect signature length (ES512)', t => {
function fn () {
return joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES512')
}
t.throws(fn, { instanceOf: Error, message: '"ES512" signatures must be "132" bytes, saw "11"' })
t.throws(() => joseToDer(decodeToBuffer('Zm9vLmJhci5iYXo'), 'ES512'), { instanceOf: Error, message: '"ES512" signatures must be "132" bytes, saw "11"' })
})
test('ES256 should jose -> der -> jose', t => {

View file

@ -79,9 +79,3 @@ test('no verify support when `use` is "enc"', t => {
t.is(result.constructor, Set)
t.deepEqual([...result], [])
})
test.todo('algorithms() no arg')
test.todo('algorithms("encrypt")')
test.todo('algorithms("decrypt")')
test.todo('algorithms("wrapKey")')
test.todo('algorithms("unwrapKey")')

View file

@ -33,7 +33,7 @@ test(`RSA key .algorithms invalid operation`, t => {
test('RSA Private key algorithms (no operation)', t => {
const result = key.algorithms()
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'RSA-OAEP', 'RSA1_5'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5'])
})
test('RSA Private key algorithms (no operation, w/ alg)', t => {
@ -46,27 +46,27 @@ test(`RSA key .algorithms invalid operation`, t => {
test(`RSA Private key supports sign alg (no use)`, t => {
const result = key.algorithms('sign')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Private key supports verify alg (no use)`, t => {
const result = key.algorithms('verify')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Private key supports sign alg when \`use\` is "sig")`, t => {
const sigKey = new RSAKey(keyObject, { use: 'sig' })
const result = sigKey.algorithms('sign')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Private key supports verify alg when \`use\` is "sig")`, t => {
const sigKey = new RSAKey(keyObject, { use: 'sig' })
const result = sigKey.algorithms('verify')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Private key supports single sign alg when \`alg\` is set)`, t => {
@ -159,7 +159,7 @@ test(`RSA key .algorithms invalid operation`, t => {
test('RSA EC Public key algorithms (no operation)', t => {
const result = key.algorithms()
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'RSA-OAEP', 'RSA1_5'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512', 'RSA-OAEP', 'RSA1_5'])
})
test('RSA EC Public key algorithms (no operation, w/ alg)', t => {
@ -178,7 +178,7 @@ test(`RSA key .algorithms invalid operation`, t => {
test(`RSA Public key supports verify alg (no use)`, t => {
const result = key.algorithms('verify')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Public key cannot sign even when \`use\` is "sig")`, t => {
@ -192,7 +192,7 @@ test(`RSA key .algorithms invalid operation`, t => {
const sigKey = new RSAKey(keyObject, { use: 'sig' })
const result = sigKey.algorithms('verify')
t.is(result.constructor, Set)
t.deepEqual([...result], ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
t.deepEqual([...result], ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'])
})
test(`RSA Public key cannot sign even when \`alg\` is set)`, t => {