diff --git a/lib/general/decode.js b/lib/general/decode.js index afe96fb..6fcd098 100644 --- a/lib/general/decode.js +++ b/lib/general/decode.js @@ -10,7 +10,7 @@ module.exports = (token, /* second arg is private API */ { parse = true } = {}) const { 0: version, 1: purpose, 2: payload, 3: footer, length } = token.split('.') if (length !== 3 && length !== 4) { - throw new PasetoInvalid('token value is not a PASETO formatted value') + throw new PasetoInvalid('token is not a PASETO formatted value') } if (version !== 'v1' && version !== 'v2' && version !== 'v3' && version !== 'v4') { @@ -37,8 +37,8 @@ module.exports = (token, /* second arg is private API */ { parse = true } = {}) let raw try { raw = decode(payload).subarray(0, -sigLength) - } catch (err) { - throw new PasetoInvalid('token value is not a PASETO formatted value') + } catch { + throw new PasetoInvalid('token is not a PASETO formatted value') } if (!parse) { diff --git a/lib/help/consume.js b/lib/help/consume.js new file mode 100644 index 0000000..15e48c1 --- /dev/null +++ b/lib/help/consume.js @@ -0,0 +1,56 @@ +const assert = require('assert') + +const { PasetoInvalid } = require('../errors') +const assertPayload = require('./assert_payload') +const { decode } = require('./base64url') +const parse = require('./parse_paseto_payload') + +function pre(h, token) { + if (typeof token !== 'string') { + throw new TypeError(`token must be a string, got: ${typeof token}`) + } + + if (token.substr(0, h.length) !== h) { + throw new PasetoInvalid(`token is not a ${h.substr(0, h.length - 1)} PASETO`) + } + + let { 0: raw, 1: f = '', length } = token.substr(h.length).split('.') + + try { + assert(length <= 2) + raw = decode(raw) + f = decode(f) + } catch { + throw new PasetoInvalid('token is not a PASETO formatted value') + } + + return { raw, f } +} + +function post(version, buffer, options, complete, m, f, purpose) { + if (buffer) { + if (Object.keys(options).length !== 0) { + throw new TypeError('options cannot contain claims when options.buffer is true') + } + if (complete) { + return { payload: m, footer: f.length ? f : undefined, version, purpose } + } + + return m + } + + const payload = parse(m) + + assertPayload(options, payload) + + if (complete) { + return { payload, footer: f.length ? f : undefined, version, purpose } + } + + return payload +} + +module.exports = { + post, + pre, +} diff --git a/lib/help/parse_paseto_payload.js b/lib/help/parse_paseto_payload.js index 95df891..2f7197f 100644 --- a/lib/help/parse_paseto_payload.js +++ b/lib/help/parse_paseto_payload.js @@ -8,7 +8,7 @@ module.exports = (payload) => { const parsed = JSON.parse(payload) assert(isObject(parsed)) return parsed - } catch (err) { + } catch { throw new PasetoInvalid('All PASETO payloads MUST be a JSON object') } } diff --git a/lib/help/verify.js b/lib/help/verify.js index bcaca5f..a7092aa 100644 --- a/lib/help/verify.js +++ b/lib/help/verify.js @@ -1,35 +1,14 @@ -const { PasetoInvalid, PasetoVerificationFailed } = require('../errors') +const { PasetoVerificationFailed } = require('../errors') -const { decode } = require('./base64url') const { verify } = require('./crypto_worker') const pae = require('./pae') +const { pre } = require('./consume') module.exports = async function verifyPaseto(h, token, alg, sigLength, key, i, eo) { - if (typeof token !== 'string') { - throw new TypeError('token must be a string') - } - - if (token.substr(0, h.length) !== h) { - throw new PasetoInvalid(`token is not a ${h.slice(0, -1)} token`) - } - - const { 0: b64ms, 1: b64f, length } = token.substr(h.length).split('.') - if (length !== 1 && length !== 2) { - throw new PasetoInvalid('token value is not a PASETO formatted value') - } - - let f - let ms - - try { - ms = decode(b64ms) - f = decode(b64f || '') - } catch (err) { - throw new PasetoInvalid('token value is not a PASETO formatted value') - } + const { raw, f } = pre(h, token) - const m = ms.subarray(0, -sigLength) - const s = ms.subarray(-sigLength) + const m = raw.subarray(0, -sigLength) + const s = raw.subarray(-sigLength) const m2 = pae(eo, h, m, f, i) if (!(await verify(alg, m2, key, s))) { diff --git a/lib/v1/decrypt.js b/lib/v1/decrypt.js index 6676576..ed70652 100644 --- a/lib/v1/decrypt.js +++ b/lib/v1/decrypt.js @@ -1,9 +1,6 @@ -const { decode } = require('../help/base64url') const { 'v1.local-decrypt': decrypt } = require('../help/crypto_worker') -const { PasetoInvalid } = require('../errors') -const assertPayload = require('../help/assert_payload') const checkKey = require('../help/symmetric_key_check').bind(undefined, 'v1.local') -const parse = require('../help/parse_paseto_payload') +const { pre, post } = require('../help/consume') const h = 'v1.local.' @@ -12,45 +9,9 @@ module.exports = async function v1Decrypt( key, { complete = false, buffer = false, ...options } = {}, ) { - if (typeof token !== 'string') { - throw new TypeError(`token must be a string, got: ${typeof token}`) - } - + const { raw, f } = pre(h, token) key = checkKey(key) - - if (token.substr(0, h.length) !== h) { - throw new PasetoInvalid('token is not a v1.local PASETO') - } - - const { 0: b64, 1: b64f = '', length } = token.substr(h.length).split('.') - if (length > 2) { - throw new PasetoInvalid('token value is not a PASETO formatted value') - } - - const f = decode(b64f) - const raw = decode(b64) const k = key.export() - const m = await decrypt(raw, f, k) - - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer: f.length ? f : undefined, version: 'v1', purpose: 'local' } - } - - return m - } - - const payload = parse(m) - - assertPayload(options, payload) - - if (complete) { - return { payload, footer: f.length ? f : undefined, version: 'v1', purpose: 'local' } - } - - return payload + return post('v1', buffer, options, complete, m, f, 'local') } diff --git a/lib/v1/verify.js b/lib/v1/verify.js index 9bba943..11787ff 100644 --- a/lib/v1/verify.js +++ b/lib/v1/verify.js @@ -3,10 +3,9 @@ const { createPublicKey, } = require('crypto') -const assertPayload = require('../help/assert_payload') -const parse = require('../help/parse_paseto_payload') const verify = require('../help/verify') const isKeyObject = require('../help/is_key_object') +const { post } = require('../help/consume') function checkKey(key) { if (!isKeyObject(key) || key.type === 'private') { @@ -45,23 +44,5 @@ module.exports = async function v1Verify( saltLength, }) - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer, version: 'v1', purpose: 'public' } - } - - return m - } - - const payload = parse(m) - assertPayload(options, payload) - - if (complete) { - return { payload, footer, version: 'v1', purpose: 'public' } - } - - return payload + return post('v1', buffer, options, complete, m, footer, 'public') } diff --git a/lib/v2/key.js b/lib/v2/key.js index 73b9979..a4dd2a2 100644 --- a/lib/v2/key.js +++ b/lib/v2/key.js @@ -6,6 +6,54 @@ const isKeyObject = require('../help/is_key_object') const generateKeyPair = promisify(crypto.generateKeyPair) +function _checkPrivateKey(v, key) { + if (Buffer.isBuffer(key)) { + try { + key = bytesToKeyObject(key) + } catch {} + } + + if (!isKeyObject(key)) { + try { + key = crypto.createPrivateKey(key) + } catch {} + } + + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') + } + + if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') { + throw new TypeError(`${v}.public signing key must be a private ed25519 key`) + } + + return key +} + +function _checkPublicKey(v, key) { + if (Buffer.isBuffer(key)) { + try { + key = bytesToKeyObject(key) + } catch {} + } + + if (!isKeyObject(key) || key.type === 'private') { + try { + key = crypto.createPublicKey(key) + } catch {} + } + + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') + } + + if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') { + throw new TypeError(`${v}.public verify key must be a public ed25519 key`) + } + + return key +} + async function _generateKey(v, purpose) { switch (purpose) { case 'public': { @@ -78,6 +126,8 @@ function keyObjectToBytes(...args) { } module.exports = { + _checkPrivateKey, + _checkPublicKey, _generateKey, _keyObjectToBytes, bytesToKeyObject, diff --git a/lib/v2/sign.js b/lib/v2/sign.js index e2cdab8..d07e6a8 100644 --- a/lib/v2/sign.js +++ b/lib/v2/sign.js @@ -1,34 +1,9 @@ -const { createPrivateKey } = require('crypto') - const checkFooter = require('../help/check_footer') const checkPayload = require('../help/check_payload') const sign = require('../help/sign') -const isKeyObject = require('../help/is_key_object') -const { bytesToKeyObject } = require('./key') - -function checkKey(key) { - if (Buffer.isBuffer(key)) { - try { - key = bytesToKeyObject(key) - } catch {} - } - - if (!isKeyObject(key)) { - try { - key = createPrivateKey(key) - } catch {} - } +const { _checkPrivateKey } = require('./key') - if (!isKeyObject(key)) { - throw new TypeError('invalid key provided') - } - - if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') { - throw new TypeError('v2.public signing key must be a private ed25519 key') - } - - return key -} +const checkKey = _checkPrivateKey.bind(undefined, 'v2') module.exports = async function v2Sign(payload, key, { footer, ...options } = {}) { const m = checkPayload(payload, options) diff --git a/lib/v2/verify.js b/lib/v2/verify.js index 91757fd..a541558 100644 --- a/lib/v2/verify.js +++ b/lib/v2/verify.js @@ -1,34 +1,8 @@ -const { createPublicKey } = require('crypto') - -const assertPayload = require('../help/assert_payload') -const parse = require('../help/parse_paseto_payload') const verify = require('../help/verify') -const isKeyObject = require('../help/is_key_object') -const { bytesToKeyObject } = require('./key') - -function checkKey(key) { - if (Buffer.isBuffer(key)) { - try { - key = bytesToKeyObject(key) - } catch {} - } - - if (!isKeyObject(key) || key.type === 'private') { - try { - key = createPublicKey(key) - } catch {} - } - - if (!isKeyObject(key)) { - throw new TypeError('invalid key provided') - } - - if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') { - throw new TypeError('v2.public verify key must be a public ed25519 key') - } +const { _checkPublicKey } = require('./key') +const { post } = require('../help/consume') - return key -} +const checkKey = _checkPublicKey.bind(undefined, 'v2') module.exports = async function v2Verify( token, @@ -39,23 +13,5 @@ module.exports = async function v2Verify( const { m, footer } = await verify('v2.public.', token, undefined, 64, key) - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer, version: 'v2', purpose: 'public' } - } - - return m - } - - const payload = parse(m) - assertPayload(options, payload) - - if (complete) { - return { payload, footer, version: 'v2', purpose: 'public' } - } - - return payload + return post('v2', buffer, options, complete, m, footer, 'public') } diff --git a/lib/v3/decrypt.js b/lib/v3/decrypt.js index bc66255..0e1b381 100644 --- a/lib/v3/decrypt.js +++ b/lib/v3/decrypt.js @@ -1,10 +1,7 @@ -const { decode } = require('../help/base64url') const { 'v3.local-decrypt': decrypt } = require('../help/crypto_worker') -const { PasetoInvalid } = require('../errors') -const assertPayload = require('../help/assert_payload') const checkKey = require('../help/symmetric_key_check').bind(undefined, 'v3.local') const checkAssertion = require('../help/check_assertion') -const parse = require('../help/parse_paseto_payload') +const { pre, post } = require('../help/consume') const h = 'v3.local.' @@ -13,46 +10,10 @@ module.exports = async function v3Decrypt( key, { complete = false, buffer = false, assertion, ...options } = {}, ) { - if (typeof token !== 'string') { - throw new TypeError(`token must be a string, got: ${typeof token}`) - } - + const { raw, f } = pre(h, token) key = checkKey(key) const i = checkAssertion(assertion) - - if (token.substr(0, h.length) !== h) { - throw new PasetoInvalid('token is not a v3.local PASETO') - } - - const { 0: b64, 1: b64f = '', length } = token.substr(h.length).split('.') - if (length > 2) { - throw new PasetoInvalid('token value is not a PASETO formatted value') - } - - const f = decode(b64f) - const raw = decode(b64) const k = key.export() - const m = await decrypt(raw, f, k, i) - - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer: f.length ? f : undefined, version: 'v3', purpose: 'local' } - } - - return m - } - - const payload = parse(m) - - assertPayload(options, payload) - - if (complete) { - return { payload, footer: f.length ? f : undefined, version: 'v3', purpose: 'local' } - } - - return payload + return post('v3', buffer, options, complete, m, f, 'local') } diff --git a/lib/v3/verify.js b/lib/v3/verify.js index 25df944..32f0f0f 100644 --- a/lib/v3/verify.js +++ b/lib/v3/verify.js @@ -1,12 +1,11 @@ const { createPublicKey } = require('crypto') -const assertPayload = require('../help/assert_payload') -const parse = require('../help/parse_paseto_payload') const checkAssertion = require('../help/check_assertion') const verify = require('../help/verify') const isKeyObject = require('../help/is_key_object') const { bytesToKeyObject } = require('./key') const compressPk = require('../help/compress_pk') +const { post } = require('../help/consume') function checkKey(key) { if (Buffer.isBuffer(key)) { @@ -51,26 +50,8 @@ module.exports = async function v3Verify( 96, { key, dsaEncoding: 'ieee-p1363' }, i, - compressPk(key) + compressPk(key), ) - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer, version: 'v3', purpose: 'public' } - } - - return m - } - - const payload = parse(m) - assertPayload(options, payload) - - if (complete) { - return { payload, footer, version: 'v3', purpose: 'public' } - } - - return payload + return post('v3', buffer, options, complete, m, footer, 'public') } diff --git a/lib/v4/key.js b/lib/v4/key.js index 31a8e0e..e6d65e0 100644 --- a/lib/v4/key.js +++ b/lib/v4/key.js @@ -1,4 +1,10 @@ -const { _generateKey, _keyObjectToBytes, bytesToKeyObject } = require('../v2/key') +const { + _checkPrivateKey, + _checkPublicKey, + _generateKey, + _keyObjectToBytes, + bytesToKeyObject, +} = require('../v2/key') async function generateKey(...args) { return _generateKey('v4', ...args) @@ -9,7 +15,9 @@ function keyObjectToBytes(...args) { } module.exports = { - generateKey, + _checkPrivateKey, + _checkPublicKey, bytesToKeyObject, + generateKey, keyObjectToBytes, } diff --git a/lib/v4/sign.js b/lib/v4/sign.js index dfd60cc..f86525c 100644 --- a/lib/v4/sign.js +++ b/lib/v4/sign.js @@ -1,35 +1,10 @@ -const { createPrivateKey } = require('crypto') - const checkFooter = require('../help/check_footer') const checkPayload = require('../help/check_payload') const checkAssertion = require('../help/check_assertion') const sign = require('../help/sign') -const isKeyObject = require('../help/is_key_object') -const { bytesToKeyObject } = require('./key') - -function checkKey(key) { - if (Buffer.isBuffer(key)) { - try { - key = bytesToKeyObject(key) - } catch {} - } - - if (!isKeyObject(key)) { - try { - key = createPrivateKey(key) - } catch {} - } +const { _checkPrivateKey } = require('./key') - if (!isKeyObject(key)) { - throw new TypeError('invalid key provided') - } - - if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') { - throw new TypeError('v4.public signing key must be a private ed25519 key') - } - - return key -} +const checkKey = _checkPrivateKey.bind(undefined, 'v4') module.exports = async function v4Sign(payload, key, { footer, assertion, ...options } = {}) { const m = checkPayload(payload, options) diff --git a/lib/v4/verify.js b/lib/v4/verify.js index 618a9fe..5fe38f5 100644 --- a/lib/v4/verify.js +++ b/lib/v4/verify.js @@ -1,35 +1,9 @@ -const { createPublicKey } = require('crypto') - -const assertPayload = require('../help/assert_payload') -const parse = require('../help/parse_paseto_payload') const checkAssertion = require('../help/check_assertion') const verify = require('../help/verify') -const isKeyObject = require('../help/is_key_object') -const { bytesToKeyObject } = require('./key') - -function checkKey(key) { - if (Buffer.isBuffer(key)) { - try { - key = bytesToKeyObject(key) - } catch {} - } - - if (!isKeyObject(key) || key.type === 'private') { - try { - key = createPublicKey(key) - } catch {} - } - - if (!isKeyObject(key)) { - throw new TypeError('invalid key provided') - } - - if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') { - throw new TypeError('v4.public verify key must be a public ed25519 key') - } +const { _checkPublicKey } = require('./key') +const { post } = require('../help/consume') - return key -} +const checkKey = _checkPublicKey.bind(undefined, 'v4') module.exports = async function v4Verify( token, @@ -41,23 +15,5 @@ module.exports = async function v4Verify( const { m, footer } = await verify('v4.public.', token, undefined, 64, key, i) - if (buffer) { - if (Object.keys(options).length !== 0) { - throw new TypeError('options cannot contain claims when options.buffer is true') - } - if (complete) { - return { payload: m, footer, version: 'v4', purpose: 'public' } - } - - return m - } - - const payload = parse(m) - assertPayload(options, payload) - - if (complete) { - return { payload, footer, version: 'v4', purpose: 'public' } - } - - return payload + return post('v4', buffer, options, complete, m, footer, 'public') } diff --git a/test/generic/decode.test.js b/test/generic/decode.test.js index ae678e9..b478483 100644 --- a/test/generic/decode.test.js +++ b/test/generic/decode.test.js @@ -10,12 +10,12 @@ test('decode input must have 3 or 4 parts', (t) => { t.throws(() => decode('.'), { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', - message: 'token value is not a PASETO formatted value', + message: 'token is not a PASETO formatted value', }) t.throws(() => decode('....'), { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', - message: 'token value is not a PASETO formatted value', + message: 'token is not a PASETO formatted value', }) }) diff --git a/test/local/v1.test.js b/test/local/v1.test.js index 748e8f7..9b90b4b 100644 --- a/test/local/v1.test.js +++ b/test/local/v1.test.js @@ -60,7 +60,7 @@ test('invalid paseto', async (t) => { const token = `${await encrypt({}, key, { footer: 'foo' })}.foo` await t.throwsAsync(() => decrypt(token, key), { - message: 'token value is not a PASETO formatted value', + message: 'token is not a PASETO formatted value', instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', }) diff --git a/test/local/v3.test.js b/test/local/v3.test.js index dc79d66..8a5b94e 100644 --- a/test/local/v3.test.js +++ b/test/local/v3.test.js @@ -60,7 +60,7 @@ test('invalid paseto', async (t) => { const token = `${await encrypt({}, key, { footer: 'foo' })}.foo` await t.throwsAsync(() => decrypt(token, key), { - message: 'token value is not a PASETO formatted value', + message: 'token is not a PASETO formatted value', instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', }) diff --git a/test/public.test.js b/test/public.test.js index 39223b1..a290d66 100644 --- a/test/public.test.js +++ b/test/public.test.js @@ -105,7 +105,7 @@ test('token must be a string', async (t) => { const k = await V2.generateKey('public') return t.throwsAsync(V2.verify(1, k), { instanceOf: TypeError, - message: 'token must be a string', + message: 'token must be a string, got: number', }) }) @@ -114,7 +114,7 @@ test('token must be a a valid paseto', async (t) => { return t.throwsAsync(V2.verify('v2.public...', k), { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', - message: 'token value is not a PASETO formatted value', + message: 'token is not a PASETO formatted value', }) }) diff --git a/test/vectors/v1.json b/test/vectors/v1.json index 990ea84..075fc54 100644 --- a/test/vectors/v1.json +++ b/test/vectors/v1.json @@ -146,4 +146,4 @@ "implicit-assertion": "discarded-anyway" } ] -} \ No newline at end of file +} diff --git a/test/vectors/v2.json b/test/vectors/v2.json index 18399f4..fcc4c31 100644 --- a/test/vectors/v2.json +++ b/test/vectors/v2.json @@ -152,4 +152,4 @@ "implicit-assertion": "discarded-anyway" } ] -} \ No newline at end of file +} diff --git a/test/vectors/v3.json b/test/vectors/v3.json index 6c407ff..d15671d 100644 --- a/test/vectors/v3.json +++ b/test/vectors/v3.json @@ -152,4 +152,4 @@ "implicit-assertion": "{\"test-vector\":\"3-S-3\"}" } ] -} \ No newline at end of file +} diff --git a/test/vectors/v4.json b/test/vectors/v4.json index 45fcc08..45b81e1 100644 --- a/test/vectors/v4.json +++ b/test/vectors/v4.json @@ -152,4 +152,4 @@ "implicit-assertion": "{\"test-vector\":\"4-S-3\"}" } ] -} \ No newline at end of file +}