From ea32ad8fed350fc5483d7c20cd4c23e080b1d628 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 19 Jul 2021 14:13:43 +0200 Subject: [PATCH] feat: add v3.local, v3.public, and v4.public Additionally allows raw byte key sequences to be passed in V2, V3, and V4's `.sign()` and `.verify()` methods as the "key" argument. Also adds utility functions to convert raw byte sequences to KeyObject. BREAKING CHANGE: Node.js runtime version v16.0.0 or greater is now required --- .github/workflows/test.yml | 21 +- .prettierrc.json | 6 + README.md | 89 ++-- docs/README.md | 531 +++++++++++++++++++++++- lib/errors.js | 4 +- lib/general/decode.js | 23 +- lib/help/apply_options.js | 7 +- lib/help/assert_payload.js | 17 +- lib/help/base64url.js | 8 +- lib/help/check_assertion.js | 15 + lib/help/check_footer.js | 2 +- lib/help/check_payload.js | 2 +- lib/help/compress_pk.js | 6 + lib/help/crypto_worker.js | 275 +++++-------- lib/help/is_key_object.js | 8 + lib/help/is_object.js | 2 +- lib/help/le64.js | 4 +- lib/help/ms.js | 3 +- lib/help/pack.js | 4 +- lib/help/pae.js | 1 + lib/help/random_bytes.js | 9 - lib/help/sign.js | 14 +- lib/help/symmetric_key_check.js | 16 +- lib/help/verify.js | 20 +- lib/index.js | 4 +- lib/v1/decrypt.js | 13 +- lib/v1/encrypt.js | 13 +- lib/v1/key.js | 11 +- lib/v1/sign.js | 33 +- lib/v1/verify.js | 41 +- lib/v2/index.js | 4 +- lib/v2/key.js | 73 +++- lib/v2/sign.js | 29 +- lib/v2/verify.js | 31 +- lib/v3/decrypt.js | 58 +++ lib/v3/encrypt.js | 14 + lib/v3/index.js | 7 + lib/v3/key.js | 94 +++++ lib/v3/sign.js | 44 ++ lib/v3/verify.js | 74 ++++ lib/v4/index.js | 5 + lib/v4/key.js | 15 + lib/v4/sign.js | 40 ++ lib/v4/verify.js | 63 +++ package.json | 26 +- test/apply_options.test.js | 51 ++- test/assert_payload.test.js | 267 ++++++------ test/check_assertion.test.js | 10 + test/check_footer.test.js | 10 +- test/generic/decode.test.js | 69 ++-- test/generic/generate_key.test.js | 58 ++- test/local/v1.test.js | 57 ++- test/local/v3.test.js | 129 ++++++ test/ms.test.js | 21 +- test/parse_paseto_payload.test.js | 22 +- test/public.test.js | 241 ++++++----- test/rfc/v1.local.test.js | 189 --------- test/rfc/v1.public.test.js | 88 ---- test/rfc/v2.public.test.js | 66 --- test/smoke.test.js | 107 +++++ test/tse.test.js | 2 +- test/vectors/v1.json | 149 +++++++ test/vectors/v1.test.js | 58 +++ test/vectors/v2.json | 155 +++++++ test/vectors/v2.test.js | 71 ++++ test/vectors/v3.json | 155 +++++++ test/vectors/v3.test.js | 106 +++++ test/vectors/v4.json | 155 +++++++ test/vectors/v4.test.js | 72 ++++ types/index.d.ts | 646 ++++++++++-------------------- types/paseto-tests.ts | 11 +- types/tsconfig.json | 13 +- 72 files changed, 3231 insertions(+), 1526 deletions(-) create mode 100644 .prettierrc.json create mode 100644 lib/help/check_assertion.js create mode 100644 lib/help/compress_pk.js create mode 100644 lib/help/is_key_object.js delete mode 100644 lib/help/random_bytes.js create mode 100644 lib/v3/decrypt.js create mode 100644 lib/v3/encrypt.js create mode 100644 lib/v3/index.js create mode 100644 lib/v3/key.js create mode 100644 lib/v3/sign.js create mode 100644 lib/v3/verify.js create mode 100644 lib/v4/index.js create mode 100644 lib/v4/key.js create mode 100644 lib/v4/sign.js create mode 100644 lib/v4/verify.js create mode 100644 test/check_assertion.test.js create mode 100644 test/local/v3.test.js delete mode 100644 test/rfc/v1.local.test.js delete mode 100644 test/rfc/v1.public.test.js delete mode 100644 test/rfc/v2.public.test.js create mode 100644 test/smoke.test.js create mode 100644 test/vectors/v1.json create mode 100644 test/vectors/v1.test.js create mode 100644 test/vectors/v2.json create mode 100644 test/vectors/v2.test.js create mode 100644 test/vectors/v3.json create mode 100644 test/vectors/v3.test.js create mode 100644 test/vectors/v4.json create mode 100644 test/vectors/v4.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b13aea..abd089b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,29 +13,14 @@ on: - cron: 0 12 * * 1-5 jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: actions/setup-node@v1 - with: - node-version: 12 - - run: npx panva/npm-install-retry - - run: npm run lint - - run: npm run lint-ts - test: runs-on: ${{ matrix.os }} strategy: matrix: node-version: - - 12.19.0 - - 12 - - 14.15.0 - - 14 - - 15.0.1 - - 15 - - '>=15' + - 16.0.0 + - 16 + - '>=16' os: - ubuntu-latest - windows-latest diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..3c9a291 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "semi": false +} diff --git a/README.md b/README.md index d596222..00d6953 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,12 @@ > [PASETO](https://paseto.io): Platform-Agnostic SEcurity TOkens for Node.js no dependencies. -## Implemented specs & features +## Implemented Protocol Versions -All crypto operations are using their async node's crypto API, where such API is not available the -operation is pushed to a [Worker Thread](https://nodejs.org/api/worker_threads.html) so that your -main thread's I/O is not blocked. - -
- -| | v1.local | v1.public | v2.local | v2.public | +| | v1 | v2 | v3 | v4 | | -- | -- | -- | -- | -- | -| supported? | ✓ | ✓ | ✕ | ✓ | +| local | ✓ | ✕ | ✓ | ✕ | +| public | ✓ | ✓ | ✓ | ✓ | ## Support @@ -21,6 +16,8 @@ If you or your business use paseto, please consider becoming a [sponsor][support ## Documentation - [API Documentation][documentation] + - [PASETO Protocol Version v4][documentation-v4] + - [PASETO Protocol Version v3][documentation-v3] - [PASETO Protocol Version v2][documentation-v2] - [PASETO Protocol Version v1][documentation-v1] @@ -43,7 +40,13 @@ const { decode } = paseto const { V1 } = paseto // { sign, verify, encrypt, decrypt, generateKey } // PASETO Protocol Version v2 specific API -const { V2 } = paseto // { sign, verify, generateKey } +const { V2 } = paseto // { sign, verify, generateKey, bytesToKeyObject, keyObjectToBytes } + +// PASETO Protocol Version v3 specific API +const { V3 } = paseto // { sign, verify, encrypt, decrypt, generateKey, bytesToKeyObject, keyObjectToBytes } + +// PASETO Protocol Version v4 specific API +const { V4 } = paseto // { sign, verify, generateKey, bytesToKeyObject, keyObjectToBytes } // errors utilized by paseto const { errors } = paseto @@ -75,57 +78,15 @@ const { V2: { verify } } = paseto })() ``` -#### Keys - -Node's [KeyObject](https://nodejs.org/api/crypto.html#crypto_class_keyobject) is ultimately what the -library works with, depending on the operation, if the key parameter is not already a KeyObject -instance the corresponding `create` function will be called with the input - -- [`crypto.createSecretKey()`](https://nodejs.org/api/crypto.html#crypto_crypto_createsecretkey_key) - for local encrypt/decrypt operations -- [`crypto.createPublicKey()`](https://nodejs.org/api/crypto.html#crypto_crypto_createpublickey_key) - for public verify operations -- [`crypto.createPrivateKey()`](https://nodejs.org/api/crypto.html#crypto_crypto_createprivatekey_key) - for public sign operations - -You can also generate keys valid for the given operation directly through paseto - -```js -const crypto = require('crypto') -const { V1, V2 } = paseto +## FAQ -(async () => { - { - const key = await V1.generateKey('local') - console.log(key instanceof crypto.KeyObject) - // true - console.log(key.type === 'secret') - // true - console.log(key.symmetricKeySize === 32) - // true - } - { - const key = await V1.generateKey('public') - console.log(key instanceof crypto.KeyObject) - // true - console.log(key.type === 'private') - // true - console.log(key.asymmetricKeyType === 'rsa') - // true - } - { - const key = await V2.generateKey('public') - console.log(key instanceof crypto.KeyObject) - // true - console.log(key.type === 'private') - // true - console.log(key.asymmetricKeyType === 'ed25519') - // true - } -})() -``` +#### Supported Versions -## FAQ +| Version | Security Fixes 🔑 | Other Bug Fixes 🐞 | New Features ⭐ | +| ------- | --------- | -------- | -------- | +| [3.x.x](https://github.com/panva/paseto) | ✅ | ✅ | ✅ | +| [2.x.x](https://github.com/panva/paseto/tree/v2.x) | ✅ | ✅ until 2022-04-30 | ❌ | +| [1.x.x](https://github.com/panva/paseto/tree/v1.x) | ✅ | ❌ | ❌ | #### Semver? @@ -136,10 +97,12 @@ private API and is subject to change between any versions. #### How do I use it outside of Node.js -It is **only built for Node.js** environment versions ^12.19.0 || >=14.15.0 +It is **only built for Node.js** environment versions >=16.0.0 -[documentation]: https://github.com/panva/paseto/blob/master/docs/README.md -[documentation-v2]: https://github.com/panva/paseto/blob/master/docs/README.md#v2-paseto-protocol-version-v2 -[documentation-v1]: https://github.com/panva/paseto/blob/master/docs/README.md#v1-paseto-protocol-version-v1 +[documentation]: https://github.com/panva/paseto/blob/main/docs/README.md +[documentation-v4]: https://github.com/panva/paseto/blob/main/docs/README.md#v4-paseto-protocol-version-v4 +[documentation-v3]: https://github.com/panva/paseto/blob/main/docs/README.md#v3-paseto-protocol-version-v3 +[documentation-v2]: https://github.com/panva/paseto/blob/main/docs/README.md#v2-paseto-protocol-version-v2 +[documentation-v1]: https://github.com/panva/paseto/blob/main/docs/README.md#v1-paseto-protocol-version-v1 [support-sponsor]: https://github.com/sponsors/panva diff --git a/docs/README.md b/docs/README.md index 1dcf226..1c691fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ **Table of Contents** +- [V4 (PASETO Protocol Version v4)](#v4-paseto-protocol-version-v4) +- [V3 (PASETO Protocol Version v3)](#v3-paseto-protocol-version-v3) - [V2 (PASETO Protocol Version v2)](#v2-paseto-protocol-version-v2) - [V1 (PASETO Protocol Version v1)](#v1-paseto-protocol-version-v1) - [decode](#decode) @@ -17,12 +19,496 @@ If you or your business use paseto, please consider becoming a [sponsor][support --- +## V4 (PASETO Protocol Version v4) + + +- [V4.sign(payload, key[, options])](#v4signpayload-key-options) +- [V4.verify(token, key[, options])](#v4verifytoken-key-options) +- [V4.generateKey(purpose)](#v4generatekeypurpose) +- [V4.bytesToKeyObject(bytes)](#v4bytestokeyobjectbytes) +- [V4.keyObjectToBytes(keyObject)](#v4keyobjecttobyteskeyobject) + + + +```js +const { V4 } = require('paseto') +// { +// sign: [AsyncFunction: v4Sign], +// verify: [AsyncFunction: v4Verify], +// generateKey: [AsyncFunction: generateKey], +// bytesToKeyObject: [Function: bytesToKeyObject], +// keyObjectToBytes: [Function: keyObjectToBytes] +// } +``` + +--- +#### V4.sign(payload, key[, options]) + +Serializes and signs the payload as a PASETO using the provided private key. + +- `payload`: `` PASETO Payload claims +- `key`: `` The key to sign with. Alternatively any input that works for `crypto.createPrivateKey()` + or `V4.bytesToKeyObject()`. +- `options`: `` + - `audience`: `` PASETO Audience, "aud" claim value, if provided it will replace + "aud" found in the payload + - `expiresIn`: `` PASETO Expiration Time, "exp" claim value, specified as string which is + added to the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it + will replace Expiration Time found in the payload + - `footer`: `` | `` | `` PASETO footer + - `iat`: `` When true it pushes the "iat" to the PASETO payload. **Default:** 'true' + - `issuer`: `` PASETO Issuer, "iss" claim value, if provided it will replace "iss" found in + the payload + - `jti`: `` Token ID, "jti" claim value, if provided it will replace "jti" found in the + payload + - `kid`: `` Key ID, "kid" claim value, if provided it will replace "kid" found in the + payload + - `notBefore`: `` PASETO Not Before, "nbf" claim value, specified as string which is added to + the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it will + replace Not Before found in the payload + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` PASETO subject, "sub" claim value, if provided it will replace "sub" found in + the payload +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createPrivateKey } = require('crypto') +const { V4 } = require('paseto') + +const key = createPrivateKey(privateKey) + +const payload = { + 'urn:example:claim': 'foo' +} + +(async () => { + const token = await V4.sign(payload, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + expiresIn: '2 hours' + }) + // v4.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMjEtMDctMTlUMTA6MTM6MjIuOTM3WiIsImV4cCI6IjIwMjEtMDctMTlUMTI6MTM6MjIuOTM3WiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifYZrfK1eH8d7Scp218_DPEX8H3ElIfzWWMu9UQVZYjyV585BEBV0wTRk-vZgtXq0y5z0euOE48a2Yd6TLKfA5Qs +})() +``` +
+ +--- + +#### V4.verify(token, key[, options]) + +Verifies the claims and signature of a PASETO + +- `token`: `` PASETO to verify +- `key`: `` The key to verify with. Alternatively any input that works for `crypto.createPublicKey()` + or `V4.bytesToKeyObject()`. +- `options`: `` + - `audience`: `` Expected audience value. An exact match must be found in the payload. + - `clockTolerance`: `` Clock Tolerance for comparing timestamps, provided as timespan + string e.g. `120s`, `2 minutes`, etc. **Default:** no clock tolerance + - `complete`: `` When false only the parsed payload is returned, otherwise an object with + a parsed payload and footer (as a Buffer) will be returned. + **Default:** 'false' + - `ignoreExp`: `` When true will not be validating the "exp" claim value to be in the + future from now. **Default:** 'false' + - `ignoreIat`: `` When true will not be validating the "iat" claim value to be in the + past from now. **Default:** 'false' + - `ignoreNbf`: `` When true will not be validating the "nbf" claim value to be in the + past from now. **Default:** 'false' + - `issuer`: `` Expected issuer value. An exact match must be found in the payload. + - `maxTokenAge`: `` When provided the payload is checked to have the "iat" claim and its + value is validated not to be older than the provided timespan string e.g. `30m`, `24 hours`. + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` Expected subject value. An exact match must be found in the payload. +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createPublicKey } = require('crypto') +const { V4 } = require('paseto') + +const key = createPrivateKey(publicKey) + +const token = 'v4.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMjEtMDctMTlUMTA6MTM6MjIuOTM3WiIsImV4cCI6IjIwMjEtMDctMTlUMTI6MTM6MjIuOTM3WiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifYZrfK1eH8d7Scp218_DPEX8H3ElIfzWWMu9UQVZYjyV585BEBV0wTRk-vZgtXq0y5z0euOE48a2Yd6TLKfA5Qs' + +(async () => { + await V4.verify(token, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + clockTolerance: '1 min' + }) + // { + // 'urn:example:claim': 'foo', + // iat: '2019-07-02T13:36:12.380Z', + // exp: '2019-07-02T15:36:12.380Z', + // aud: 'urn:example:client', + // iss: 'https://op.example.com' + // } +})() +``` +
+ +--- + +#### V4.generateKey(purpose) + +Generates a new secret or private key for a given purpose. + +- `purpose`: `` PASETO purpose, only 'public' is supported. +- Returns: `Promise` + +--- + +#### V4.bytesToKeyObject(bytes) + +_This function aids in conversion between raw keying material used by the PASETO +specification, which is inspired by Libsodium, and the native Node.js KeyObject class +used to represent keys._ + +Imports a sequence of bytes as a KeyObject for use with the V4.sign and V4.verify functions. +`bytes` must be of either 32 bytes (public key) or 64 bytes (private key) in the format used +by [Libsodium](https://libsodium.org) (NaCl). + +The result will be either "private", or "public" KeyObject depending on the "bytes" argument. + +- `bytes`: `` +- Returns: `` + +--- + +#### V4.keyObjectToBytes(keyObject) + +_This function aids in conversion between the native Node.js KeyObject class +used to represent keys and the raw keying material used by the PASETO +specification, which is inspired by Libsodium._ + +Exports a sequence of bytes from a KeyObject for use with other PASETO +implementations. + +When `keyObject.type` is `"private"` the resulting Buffer will be the private key. +When `keyObject.type` is `"public"` the resulting Buffer will be the public key. +Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public one. + +- `keyObject`: `` +- Returns: `` + +--- + +## V3 (PASETO Protocol Version v3) + + +- [V3.sign(payload, key[, options])](#v3signpayload-key-options) +- [V3.verify(token, key[, options])](#v3verifytoken-key-options) +- [V3.encrypt(payload, key[, options])](#v3encryptpayload-key-options) +- [V3.decrypt(token, key[, options])](#v3decrypttoken-key-options) +- [V3.generateKey(purpose)](#v3generatekeypurpose) +- [V3.bytesToKeyObject(bytes)](#v3bytestokeyobjectbytes) +- [V3.keyObjectToBytes(keyObject)](#v3keyobjecttobyteskeyobject) + + + +```js +const { V3 } = require('paseto') +// { +// sign: [AsyncFunction: v3Sign], +// verify: [AsyncFunction: v3Verify], +// encrypt: [AsyncFunction: v3Encrypt], +// decrypt: [AsyncFunction: v3Decrypt], +// generateKey: [AsyncFunction: generateKey], +// bytesToKeyObject: [Function: bytesToKeyObject], +// keyObjectToBytes: [Function: keyObjectToBytes] +// } +``` + +--- +#### V3.sign(payload, key[, options]) + +Serializes and signs the payload as a PASETO using the provided private key. + +- `payload`: `` PASETO Payload claims +- `key`: `` The key to sign with. Alternatively any input that works for `crypto.createPrivateKey()` + or `V3.bytesToKeyObject()`. +- `options`: `` + - `assertion`: `` | `` PASETO Implicit Assertion + - `audience`: `` PASETO Audience, "aud" claim value, if provided it will replace + "aud" found in the payload + - `expiresIn`: `` PASETO Expiration Time, "exp" claim value, specified as string which is + added to the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it + will replace Expiration Time found in the payload + - `footer`: `` | `` | `` PASETO footer + - `iat`: `` When true it pushes the "iat" to the PASETO payload. **Default:** 'true' + - `issuer`: `` PASETO Issuer, "iss" claim value, if provided it will replace "iss" found in + the payload + - `jti`: `` Token ID, "jti" claim value, if provided it will replace "jti" found in the + payload + - `kid`: `` Key ID, "kid" claim value, if provided it will replace "kid" found in the + payload + - `notBefore`: `` PASETO Not Before, "nbf" claim value, specified as string which is added to + the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it will + replace Not Before found in the payload + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` PASETO subject, "sub" claim value, if provided it will replace "sub" found in + the payload +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createPrivateKey } = require('crypto') +const { V3 } = require('paseto') + +const key = createPrivateKey(privateKey) + +const payload = { + 'urn:example:claim': 'foo' +} + +(async () => { + const token = await V3.sign(payload, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + expiresIn: '2 hours' + }) + // v3.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMjEtMDctMTlUMTA6MTY6MTIuNzgxWiIsImV4cCI6IjIwMjEtMDctMTlUMTI6MTY6MTIuNzgxWiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifZ4uLDV9BLaDTBqH88cjLkbNiCjz-Q9bTWBwFcsNT9jGQr2eVr36J0x8osSB3ErvqWvAoaVyN-h-TLzlh_bQKc4dicQIVRikMsSo9A31--KstKef5NFEMAK5j6Q6ZXNSMQ +})() +``` +
+ +--- + +#### V3.verify(token, key[, options]) + +Verifies the claims and signature of a PASETO + +- `token`: `` PASETO to verify +- `key`: `` The key to verify with. Alternatively any input that works for `crypto.createPublicKey()` + or `V3.bytesToKeyObject()`. +- `options`: `` + - `assertion`: `` | `` PASETO Implicit Assertion + - `audience`: `` Expected audience value. An exact match must be found in the payload. + - `clockTolerance`: `` Clock Tolerance for comparing timestamps, provided as timespan + string e.g. `120s`, `2 minutes`, etc. **Default:** no clock tolerance + - `complete`: `` When false only the parsed payload is returned, otherwise an object with + a parsed payload and footer (as a Buffer) will be returned. + **Default:** 'false' + - `ignoreExp`: `` When true will not be validating the "exp" claim value to be in the + future from now. **Default:** 'false' + - `ignoreIat`: `` When true will not be validating the "iat" claim value to be in the + past from now. **Default:** 'false' + - `ignoreNbf`: `` When true will not be validating the "nbf" claim value to be in the + past from now. **Default:** 'false' + - `issuer`: `` Expected issuer value. An exact match must be found in the payload. + - `maxTokenAge`: `` When provided the payload is checked to have the "iat" claim and its + value is validated not to be older than the provided timespan string e.g. `30m`, `24 hours`. + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` Expected subject value. An exact match must be found in the payload. +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createPublicKey } = require('crypto') +const { V3 } = require('paseto') + +const key = createPrivateKey(publicKey) + +const token = 'v3.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMjEtMDctMTlUMTA6MTY6MTIuNzgxWiIsImV4cCI6IjIwMjEtMDctMTlUMTI6MTY6MTIuNzgxWiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifZ4uLDV9BLaDTBqH88cjLkbNiCjz-Q9bTWBwFcsNT9jGQr2eVr36J0x8osSB3ErvqWvAoaVyN-h-TLzlh_bQKc4dicQIVRikMsSo9A31--KstKef5NFEMAK5j6Q6ZXNSMQ' + +(async () => { + await V3.verify(token, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + clockTolerance: '1 min' + }) + // { + // 'urn:example:claim': 'foo', + // iat: '2019-07-02T14:02:22.489Z', + // exp: '2019-07-02T16:02:22.489Z', + // aud: 'urn:example:client', + // iss: 'https://op.example.com' + // } +})() +``` +
+ +--- + +#### V3.encrypt(payload, key[, options]) + +Serializes and encrypts the payload as a PASETO using the provided secret key. + +- `payload`: `` PASETO Payload claims +- `key`: `` The secret key to encrypt with. Alternatively any input that works for `crypto.createSecretKey` +- `options`: `` + - `assertion`: `` | `` PASETO Implicit Assertion + - `audience`: `` PASETO Audience, "aud" claim value, if provided it will replace + "aud" found in the payload + - `expiresIn`: `` PASETO Expiration Time, "exp" claim value, specified as string which is + added to the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it + will replace Expiration Time found in the payload + - `footer`: `` | `` | `` PASETO footer + - `iat`: `` When true it pushes the "iat" to the PASETO payload. **Default:** 'true' + - `issuer`: `` PASETO Issuer, "iss" claim value, if provided it will replace "iss" found in + the payload + - `jti`: `` Token ID, "jti" claim value, if provided it will replace "jti" found in the + payload + - `kid`: `` Key ID, "kid" claim value, if provided it will replace "kid" found in the + payload + - `notBefore`: `` PASETO Not Before, "nbf" claim value, specified as string which is added to + the current unix epoch timestamp e.g. `24 hours`, `20 m`, `60s`, etc., if provided it will + replace Not Before found in the payload + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` PASETO subject, "sub" claim value, if provided it will replace "sub" found in + the payload +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createSecretKey } = require('crypto') +const { V3 } = require('paseto') + +const key = createSecretKey(secret) + +const payload = { + 'urn:example:claim': 'foo' +} + +(async () => { + const token = await V3.encrypt(payload, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + expiresIn: '2 hours' + }) + // v3.local.aY9txiwDEjnQpCUe2muaPlFSEHH7OTYcjv4GTEyiFvecI7Y4-0_msLxpyq-_iYb3JGwgnlxCRYc1vhuRsrERE6TZxPj6dgXUzAASZQ48SqyTtqT2ntQ3l0kO1fw5neCQvHTEkIT7wENLrDBjeGWjBye0Eyh-Tj9-fZbn75TnQJ09uE5-5hbj5DXAp9IMMy-rCWM9Zecnn8_TN31IZiIMFu5EyHj304UKdgHNq2HHHSsVH24QjQjX-8K-0WYvpR7zNem8YWnOaCdDkb3dvvhJH0L8BWDQxKtvZiagxI1-Iw1GNXxK9LQe +})() +``` +
+ +--- + +#### V3.decrypt(token, key[, options]) + +Decrypts and validates the claims of a PASETO + +- `token`: `` PASETO to decrypt and validate +- `key`: `` The secret key to decrypt with. Alternatively any input that works for `crypto.createSecretKey` +- `options`: `` + - `assertion`: `` | `` PASETO Implicit Assertion + - `audience`: `` Expected audience value. An exact match must be found in the payload. + - `clockTolerance`: `` Clock Tolerance for comparing timestamps, provided as timespan + string e.g. `120s`, `2 minutes`, etc. **Default:** no clock tolerance + - `complete`: `` When false only the parsed payload is returned, otherwise an object with + a parsed payload and footer (as a Buffer) will be returned. + **Default:** 'false' + - `ignoreExp`: `` When true will not be validating the "exp" claim value to be in the + future from now. **Default:** 'false' + - `ignoreIat`: `` When true will not be validating the "iat" claim value to be in the + past from now. **Default:** 'false' + - `ignoreNbf`: `` When true will not be validating the "nbf" claim value to be in the + past from now. **Default:** 'false' + - `issuer`: `` Expected issuer value. An exact match must be found in the payload. + - `maxTokenAge`: `` When provided the payload is checked to have the "iat" claim and its + value is validated not to be older than the provided timespan string e.g. `30m`, `24 hours`. + - `now`: `` Date object to be used instead of the current unix epoch timestamp. + **Default:** 'new Date()' + - `subject`: `` Expected subject value. An exact match must be found in the payload. +- Returns: `Promise` + +
+Example (Click to expand) + +```js +const { createSecretKey } = require('crypto') +const { V3 } = require('paseto') + +const key = createSecretKey(secret) + +const token = 'v3.local.aY9txiwDEjnQpCUe2muaPlFSEHH7OTYcjv4GTEyiFvecI7Y4-0_msLxpyq-_iYb3JGwgnlxCRYc1vhuRsrERE6TZxPj6dgXUzAASZQ48SqyTtqT2ntQ3l0kO1fw5neCQvHTEkIT7wENLrDBjeGWjBye0Eyh-Tj9-fZbn75TnQJ09uE5-5hbj5DXAp9IMMy-rCWM9Zecnn8_TN31IZiIMFu5EyHj304UKdgHNq2HHHSsVH24QjQjX-8K-0WYvpR7zNem8YWnOaCdDkb3dvvhJH0L8BWDQxKtvZiagxI1-Iw1GNXxK9LQe' + +(async () => { + await V3.decrypt(token, key, { + audience: 'urn:example:client', + issuer: 'https://op.example.com', + clockTolerance: '1 min' + }) + // { + // 'urn:example:claim': 'foo', + // iat: '2019-07-02T14:03:39.631Z', + // exp: '2019-07-02T16:03:39.631Z', + // aud: 'urn:example:client', + // iss: 'https://op.example.com' + // } +})() +``` +
+ +--- + +#### V3.generateKey(purpose) + +Generates a new secret or private key for a given purpose. + +- `purpose`: `` PASETO purpose, either 'local' or 'public' +- Returns: `Promise` + +--- + +#### V3.bytesToKeyObject(bytes) + +_This function aids in conversion between raw keying material used by the PASETO +specification, which is inspired by Libsodium, and the native Node.js KeyObject class +used to represent keys._ + +Imports a sequence of bytes as a KeyObject for use with the V3.sign and V3.verify functions. +`bytes` must be of either 48 bytes (private key), 49 bytes (compressed public key coordinates), +or 97 bytes (uncompressed public key coordinates). + +The result will be either "private", or "public" KeyObject depending on the "bytes" argument. + +- `bytes`: `` +- Returns: `` + +--- + +#### V3.keyObjectToBytes(keyObject) + +_This function aids in conversion between the native Node.js KeyObject class +used to represent keys and the raw keying material used by the PASETO +specification, which is inspired by Libsodium._ + +Exports a sequence of bytes from a KeyObject for use with other PASETO +implementations. + +When `keyObject.type` is `"private"` the resulting Buffer will be the private key. +When `keyObject.type` is `"public"` the resulting Buffer will be the compressed public key coordinates. +Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public one. + +- `keyObject`: `` +- Returns: `` + +--- + ## V2 (PASETO Protocol Version v2) - [V2.sign(payload, key[, options])](#v2signpayload-key-options) - [V2.verify(token, key[, options])](#v2verifytoken-key-options) - [V2.generateKey(purpose)](#v2generatekeypurpose) +- [V2.bytesToKeyObject(bytes)](#v2bytestokeyobjectbytes) +- [V2.keyObjectToBytes(keyObject)](#v2keyobjecttobyteskeyobject) @@ -31,7 +517,9 @@ const { V2 } = require('paseto') // { // sign: [AsyncFunction: v2Sign], // verify: [AsyncFunction: v2Verify], -// generateKey: [AsyncFunction: generateKey] +// generateKey: [AsyncFunction: generateKey], +// bytesToKeyObject: [Function: bytesToKeyObject], +// keyObjectToBytes: [Function: keyObjectToBytes] // } ``` @@ -41,7 +529,8 @@ const { V2 } = require('paseto') Serializes and signs the payload as a PASETO using the provided private key. - `payload`: `` PASETO Payload claims -- `key`: `` The key to sign with. Alternatively any input that works for `crypto.createPrivateKey` +- `key`: `` The key to sign with. Alternatively any input that works for `crypto.createPrivateKey()` + or `V2.bytesToKeyObject()`. - `options`: `` - `audience`: `` PASETO Audience, "aud" claim value, if provided it will replace "aud" found in the payload @@ -96,7 +585,8 @@ const payload = { Verifies the claims and signature of a PASETO - `token`: `` PASETO to verify -- `key`: `` The key to verify with. Alternatively any input that works for `crypto.createPublicKey`. +- `key`: `` The key to verify with. Alternatively any input that works for `crypto.createPublicKey()` + or `V2.bytesToKeyObject()`. - `options`: `` - `audience`: `` Expected audience value. An exact match must be found in the payload. - `clockTolerance`: `` Clock Tolerance for comparing timestamps, provided as timespan @@ -157,6 +647,41 @@ Generates a new secret or private key for a given purpose. --- +#### V2.bytesToKeyObject(bytes) + +_This function aids in conversion between raw keying material used by the PASETO +specification, which is inspired by Libsodium, and the native Node.js KeyObject class +used to represent keys._ + +Imports a sequence of bytes as a KeyObject for use with the V2.sign and V2.verify functions. +`bytes` must be of either 32 bytes (public key) or 64 bytes (private key) in the format used +by [Libsodium](https://libsodium.org) (NaCl). + +The result will be either "private", or "public" KeyObject depending on the "bytes" argument. + +- `bytes`: `` +- Returns: `` + +--- + +#### V2.keyObjectToBytes(keyObject) + +_This function aids in conversion between the native Node.js KeyObject class +used to represent keys and the raw keying material used by the PASETO +specification, which is inspired by Libsodium._ + +Exports a sequence of bytes from a KeyObject for use with other PASETO +implementations. + +When `keyObject.type` is `"private"` the resulting Buffer will be the private key. +When `keyObject.type` is `"public"` the resulting Buffer will be the public key. +Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public one. + +- `keyObject`: `` +- Returns: `` + +--- + ## V1 (PASETO Protocol Version v1) diff --git a/lib/errors.js b/lib/errors.js index de683a5..076de3c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -4,11 +4,10 @@ const CODES = { PasetoInvalid: 'ERR_PASETO_INVALID', PasetoVerificationFailed: 'ERR_PASETO_VERIFICATION_FAILED', PasetoClaimInvalid: 'ERR_PASETO_CLAIM_INVALID', - PasetoWorkerFailure: 'ERR_PASETO_WORKER_FAILURE' } class PasetoError extends Error { - constructor (message) { + constructor(message) { super(message) this.name = this.constructor.name this.code = CODES[this.constructor.name] @@ -23,4 +22,3 @@ module.exports.PasetoDecryptionFailed = class PasetoDecryptionFailed extends Pas module.exports.PasetoInvalid = class PasetoInvalid extends PasetoError {} module.exports.PasetoVerificationFailed = class PasetoVerificationFailed extends PasetoError {} module.exports.PasetoClaimInvalid = class PasetoClaimInvalid extends PasetoError {} -module.exports.PasetoWorkerFailure = class PasetoWorkerFailure extends PasetoError {} diff --git a/lib/general/decode.js b/lib/general/decode.js index 1196950..afe96fb 100644 --- a/lib/general/decode.js +++ b/lib/general/decode.js @@ -2,24 +2,18 @@ const { PasetoInvalid, PasetoNotSupported } = require('../errors') const { decode } = require('../help/base64url') const parsePayload = require('../help/parse_paseto_payload') -module.exports = (token, /* second arg is private API */{ parse = true } = {}) => { +module.exports = (token, /* second arg is private API */ { parse = true } = {}) => { if (typeof token !== 'string') { throw new TypeError('token must be a string') } - const { - 0: version, - 1: purpose, - 2: payload, - 3: footer, - length - } = token.split('.') + 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') } - if (version !== 'v1' && version !== 'v2') { + if (version !== 'v1' && version !== 'v2' && version !== 'v3' && version !== 'v4') { throw new PasetoNotSupported('unsupported PASETO version') } @@ -27,17 +21,22 @@ module.exports = (token, /* second arg is private API */{ parse = true } = {}) = throw new PasetoNotSupported('unsupported PASETO purpose') } - const result = { footer: footer ? decode(footer) : undefined, payload: undefined, version, purpose } + const result = { + footer: footer ? decode(footer) : undefined, + payload: undefined, + version, + purpose, + } if (purpose === 'local') { return result } - const sigLength = version === 'v1' ? 256 : 64 + const sigLength = version === 'v1' ? 256 : version === 'v3' ? 96 : 64 let raw try { - raw = decode(payload).slice(0, -sigLength) + raw = decode(payload).subarray(0, -sigLength) } catch (err) { throw new PasetoInvalid('token value is not a PASETO formatted value') } diff --git a/lib/help/apply_options.js b/lib/help/apply_options.js index 9e88fe6..e1cdcb6 100644 --- a/lib/help/apply_options.js +++ b/lib/help/apply_options.js @@ -1,8 +1,9 @@ const ms = require('../help/ms') -module.exports = ({ - audience, expiresIn, iat = true, issuer, jti, kid, notBefore, now = new Date(), subject -}, payload) => { +module.exports = ( + { audience, expiresIn, iat = true, issuer, jti, kid, notBefore, now = new Date(), subject }, + payload, +) => { if (!(now instanceof Date) || !now.getTime()) { throw new TypeError('options.now must be a valid Date object') } diff --git a/lib/help/assert_payload.js b/lib/help/assert_payload.js index 4075f2c..ef8213a 100644 --- a/lib/help/assert_payload.js +++ b/lib/help/assert_payload.js @@ -1,9 +1,20 @@ const { PasetoClaimInvalid } = require('../errors') const ms = require('./ms') -module.exports = ({ - ignoreExp, ignoreNbf, ignoreIat, maxTokenAge, subject, issuer, clockTolerance, audience, now = new Date() -}, payload) => { +module.exports = ( + { + ignoreExp, + ignoreNbf, + ignoreIat, + maxTokenAge, + subject, + issuer, + clockTolerance, + audience, + now = new Date(), + }, + payload, +) => { if (!(now instanceof Date) || !now.getTime()) { throw new TypeError('options.now must be a valid Date object') } diff --git a/lib/help/base64url.js b/lib/help/base64url.js index 4cc8972..5948726 100644 --- a/lib/help/base64url.js +++ b/lib/help/base64url.js @@ -1,8 +1,2 @@ -if (Buffer.isEncoding('base64url')) { - module.exports.encode = (input) => Buffer.from(input).toString('base64url') -} else { - module.exports.encode = (input) => - input.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') -} - +module.exports.encode = (input) => input.toString('base64url') module.exports.decode = (input) => Buffer.from(input, 'base64') diff --git a/lib/help/check_assertion.js b/lib/help/check_assertion.js new file mode 100644 index 0000000..e59bec9 --- /dev/null +++ b/lib/help/check_assertion.js @@ -0,0 +1,15 @@ +module.exports = function checkAssertion(assertion) { + if (typeof assertion === 'undefined') { + return Buffer.from('') + } + + if (Buffer.isBuffer(assertion)) { + return assertion + } + + if (typeof assertion !== 'string') { + throw new TypeError('options.assertion must be a string, or a Buffer') + } + + return Buffer.from(assertion, 'utf8') +} diff --git a/lib/help/check_footer.js b/lib/help/check_footer.js index 59828fd..935d8de 100644 --- a/lib/help/check_footer.js +++ b/lib/help/check_footer.js @@ -1,6 +1,6 @@ const isObject = require('./is_object') -module.exports = function checkFooter (footer) { +module.exports = function checkFooter(footer) { if (typeof footer === 'undefined') { return Buffer.from('') } diff --git a/lib/help/check_payload.js b/lib/help/check_payload.js index 773743f..14d1125 100644 --- a/lib/help/check_payload.js +++ b/lib/help/check_payload.js @@ -1,6 +1,6 @@ const applyOptions = require('./apply_options') const isObject = require('./is_object') -const deepClone = payload => JSON.parse(JSON.stringify(payload)) +const deepClone = (payload) => JSON.parse(JSON.stringify(payload)) module.exports = (payload, options) => { if (Buffer.isBuffer(payload)) { diff --git a/lib/help/compress_pk.js b/lib/help/compress_pk.js new file mode 100644 index 0000000..8538672 --- /dev/null +++ b/lib/help/compress_pk.js @@ -0,0 +1,6 @@ +module.exports = (key) => { + const { x, y } = key.export({ format: 'jwk' }) + const yB = Buffer.from(y, 'base64') + const sign = 0x02 + (yB[yB.length - 1] & 1) + return Buffer.concat([Buffer.alloc(1, sign), Buffer.from(x, 'base64')]) +} diff --git a/lib/help/crypto_worker.js b/lib/help/crypto_worker.js index e42b9f6..2dd0329 100644 --- a/lib/help/crypto_worker.js +++ b/lib/help/crypto_worker.js @@ -1,167 +1,114 @@ -const { parentPort, Worker, isMainThread } = require('worker_threads') const crypto = require('crypto') const util = require('util') -const { PasetoWorkerFailure } = require('../errors') - -if (isMainThread) { - const tasks = new Map() - - let worker - let taskId = 0 - - const spawn = () => { - worker = new Worker(__filename) - worker.on('message', function ({ id, fulfilled, rejected }) { - const [resolve, reject] = tasks.get(id) - tasks.delete(id) - if (tasks.size === 0) { - worker.unref() - } - - if (rejected) { - reject(new PasetoWorkerFailure()) - } else { - if (fulfilled instanceof Uint8Array) { - fulfilled = Buffer.from(fulfilled) - } - resolve(fulfilled) - } - }) - } - - const work = (method, ...args) => new Promise((resolve, reject) => { - const id = taskId++ - if (id === Number.MAX_SAFE_INTEGER) { - taskId = 0 - } - tasks.set(id, [resolve, reject]) - - if (worker === undefined) { - spawn() - } - - worker.ref() - worker.postMessage({ id, method, args }) - }) - - const [major, minor] = process.version - .substr(1) - .split('.') - .map((str) => parseInt(str, 10)) - - const oneShotCallbackSupported = major >= 16 || (major === 15 && minor >= 13) - - module.exports = { - sign: oneShotCallbackSupported ? util.promisify(crypto.sign) : work.bind(undefined, 'sign'), - verify: oneShotCallbackSupported ? util.promisify(crypto.verify) : work.bind(undefined, 'verify'), - 'aes-256-ctr-hmac-sha-384-encrypt': work.bind(undefined, 'aes-256-ctr-hmac-sha-384-encrypt'), - 'aes-256-ctr-hmac-sha-384-decrypt': work.bind(undefined, 'aes-256-ctr-hmac-sha-384-decrypt') - } -} else { - const pae = require('./pae') - let hkdf - if (crypto.hkdf) { - const pHkdf = util.promisify(crypto.hkdf) - hkdf = (key, length, salt, info) => pHkdf('sha384', key, salt, info, length) - } else { - hkdf = (key, length, salt, info) => { - const prk = methods.hmac('sha384', key, salt) - - const u = Buffer.from(info) - - let t = Buffer.from('') - let lb = Buffer.from('') - let i - - for (let bi = 1; Buffer.byteLength(t) < length; ++i) { - i = Buffer.from(String.fromCharCode(bi)) - const inp = Buffer.concat([lb, u, i]) - - lb = methods.hmac('sha384', inp, prk) - t = Buffer.concat([t, lb]) - } - - const orm = Buffer.from(t).slice(0, length) - return orm - } - } - - const pack = require('./pack') - const timingSafeEqual = require('./timing_safe_equal') - - const methods = { - async 'aes-256-ctr-hmac-sha-384-encrypt' (m, f, k, nonce) { - let n = methods.hmac('sha384', m, nonce) - n = n.slice(0, 32) - f = Buffer.from(f) - - const salt = n.slice(0, 16) - const [ek, ak] = await Promise.all([ - hkdf(k, 32, salt, 'paseto-encryption-key'), - hkdf(k, 32, salt, 'paseto-auth-key-for-aead') - ]) - - const c = methods.encrypt('aes-256-ctr', m, ek, n.slice(16)) - const preAuth = pae('v1.local.', n, c, f) - const t = methods.hmac('sha384', preAuth, ak) - - return pack('v1.local.', [n, c, t], f) - }, - async 'aes-256-ctr-hmac-sha-384-decrypt' (raw, f, k) { - const n = raw.slice(0, 32) - const t = raw.slice(-48) - const c = raw.slice(32, -48) - - const salt = n.slice(0, 16) - const [ek, ak] = await Promise.all([ - hkdf(k, 32, salt, 'paseto-encryption-key'), - hkdf(k, 32, salt, 'paseto-auth-key-for-aead') - ]) - - const preAuth = pae('v1.local.', n, c, f) - - const t2 = methods.hmac('sha384', preAuth, ak) - if (!timingSafeEqual(t, t2)) return false - const payload = methods.decrypt('aes-256-ctr', c, ek, n.slice(16)) - if (!payload) return false - - return payload - }, - hmac (alg, payload, key) { - const hmac = crypto.createHmac(alg, key) - hmac.update(payload) - return hmac.digest() - }, - verify (alg, payload, key, signature) { - return crypto.verify(alg, payload, key, signature) - }, - sign (alg, payload, key) { - return crypto.sign(alg, payload, key) - }, - encrypt (cipher, cleartext, key, iv) { - const encryptor = crypto.createCipheriv(cipher, key, iv) - return Buffer.concat([encryptor.update(cleartext), encryptor.final()]) - }, - decrypt (cipher, ciphertext, key, iv) { - const decryptor = crypto.createDecipheriv(cipher, key, iv) - return Buffer.concat([decryptor.update(ciphertext), decryptor.final()]) - } - } - - parentPort.on('message', function ({ id, method, args }) { - try { - const value = methods[method](...args) - if (value instanceof Promise) { - value.then((fulfilled) => { - parentPort.postMessage({ id, fulfilled }) - }, (rejected) => { - parentPort.postMessage({ id, rejected: true }) - }) - } else { - parentPort.postMessage({ id, fulfilled: value }) - } - } catch (err) { - parentPort.postMessage({ id, rejected: true }) - } - }) + +const pae = require('./pae') +const pack = require('./pack') +const { PasetoDecryptionFailed } = require('../errors') +const timingSafeEqual = require('./timing_safe_equal') + +const { + webcrypto: { subtle }, +} = crypto +const hkdf = util.promisify(crypto.hkdf) + +const EK_INFO = Buffer.from('paseto-encryption-key') +const AK_INFO = Buffer.from('paseto-auth-key-for-aead') +const EMPTY = Buffer.alloc(0) + +async function v1encrypt(m, f, k) { + const h = 'v1.local.' + const n = hmac(m, crypto.randomBytes(32)).subarray(0, 32) + const salt = n.subarray(0, 16) + const [ek, ak] = await Promise.all([ + hkdf('sha384', k, salt, EK_INFO, 32).then(Buffer.from), + hkdf('sha384', k, salt, AK_INFO, 32).then(Buffer.from), + ]) + + const c = await encrypt(m, ek, n.subarray(16)) + const preAuth = pae(h, n, c, f) + const t = hmac(preAuth, ak) + + return pack(h, [n, c, t], f) +} + +async function v1decrypt(raw, f, k) { + const h = 'v1.local.' + const n = raw.subarray(0, 32) + const t = raw.subarray(-48) + const c = raw.subarray(32, -48) + + const salt = n.subarray(0, 16) + const [ek, ak] = await Promise.all([ + hkdf('sha384', k, salt, EK_INFO, 32).then(Buffer.from), + hkdf('sha384', k, salt, AK_INFO, 32).then(Buffer.from), + ]) + + const preAuth = pae(h, n, c, f) + + const t2 = hmac(preAuth, ak) + if (!timingSafeEqual(t, t2)) throw new PasetoDecryptionFailed('decryption failed') + const payload = await decrypt(c, ek, n.subarray(16)) + if (!payload) throw new PasetoDecryptionFailed('decryption failed') + + return payload +} + +async function v3encrypt(m, f, k, i) { + const h = 'v3.local.' + const n = crypto.randomBytes(32) + const [tmp, ak] = await Promise.all([ + hkdf('sha384', k, EMPTY, Buffer.concat([EK_INFO, n]), 48).then(Buffer.from), + hkdf('sha384', k, EMPTY, Buffer.concat([AK_INFO, n]), 48).then(Buffer.from), + ]) + const ek = tmp.subarray(0, 32) + const n2 = tmp.subarray(32) + + const c = await encrypt(m, ek, n2) + const preAuth = pae(h, n, c, f, i) + const t = hmac(preAuth, ak) + + return pack(h, [n, c, t], f) +} + +async function v3decrypt(raw, f, k, i) { + const h = 'v3.local.' + const n = raw.subarray(0, 32) + const t = raw.subarray(-48) + const c = raw.subarray(32, -48) + + const [tmp, ak] = await Promise.all([ + hkdf('sha384', k, EMPTY, Buffer.concat([EK_INFO, n]), 48).then(Buffer.from), + hkdf('sha384', k, EMPTY, Buffer.concat([AK_INFO, n]), 48).then(Buffer.from), + ]) + + const ek = tmp.subarray(0, 32) + const n2 = tmp.subarray(32) + const preAuth = pae(h, n, c, f, i) + const t2 = hmac(preAuth, ak) + + if (!timingSafeEqual(t, t2)) throw new PasetoDecryptionFailed('decryption failed') + const payload = await decrypt(c, ek, n2) + if (!payload) throw new PasetoDecryptionFailed('decryption failed') + + return payload +} + +const hmac = (data, key) => crypto.createHmac('sha384', key).update(data).digest() + +const ctr = async (op, data, key, iv) => + subtle[op]( + { name: 'AES-CTR', counter: iv, length: 16 }, + await subtle.importKey('raw', key, 'AES-CTR', false, [op]), + data, + ).then(Buffer.from) +const encrypt = ctr.bind(undefined, 'encrypt') +const decrypt = ctr.bind(undefined, 'decrypt') + +module.exports = { + sign: util.promisify(crypto.sign), + verify: util.promisify(crypto.verify), + 'v1.local-encrypt': v1encrypt, + 'v1.local-decrypt': v1decrypt, + 'v3.local-encrypt': v3encrypt, + 'v3.local-decrypt': v3decrypt, } diff --git a/lib/help/is_key_object.js b/lib/help/is_key_object.js new file mode 100644 index 0000000..295f007 --- /dev/null +++ b/lib/help/is_key_object.js @@ -0,0 +1,8 @@ +const { KeyObject } = require('crypto') +let { isKeyObject } = require('util/types') + +if (!isKeyObject) { + isKeyObject = (obj) => obj != null && obj instanceof KeyObject +} + +module.exports = isKeyObject diff --git a/lib/help/is_object.js b/lib/help/is_object.js index 0c20e2c..f3cca4c 100644 --- a/lib/help/is_object.js +++ b/lib/help/is_object.js @@ -1 +1 @@ -module.exports = input => !!input && input.constructor === Object +module.exports = (input) => !!input && input.constructor === Object diff --git a/lib/help/le64.js b/lib/help/le64.js index e89eb70..eb7dc15 100644 --- a/lib/help/le64.js +++ b/lib/help/le64.js @@ -5,8 +5,8 @@ module.exports = (n) => { throw new PasetoNotSupported('message is too long for Node.js to safely process') } - const up = ~~(n / 0xFFFFFFFF) - const dn = (n % 0xFFFFFFFF) - up + const up = ~~(n / 0xffffffff) + const dn = (n % 0xffffffff) - up const buf = Buffer.allocUnsafe(8) diff --git a/lib/help/ms.js b/lib/help/ms.js index 3ac72e0..ceaa318 100644 --- a/lib/help/ms.js +++ b/lib/help/ms.js @@ -5,7 +5,8 @@ const day = hour * 24 const week = day * 7 const year = day * 365.25 -const REGEX = /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i +const REGEX = + /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i module.exports = (str) => { const matched = REGEX.exec(str) diff --git a/lib/help/pack.js b/lib/help/pack.js index 290a4a7..68068ae 100644 --- a/lib/help/pack.js +++ b/lib/help/pack.js @@ -1,7 +1,7 @@ const { encode } = require('./base64url') -module.exports = function pack (header, payload, footer) { - if (footer.length !== 0) { +module.exports = function pack(header, payload, footer) { + if (footer.byteLength) { return `${header}${encode(Buffer.concat(payload))}.${encode(footer)}` } diff --git a/lib/help/pae.js b/lib/help/pae.js index 0d7241b..0dc4398 100644 --- a/lib/help/pae.js +++ b/lib/help/pae.js @@ -1,6 +1,7 @@ const le64 = require('./le64') module.exports = (...pieces) => { + pieces = pieces.filter(Boolean) let accumulator = le64(pieces.length) for (let piece of pieces) { piece = Buffer.from(piece, 'utf8') diff --git a/lib/help/random_bytes.js b/lib/help/random_bytes.js deleted file mode 100644 index 1fb194d..0000000 --- a/lib/help/random_bytes.js +++ /dev/null @@ -1,9 +0,0 @@ -const crypto = require('crypto') -const { promisify } = require('util') - -const randomFill = promisify(crypto.randomFill) - -module.exports = async function randomBytes (bytes) { - const buf = Buffer.allocUnsafe(bytes) - return randomFill(buf) -} diff --git a/lib/help/sign.js b/lib/help/sign.js index 59d723d..3b4cb5e 100644 --- a/lib/help/sign.js +++ b/lib/help/sign.js @@ -2,14 +2,16 @@ const { sign } = require('./crypto_worker') const pae = require('./pae') const pack = require('./pack') +const compressPk = require('./compress_pk') -module.exports = async function signPaseto (h, m, f, alg, key, expectedSigLength) { - const m2 = pae(h, m, f) - const sig = await sign(alg, m2, key) - - if (sig.length !== expectedSigLength) { - throw new TypeError(`invalid ${h.slice(0, -1)} signing key bit length`) +module.exports = async function signPaseto(h, m, f, alg, key, i) { + let m2 + if (h === 'v3.public.') { + m2 = pae(compressPk(key.key), h, m, f, i) + } else { + m2 = pae(h, m, f, i) } + const sig = await sign(alg, m2, key) return pack(h, [m, sig], f) } diff --git a/lib/help/symmetric_key_check.js b/lib/help/symmetric_key_check.js index 68bf4cf..cdea4de 100644 --- a/lib/help/symmetric_key_check.js +++ b/lib/help/symmetric_key_check.js @@ -1,8 +1,16 @@ -const { createSecretKey, KeyObject } = require('crypto') +const { createSecretKey } = require('crypto') -module.exports = function checkKey (header, key) { - if (!(key instanceof KeyObject)) { - key = createSecretKey(key) +const isKeyObject = require('./is_key_object') + +module.exports = function checkKey(header, key) { + if (!isKeyObject(key)) { + try { + key = createSecretKey(key) + } catch {} + } + + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') } if (key.type !== 'secret' || key.symmetricKeySize !== 32) { diff --git a/lib/help/verify.js b/lib/help/verify.js index 3913a59..41b97db 100644 --- a/lib/help/verify.js +++ b/lib/help/verify.js @@ -3,8 +3,9 @@ const { PasetoInvalid, PasetoVerificationFailed } = require('../errors') const { decode } = require('./base64url') const { verify } = require('./crypto_worker') const pae = require('./pae') +const compressPk = require('./compress_pk') -module.exports = async function verifyPaseto (h, token, alg, sigLength, key) { +module.exports = async function verifyPaseto(h, token, alg, sigLength, key, i) { if (typeof token !== 'string') { throw new TypeError('token must be a string') } @@ -28,18 +29,21 @@ module.exports = async function verifyPaseto (h, token, alg, sigLength, key) { throw new PasetoInvalid('token value is not a PASETO formatted value') } - const m = ms.slice(0, -sigLength) - const s = ms.slice(-sigLength) - const m2 = pae(h, m, f) - - const valid = await verify(alg, m2, key, s) + const m = ms.subarray(0, -sigLength) + const s = ms.subarray(-sigLength) + let m2 + if (h === 'v3.public.') { + m2 = pae(compressPk(key.key), h, m, f, i) + } else { + m2 = pae(h, m, f, i) + } - if (!valid) { + if (!(await verify(alg, m2, key, s))) { throw new PasetoVerificationFailed('invalid signature') } return { m, - footer: f.length ? f : undefined + footer: f.length ? f : undefined, } } diff --git a/lib/index.js b/lib/index.js index a57b957..bdfba8a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,9 @@ const errors = require('./errors') const V1 = require('./v1') const V2 = require('./v2') +const V3 = require('./v3') +const V4 = require('./v4') const { decode } = require('./general') -module.exports = { decode, V1, V2, errors } +module.exports = { decode, V1, V2, V3, V4, errors } diff --git a/lib/v1/decrypt.js b/lib/v1/decrypt.js index 50327b1..6676576 100644 --- a/lib/v1/decrypt.js +++ b/lib/v1/decrypt.js @@ -1,13 +1,17 @@ const { decode } = require('../help/base64url') -const { 'aes-256-ctr-hmac-sha-384-decrypt': decrypt } = require('../help/crypto_worker') -const { PasetoDecryptionFailed, PasetoInvalid } = require('../errors') +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 h = 'v1.local.' -module.exports = async function v1Decrypt (token, key, { complete = false, buffer = false, ...options } = {}) { +module.exports = async function v1Decrypt( + token, + key, + { complete = false, buffer = false, ...options } = {}, +) { if (typeof token !== 'string') { throw new TypeError(`token must be a string, got: ${typeof token}`) } @@ -28,9 +32,6 @@ module.exports = async function v1Decrypt (token, key, { complete = false, buffe const k = key.export() const m = await decrypt(raw, f, k) - if (!m) { - throw new PasetoDecryptionFailed('decryption failed') - } if (buffer) { if (Object.keys(options).length !== 0) { diff --git a/lib/v1/encrypt.js b/lib/v1/encrypt.js index c39d149..d73c607 100644 --- a/lib/v1/encrypt.js +++ b/lib/v1/encrypt.js @@ -1,19 +1,12 @@ const checkFooter = require('../help/check_footer') const checkKey = require('../help/symmetric_key_check').bind(undefined, 'v1.local') const checkPayload = require('../help/check_payload') -const randomBytes = require('../help/random_bytes') -const { 'aes-256-ctr-hmac-sha-384-encrypt': encrypt } = require('../help/crypto_worker') +const { 'v1.local-encrypt': encrypt } = require('../help/crypto_worker') -module.exports = async function v1Encrypt (payload, key, { footer, nonce, ...options } = {}) { +module.exports = async function v1Encrypt(payload, key, { footer, ...options } = {}) { const m = checkPayload(payload, options) key = checkKey(key) const f = checkFooter(footer) - const k = key.export() - - if ((nonce && process.env.NODE_ENV !== 'test') || !nonce) { - nonce = await randomBytes(32) - } - - return encrypt(m, f, k, nonce) + return encrypt(m, f, k) } diff --git a/lib/v1/key.js b/lib/v1/key.js index 7e70523..edf3781 100644 --- a/lib/v1/key.js +++ b/lib/v1/key.js @@ -2,19 +2,16 @@ const crypto = require('crypto') const { promisify } = require('util') const { PasetoNotSupported } = require('../errors') -const randomBytes = require('../help/random_bytes') const generateKeyPair = promisify(crypto.generateKeyPair) +const generateSecretKey = promisify(crypto.generateKey) -const LOCAL_KEY_LENGTH = 32 -const PUBLIC_KEY_ARGS = ['rsa', { modulusLength: 2048 }] - -async function generateKey (purpose) { +async function generateKey(purpose) { switch (purpose) { case 'local': - return crypto.createSecretKey(await randomBytes(LOCAL_KEY_LENGTH)) + return generateSecretKey('aes', { length: 256 }) case 'public': { - const { privateKey } = await generateKeyPair(...PUBLIC_KEY_ARGS) + const { privateKey } = await generateKeyPair('rsa', { modulusLength: 2048 }) return privateKey } default: diff --git a/lib/v1/sign.js b/lib/v1/sign.js index cbd54a6..83a058d 100644 --- a/lib/v1/sign.js +++ b/lib/v1/sign.js @@ -1,31 +1,40 @@ const { - constants: { - RSA_PKCS1_PSS_PADDING: padding, - RSA_PSS_SALTLEN_DIGEST: saltLength - }, + constants: { RSA_PKCS1_PSS_PADDING: padding, RSA_PSS_SALTLEN_DIGEST: saltLength }, createPrivateKey, - KeyObject } = 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') -function checkKey (key) { - if (!(key instanceof KeyObject)) { - key = createPrivateKey(key) +function checkKey(key) { + if (!isKeyObject(key)) { + try { + key = createPrivateKey(key) + } catch {} } - if (key.type !== 'private' || key.asymmetricKeyType !== 'rsa') { - throw new TypeError('v1.public signing key must be a private RSA key') + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') + } + + if ( + key.type !== 'private' || + key.asymmetricKeyType !== 'rsa' || + key.asymmetricKeyDetails.modulusLength !== 2048 + ) { + throw new TypeError( + 'v1.public signing key must be a private RSA key with 2048 bit modulus length', + ) } return key } -module.exports = async function v1Sign (payload, key, { footer, ...options } = {}) { +module.exports = async function v1Sign(payload, key, { footer, ...options } = {}) { const m = checkPayload(payload, options) const f = checkFooter(footer) key = checkKey(key) - return sign('v1.public.', m, f, 'sha384', { key, padding, saltLength }, 256) + return sign('v1.public.', m, f, 'sha384', { key, padding, saltLength }) } diff --git a/lib/v1/verify.js b/lib/v1/verify.js index 3702cd2..9bba943 100644 --- a/lib/v1/verify.js +++ b/lib/v1/verify.js @@ -1,32 +1,49 @@ const { - constants: { - RSA_PKCS1_PSS_PADDING: padding, - RSA_PSS_SALTLEN_DIGEST: saltLength - }, + constants: { RSA_PKCS1_PSS_PADDING: padding, RSA_PSS_SALTLEN_DIGEST: saltLength }, createPublicKey, - KeyObject } = 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') -function checkKey (key) { - if (!(key instanceof KeyObject) || key.type === 'private') { - key = createPublicKey(key) +function checkKey(key) { + if (!isKeyObject(key) || key.type === 'private') { + try { + key = createPublicKey(key) + } catch {} } - if (key.type !== 'public' || key.asymmetricKeyType !== 'rsa') { - throw new TypeError('v1.public verify key must be a public RSA key') + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') + } + + if ( + key.type !== 'public' || + key.asymmetricKeyType !== 'rsa' || + key.asymmetricKeyDetails.modulusLength !== 2048 + ) { + throw new TypeError( + 'v1.public verify key must be a public RSA key with 2048 bit modulus length', + ) } return key } -module.exports = async function v1Verify (token, key, { complete = false, buffer = false, ...options } = {}) { +module.exports = async function v1Verify( + token, + key, + { complete = false, buffer = false, ...options } = {}, +) { key = checkKey(key) - const { m, footer } = await verify('v1.public.', token, 'sha384', 256, { key, padding, saltLength }) + const { m, footer } = await verify('v1.public.', token, 'sha384', 256, { + key, + padding, + saltLength, + }) if (buffer) { if (Object.keys(options).length !== 0) { diff --git a/lib/v2/index.js b/lib/v2/index.js index e5c003c..fafb73e 100644 --- a/lib/v2/index.js +++ b/lib/v2/index.js @@ -1,5 +1,5 @@ const sign = require('./sign') const verify = require('./verify') -const generateKey = require('./key') +const { generateKey, bytesToKeyObject, keyObjectToBytes } = require('./key') -module.exports = { sign, verify, generateKey } +module.exports = { sign, verify, generateKey, bytesToKeyObject, keyObjectToBytes } diff --git a/lib/v2/key.js b/lib/v2/key.js index c8fb08b..73b9979 100644 --- a/lib/v2/key.js +++ b/lib/v2/key.js @@ -2,18 +2,85 @@ const crypto = require('crypto') const { promisify } = require('util') const { PasetoNotSupported } = require('../errors') +const isKeyObject = require('../help/is_key_object') const generateKeyPair = promisify(crypto.generateKeyPair) -async function generateKey (purpose) { +async function _generateKey(v, purpose) { switch (purpose) { case 'public': { const { privateKey } = await generateKeyPair('ed25519') return privateKey } default: - throw new PasetoNotSupported('unsupported v2 purpose') + throw new PasetoNotSupported(`unsupported ${v} purpose`) } } -module.exports = generateKey +function bytesToKeyObject(bytes) { + if (!Buffer.isBuffer(bytes)) { + throw new TypeError('bytes must be a Buffer') + } + + switch (bytes.byteLength) { + case 64: { + const keyObject = crypto.createPrivateKey({ + key: Buffer.concat([ + Buffer.from('302e020100300506032b657004220420', 'hex'), + bytes.subarray(0, 32), + ]), + format: 'der', + type: 'pkcs8', + }) + + if ( + !bytes.subarray(32).equals(Buffer.from(keyObject.export({ format: 'jwk' }).x, 'base64')) + ) { + throw new TypeError('invalid byte sequence') + } + + return keyObject + } + case 32: + return crypto.createPublicKey({ + key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), bytes]), + format: 'der', + type: 'spki', + }) + default: + throw new TypeError('bytes must be 64 bytes (private key), or 32 bytes (public key)') + } +} + +function _keyObjectToBytes(v, keyObject) { + if (!isKeyObject(keyObject)) { + throw new TypeError('keyObject must be a KeyObject instance') + } + if (keyObject.type === 'secret' || keyObject.asymmetricKeyType !== 'ed25519') { + throw new TypeError(`${v}.public key must be an Ed25519 key`) + } + switch (keyObject.type) { + case 'public': + return Buffer.from(keyObject.export({ format: 'jwk' }).x, 'base64') + case 'private': { + const { d, x } = keyObject.export({ format: 'jwk' }) + return Buffer.concat([Buffer.from(d, 'base64'), Buffer.from(x, 'base64')]) + } + } +} + +async function generateKey(...args) { + return _generateKey('v2', ...args) +} + +function keyObjectToBytes(...args) { + return _keyObjectToBytes('v2', ...args) +} + +module.exports = { + _generateKey, + _keyObjectToBytes, + bytesToKeyObject, + generateKey, + keyObjectToBytes, +} diff --git a/lib/v2/sign.js b/lib/v2/sign.js index 929b018..e2cdab8 100644 --- a/lib/v2/sign.js +++ b/lib/v2/sign.js @@ -1,15 +1,26 @@ -const { - createPrivateKey, - KeyObject -} = require('crypto') +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 (!(key instanceof KeyObject)) { - key = createPrivateKey(key) +function checkKey(key) { + if (Buffer.isBuffer(key)) { + try { + key = bytesToKeyObject(key) + } catch {} + } + + if (!isKeyObject(key)) { + try { + key = createPrivateKey(key) + } catch {} + } + + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') } if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') { @@ -19,9 +30,9 @@ function checkKey (key) { return key } -module.exports = async function v2Sign (payload, key, { footer, ...options } = {}) { +module.exports = async function v2Sign(payload, key, { footer, ...options } = {}) { const m = checkPayload(payload, options) key = checkKey(key) const f = checkFooter(footer) - return sign('v2.public.', m, f, undefined, key, 64) + return sign('v2.public.', m, f, undefined, key) } diff --git a/lib/v2/verify.js b/lib/v2/verify.js index 209b5bc..91757fd 100644 --- a/lib/v2/verify.js +++ b/lib/v2/verify.js @@ -1,15 +1,26 @@ -const { - createPublicKey, - KeyObject -} = require('crypto') +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 {} + } -function checkKey (key) { - if (!(key instanceof KeyObject) || key.type === 'private') { - key = createPublicKey(key) + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') } if (key.type !== 'public' || key.asymmetricKeyType !== 'ed25519') { @@ -19,7 +30,11 @@ function checkKey (key) { return key } -module.exports = async function v2Verify (token, key, { complete = false, buffer = false, ...options } = {}) { +module.exports = async function v2Verify( + token, + key, + { complete = false, buffer = false, ...options } = {}, +) { key = checkKey(key) const { m, footer } = await verify('v2.public.', token, undefined, 64, key) diff --git a/lib/v3/decrypt.js b/lib/v3/decrypt.js new file mode 100644 index 0000000..bc66255 --- /dev/null +++ b/lib/v3/decrypt.js @@ -0,0 +1,58 @@ +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 h = 'v3.local.' + +module.exports = async function v3Decrypt( + token, + key, + { complete = false, buffer = false, assertion, ...options } = {}, +) { + if (typeof token !== 'string') { + throw new TypeError(`token must be a string, got: ${typeof 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 +} diff --git a/lib/v3/encrypt.js b/lib/v3/encrypt.js new file mode 100644 index 0000000..ccf6e02 --- /dev/null +++ b/lib/v3/encrypt.js @@ -0,0 +1,14 @@ +const checkFooter = require('../help/check_footer') +const checkKey = require('../help/symmetric_key_check').bind(undefined, 'v3.local') +const checkPayload = require('../help/check_payload') +const checkAssertion = require('../help/check_assertion') +const { 'v3.local-encrypt': encrypt } = require('../help/crypto_worker') + +module.exports = async function v3Encrypt(payload, key, { footer, assertion, ...options } = {}) { + const m = checkPayload(payload, options) + key = checkKey(key) + const f = checkFooter(footer) + const i = checkAssertion(assertion) + const k = key.export() + return encrypt(m, f, k, i) +} diff --git a/lib/v3/index.js b/lib/v3/index.js new file mode 100644 index 0000000..af8907c --- /dev/null +++ b/lib/v3/index.js @@ -0,0 +1,7 @@ +const sign = require('./sign') +const verify = require('./verify') +const encrypt = require('./encrypt') +const decrypt = require('./decrypt') +const { generateKey, bytesToKeyObject, keyObjectToBytes } = require('./key') + +module.exports = { sign, verify, encrypt, decrypt, generateKey, bytesToKeyObject, keyObjectToBytes } diff --git a/lib/v3/key.js b/lib/v3/key.js new file mode 100644 index 0000000..571aed7 --- /dev/null +++ b/lib/v3/key.js @@ -0,0 +1,94 @@ +const crypto = require('crypto') +const { promisify } = require('util') + +const { PasetoNotSupported } = require('../errors') +const isKeyObject = require('../help/is_key_object') +const compressPk = require('../help/compress_pk') + +const generateKeyPair = promisify(crypto.generateKeyPair) +const generateSecretKey = promisify(crypto.generateKey) + +async function generateKey(purpose) { + switch (purpose) { + case 'local': + return generateSecretKey('aes', { length: 256 }) + case 'public': { + const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-384' }) + return privateKey + } + default: + throw new PasetoNotSupported('unsupported v3 purpose') + } +} + +function bytesToKeyObject(bytes) { + if (!Buffer.isBuffer(bytes)) { + throw new TypeError('bytes must be a Buffer') + } + + switch (bytes.byteLength) { + case 48: + return crypto.createPrivateKey({ + key: Buffer.concat([ + Buffer.from('303e0201010430', 'hex'), + bytes, + Buffer.from('a00706052b81040022', 'hex'), + ]), + format: 'der', + type: 'sec1', + }) + case 49: + if (bytes[0] !== 0x02 && bytes[0] !== 0x03) { + throw new TypeError('invalid compressed public key') + } + return crypto.createPublicKey({ + key: Buffer.concat([ + Buffer.from('3046301006072a8648ce3d020106052b81040022033200', 'hex'), + bytes, + ]), + format: 'der', + type: 'spki', + }) + case 97: + if (bytes[0] !== 0x04) { + throw new TypeError('invalid uncompressed public key') + } + return crypto.createPublicKey({ + key: Buffer.concat([ + Buffer.from('3076301006072a8648ce3d020106052b81040022036200', 'hex'), + bytes, + ]), + format: 'der', + type: 'spki', + }) + default: + throw new TypeError( + 'bytes must be 48 bytes (private key), 49 bytes (compressed public key), or 97 bytes (uncompressed public key)', + ) + } +} + +function keyObjectToBytes(keyObject) { + if (!isKeyObject(keyObject)) { + throw new TypeError('keyObject must be a KeyObject instance') + } + if ( + keyObject.type === 'secret' || + keyObject.asymmetricKeyType !== 'ec' || + keyObject.asymmetricKeyDetails.namedCurve !== 'secp384r1' + ) { + throw new TypeError('v3.public key must be an EC P-384 key') + } + switch (keyObject.type) { + case 'public': + return compressPk(keyObject) + case 'private': + return Buffer.from(keyObject.export({ format: 'jwk' }).d, 'base64') + } +} + +module.exports = { + generateKey, + bytesToKeyObject, + keyObjectToBytes, +} diff --git a/lib/v3/sign.js b/lib/v3/sign.js new file mode 100644 index 0000000..957173c --- /dev/null +++ b/lib/v3/sign.js @@ -0,0 +1,44 @@ +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 {} + } + + if (!isKeyObject(key)) { + throw new TypeError('invalid key provided') + } + + if ( + key.type !== 'private' || + key.asymmetricKeyType !== 'ec' || + key.asymmetricKeyDetails.namedCurve !== 'secp384r1' + ) { + throw new TypeError('v3.public signing key must be a private EC P-384 key') + } + + return key +} + +module.exports = async function v3Sign(payload, key, { footer, assertion, ...options } = {}) { + const m = checkPayload(payload, options) + const f = checkFooter(footer) + const i = checkAssertion(assertion) + key = checkKey(key) + return sign('v3.public.', m, f, 'sha384', { key, dsaEncoding: 'ieee-p1363' }, i) +} diff --git a/lib/v3/verify.js b/lib/v3/verify.js new file mode 100644 index 0000000..5c4eab8 --- /dev/null +++ b/lib/v3/verify.js @@ -0,0 +1,74 @@ +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 !== 'ec' || + key.asymmetricKeyDetails.namedCurve !== 'secp384r1' + ) { + throw new TypeError('v3.public verify key must be a public EC P-384 key') + } + + return key +} + +module.exports = async function v3Verify( + token, + key, + { complete = false, buffer = false, assertion, ...options } = {}, +) { + key = checkKey(key) + const i = checkAssertion(assertion) + + const { m, footer } = await verify( + 'v3.public.', + token, + 'sha384', + 96, + { key, dsaEncoding: 'ieee-p1363' }, + 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: 'v3', purpose: 'public' } + } + + return m + } + + const payload = parse(m) + assertPayload(options, payload) + + if (complete) { + return { payload, footer, version: 'v3', purpose: 'public' } + } + + return payload +} diff --git a/lib/v4/index.js b/lib/v4/index.js new file mode 100644 index 0000000..fafb73e --- /dev/null +++ b/lib/v4/index.js @@ -0,0 +1,5 @@ +const sign = require('./sign') +const verify = require('./verify') +const { generateKey, bytesToKeyObject, keyObjectToBytes } = require('./key') + +module.exports = { sign, verify, generateKey, bytesToKeyObject, keyObjectToBytes } diff --git a/lib/v4/key.js b/lib/v4/key.js new file mode 100644 index 0000000..31a8e0e --- /dev/null +++ b/lib/v4/key.js @@ -0,0 +1,15 @@ +const { _generateKey, _keyObjectToBytes, bytesToKeyObject } = require('../v2/key') + +async function generateKey(...args) { + return _generateKey('v4', ...args) +} + +function keyObjectToBytes(...args) { + return _keyObjectToBytes('v4', ...args) +} + +module.exports = { + generateKey, + bytesToKeyObject, + keyObjectToBytes, +} diff --git a/lib/v4/sign.js b/lib/v4/sign.js new file mode 100644 index 0000000..dfd60cc --- /dev/null +++ b/lib/v4/sign.js @@ -0,0 +1,40 @@ +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 {} + } + + 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 +} + +module.exports = async function v4Sign(payload, key, { footer, assertion, ...options } = {}) { + const m = checkPayload(payload, options) + const i = checkAssertion(assertion) + key = checkKey(key) + const f = checkFooter(footer) + return sign('v4.public.', m, f, undefined, key, i) +} diff --git a/lib/v4/verify.js b/lib/v4/verify.js new file mode 100644 index 0000000..618a9fe --- /dev/null +++ b/lib/v4/verify.js @@ -0,0 +1,63 @@ +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') + } + + return key +} + +module.exports = async function v4Verify( + token, + key, + { complete = false, buffer = false, assertion, ...options } = {}, +) { + key = checkKey(key) + const i = checkAssertion(assertion) + + 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 +} diff --git a/package.json b/package.json index 502e5e1..b1fd048 100644 --- a/package.json +++ b/package.json @@ -12,39 +12,39 @@ "sign", "v1", "v2", + "v3", + "v4", "verify" ], "homepage": "https://github.com/panva/paseto", "repository": "panva/paseto", + "funding": "https://github.com/sponsors/panva", "license": "MIT", "author": "Filip Skokan ", + "main": "lib/index.js", + "types": "types/index.d.ts", "files": [ "lib", "types/index.d.ts" ], - "funding": "https://github.com/sponsors/panva", - "main": "lib/index.js", - "types": "types/index.d.ts", "scripts": { "coverage": "c8 ava", - "lint": "standard", "lint-ts": "npx typescript@~3.6.0 --build types", - "lint-fix": "standard --fix", "test": "ava", "watch": "ava --watch" }, + "ava": { + "files": [ + "test/**/*.test.js" + ] + }, "devDependencies": { - "@types/node": "^14.14.31", + "@types/node": "^16.4.0", "ava": "^3.15.0", "c8": "^7.6.0", - "standard": "^16.0.3" + "sinon": "^11.1.2" }, "engines": { - "node": "^12.19.0 || >=14.15.0" - }, - "ava": { - "files": [ - "test/**/*.test.js" - ] + "node": ">=16.0.0" } } diff --git a/test/apply_options.test.js b/test/apply_options.test.js index 40a68b9..61e81e2 100644 --- a/test/apply_options.test.js +++ b/test/apply_options.test.js @@ -2,33 +2,32 @@ const test = require('ava') const applyOptions = require('../lib/help/apply_options') -test('options.iat must be a boolean', t => { - t.throws( - () => applyOptions({ iat: 1 }, {}), - { instanceOf: TypeError, message: 'options.iat must be a boolean' } - ) +test('options.iat must be a boolean', (t) => { + t.throws(() => applyOptions({ iat: 1 }, {}), { + instanceOf: TypeError, + message: 'options.iat must be a boolean', + }) }) -test('now not a Date object', t => { - t.throws( - () => applyOptions({ now: 1 }, {}), - { instanceOf: TypeError, message: 'options.now must be a valid Date object' } - ) +test('now not a Date object', (t) => { + t.throws(() => applyOptions({ now: 1 }, {}), { + instanceOf: TypeError, + message: 'options.now must be a valid Date object', + }) }) -test('now not a valid Date object', t => { - t.throws( - () => applyOptions({ now: 'foo' }, {}), - { instanceOf: TypeError, message: 'options.now must be a valid Date object' } - ) +test('now not a valid Date object', (t) => { + t.throws(() => applyOptions({ now: 'foo' }, {}), { + instanceOf: TypeError, + message: 'options.now must be a valid Date object', + }) }) - ;['expiresIn', 'notBefore', 'audience', 'issuer', 'subject', 'kid', 'jti'].forEach((option) => { - test(`options.${option} must be a string`, t => { - t.throws( - () => applyOptions({ [option]: 1 }, {}), - { instanceOf: TypeError, message: `options.${option} must be a string` } - ) + test(`options.${option} must be a string`, (t) => { + t.throws(() => applyOptions({ [option]: 1 }, {}), { + instanceOf: TypeError, + message: `options.${option} must be a string`, + }) }) }) @@ -37,21 +36,21 @@ Object.entries({ audience: 'aud', kid: 'kid', jti: 'jti', - subject: 'sub' + subject: 'sub', }).forEach(([option, claim]) => { - test(`options.${option} puts a ${claim} in the payload`, t => { + test(`options.${option} puts a ${claim} in the payload`, (t) => { t.deepEqual(applyOptions({ [option]: 'value', iat: false }, {}), { [claim]: 'value' }) }) }) -test('defaults', t => { +test('defaults', (t) => { t.true('iat' in applyOptions({}, {})) }) -test('expiresIn', t => { +test('expiresIn', (t) => { t.true('exp' in applyOptions({ expiresIn: '1d' }, {})) }) -test('notBefore', t => { +test('notBefore', (t) => { t.true('nbf' in applyOptions({ notBefore: '1d' }, {})) }) diff --git a/test/assert_payload.test.js b/test/assert_payload.test.js index 2a2511c..7c93b26 100644 --- a/test/assert_payload.test.js +++ b/test/assert_payload.test.js @@ -4,210 +4,247 @@ const ms = require('../lib/help/ms') const errors = require('../lib/errors') const assertPayload = require('../lib/help/assert_payload') -test('now not a Date object', t => { - t.throws( - () => assertPayload({ now: 1 }, {}), - { instanceOf: TypeError, message: 'options.now must be a valid Date object' } - ) +test('now not a Date object', (t) => { + t.throws(() => assertPayload({ now: 1 }, {}), { + instanceOf: TypeError, + message: 'options.now must be a valid Date object', + }) }) -test('now not a valid Date object', t => { - t.throws( - () => assertPayload({ now: 'foo' }, {}), - { instanceOf: TypeError, message: 'options.now must be a valid Date object' } - ) +test('now not a valid Date object', (t) => { + t.throws(() => assertPayload({ now: 'foo' }, {}), { + instanceOf: TypeError, + message: 'options.now must be a valid Date object', + }) }) Object.entries({ issuer: 'iss', audience: 'aud', - subject: 'sub' + subject: 'sub', }).forEach(([option, claim]) => { - test(`options.${option} must be a string`, t => { - t.throws( - () => assertPayload({ [option]: 1 }, {}), - { instanceOf: TypeError, message: `options.${option} must be a string` } - ) + test(`options.${option} must be a string`, (t) => { + t.throws(() => assertPayload({ [option]: 1 }, {}), { + instanceOf: TypeError, + message: `options.${option} must be a string`, + }) }) - test(`${option} mismatch`, t => { - t.throws( - () => assertPayload({ [option]: 'foo' }, {}), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: `${option} mismatch` } - ) + test(`${option} mismatch`, (t) => { + t.throws(() => assertPayload({ [option]: 'foo' }, {}), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: `${option} mismatch`, + }) }) - test(`${option} passes`, t => { + test(`${option} passes`, (t) => { t.notThrows(() => assertPayload({ [option]: 'foo' }, { [claim]: 'foo' })) }) - test(`payload.${claim} must be a string`, t => { - t.throws( - () => assertPayload({}, { [claim]: 1 }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: `payload.${claim} must be a string` } - ) + test(`payload.${claim} must be a string`, (t) => { + t.throws(() => assertPayload({}, { [claim]: 1 }), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: `payload.${claim} must be a string`, + }) }) }) - ;['iat', 'exp', 'nbf'].forEach((claim) => { - test(`payload.${claim} must be a string`, t => { - t.throws( - () => assertPayload({}, { [claim]: 1 }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: `payload.${claim} must be a string` } - ) + test(`payload.${claim} must be a string`, (t) => { + t.throws(() => assertPayload({}, { [claim]: 1 }), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: `payload.${claim} must be a string`, + }) }) - test(`payload.${claim} must be an ISO8601 string`, t => { - t.throws( - () => assertPayload({}, { [claim]: 'foo' }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: `payload.${claim} must be a valid ISO8601 string` } - ) + test(`payload.${claim} must be an ISO8601 string`, (t) => { + t.throws(() => assertPayload({}, { [claim]: 'foo' }), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: `payload.${claim} must be a valid ISO8601 string`, + }) }) }) -test('iat in the future', t => { - t.throws( - () => assertPayload({ }, { iat: new Date(Date.now() + 1000).toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'token issued in the future' } - ) +test('iat in the future', (t) => { + t.throws(() => assertPayload({}, { iat: new Date(Date.now() + 1000).toISOString() }), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'token issued in the future', + }) }) -test('iat in the future (ignoreIat)', t => { - t.notThrows( - () => assertPayload({ ignoreIat: true }, { iat: new Date(Date.now() + 1000).toISOString() }) +test('iat in the future (ignoreIat)', (t) => { + t.notThrows(() => + assertPayload({ ignoreIat: true }, { iat: new Date(Date.now() + 1000).toISOString() }), ) }) -test('iat in the future (clockTolerance)', t => { +test('iat in the future (clockTolerance)', (t) => { const now = new Date() - t.notThrows( - () => assertPayload({ now, clockTolerance: '1s' }, { iat: new Date(now.getTime() + 1000).toISOString() }) + t.notThrows(() => + assertPayload( + { now, clockTolerance: '1s' }, + { iat: new Date(now.getTime() + 1000).toISOString() }, + ), ) }) -test('iat in the future (clockTolerance not enough)', t => { +test('iat in the future (clockTolerance not enough)', (t) => { const now = new Date() t.throws( - () => assertPayload({ now, clockTolerance: '1s' }, { iat: new Date(now.getTime() + 1001).toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'token issued in the future' } + () => + assertPayload( + { now, clockTolerance: '1s' }, + { iat: new Date(now.getTime() + 1001).toISOString() }, + ), + { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'token issued in the future', + }, ) }) -test('iat in the past', t => { - t.notThrows( - () => assertPayload({ }, { iat: new Date(Date.now() - 1000).toISOString() }) - ) +test('iat in the past', (t) => { + t.notThrows(() => assertPayload({}, { iat: new Date(Date.now() - 1000).toISOString() })) }) -test('iat exactly now', t => { +test('iat exactly now', (t) => { const now = new Date() - t.notThrows( - () => assertPayload({ now }, { iat: now.toISOString() }) - ) + t.notThrows(() => assertPayload({ now }, { iat: now.toISOString() })) }) -test('exp in the past (ignoreExp)', t => { - t.notThrows( - () => assertPayload({ ignoreExp: true }, { exp: new Date(Date.now() - 1000).toISOString() }) +test('exp in the past (ignoreExp)', (t) => { + t.notThrows(() => + assertPayload({ ignoreExp: true }, { exp: new Date(Date.now() - 1000).toISOString() }), ) }) -test('exp in the past (clockTolerance)', t => { +test('exp in the past (clockTolerance)', (t) => { const now = new Date() - t.notThrows( - () => assertPayload({ now, clockTolerance: '1s' }, { exp: new Date(now.getTime() - 999).toISOString() }) + t.notThrows(() => + assertPayload( + { now, clockTolerance: '1s' }, + { exp: new Date(now.getTime() - 999).toISOString() }, + ), ) }) -test('exp in the past (clockTolerance not enough)', t => { +test('exp in the past (clockTolerance not enough)', (t) => { const now = new Date() t.throws( - () => assertPayload({ now, clockTolerance: '1s' }, { exp: new Date(now.getTime() - 1001).toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'token is expired' } + () => + assertPayload( + { now, clockTolerance: '1s' }, + { exp: new Date(now.getTime() - 1001).toISOString() }, + ), + { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'token is expired', + }, ) }) -test('exp in the future', t => { - t.notThrows( - () => assertPayload({ }, { exp: new Date(Date.now() + 1000).toISOString() }) - ) +test('exp in the future', (t) => { + t.notThrows(() => assertPayload({}, { exp: new Date(Date.now() + 1000).toISOString() })) }) -test('exp exactly now', t => { +test('exp exactly now', (t) => { const now = new Date() - t.throws( - () => assertPayload({ now }, { exp: now.toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'token is expired' } - ) + t.throws(() => assertPayload({ now }, { exp: now.toISOString() }), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'token is expired', + }) }) -test('nbf in the future (ignoreNbf)', t => { - t.notThrows( - () => assertPayload({ ignoreNbf: true }, { nbf: new Date(Date.now() + 1000).toISOString() }) +test('nbf in the future (ignoreNbf)', (t) => { + t.notThrows(() => + assertPayload({ ignoreNbf: true }, { nbf: new Date(Date.now() + 1000).toISOString() }), ) }) -test('nbf in the future (clockTolerance)', t => { +test('nbf in the future (clockTolerance)', (t) => { const now = new Date() - t.notThrows( - () => assertPayload({ now, clockTolerance: '1s' }, { nbf: new Date(now.getTime() + 1000).toISOString() }) + t.notThrows(() => + assertPayload( + { now, clockTolerance: '1s' }, + { nbf: new Date(now.getTime() + 1000).toISOString() }, + ), ) }) -test('nbf in the future (clockTolerance not enough)', t => { +test('nbf in the future (clockTolerance not enough)', (t) => { const now = new Date() t.throws( - () => assertPayload({ now, clockTolerance: '1s' }, { nbf: new Date(now.getTime() + 1001).toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'token is not active yet' } + () => + assertPayload( + { now, clockTolerance: '1s' }, + { nbf: new Date(now.getTime() + 1001).toISOString() }, + ), + { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'token is not active yet', + }, ) }) -test('nbf in the past', t => { - t.notThrows( - () => assertPayload({ }, { nbf: new Date(Date.now() - 1000).toISOString() }) - ) +test('nbf in the past', (t) => { + t.notThrows(() => assertPayload({}, { nbf: new Date(Date.now() - 1000).toISOString() })) }) -test('nbf exactly now', t => { +test('nbf exactly now', (t) => { const now = new Date() - t.notThrows( - () => assertPayload({ now }, { nbf: now.toISOString() }) - ) + t.notThrows(() => assertPayload({ now }, { nbf: now.toISOString() })) }) -test('clockTolerance must be a string', t => { - t.throws( - () => assertPayload({ clockTolerance: 1 }, {}), - { instanceOf: TypeError, message: 'options.clockTolerance must be a string' } - ) +test('clockTolerance must be a string', (t) => { + t.throws(() => assertPayload({ clockTolerance: 1 }, {}), { + instanceOf: TypeError, + message: 'options.clockTolerance must be a string', + }) }) -test('blank payload, defaults', t => { +test('blank payload, defaults', (t) => { assertPayload({}, {}) t.pass() }) -test('blank payload, maxTokenAge', t => { - t.throws( - () => assertPayload({ maxTokenAge: 1 }, {}), - { instanceOf: TypeError, message: 'options.maxTokenAge must be a string' } - ) +test('blank payload, maxTokenAge', (t) => { + t.throws(() => assertPayload({ maxTokenAge: 1 }, {}), { + instanceOf: TypeError, + message: 'options.maxTokenAge must be a string', + }) }) -test('payload missing iat, maxTokenAge', t => { - t.throws( - () => assertPayload({ maxTokenAge: '1d' }, {}), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'missing iat claim' } - ) +test('payload missing iat, maxTokenAge', (t) => { + t.throws(() => assertPayload({ maxTokenAge: '1d' }, {}), { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'missing iat claim', + }) }) -test('maxTokenAge passed', t => { +test('maxTokenAge passed', (t) => { t.notThrows(() => assertPayload({ maxTokenAge: '1d' }, { iat: new Date().toISOString() })) }) -test('maxTokenAge exceeded', t => { +test('maxTokenAge exceeded', (t) => { t.throws( - () => assertPayload({ maxTokenAge: '59m' }, { iat: new Date(Date.now() - ms('60m')).toISOString() }), - { instanceOf: errors.PasetoClaimInvalid, code: 'ERR_PASETO_CLAIM_INVALID', message: 'maxTokenAge exceeded' } + () => + assertPayload( + { maxTokenAge: '59m' }, + { iat: new Date(Date.now() - ms('60m')).toISOString() }, + ), + { + instanceOf: errors.PasetoClaimInvalid, + code: 'ERR_PASETO_CLAIM_INVALID', + message: 'maxTokenAge exceeded', + }, ) }) diff --git a/test/check_assertion.test.js b/test/check_assertion.test.js new file mode 100644 index 0000000..e727435 --- /dev/null +++ b/test/check_assertion.test.js @@ -0,0 +1,10 @@ +const test = require('ava') + +const checkAssertion = require('../lib/help/check_assertion') + +test('when not a buffer, or a string', (t) => { + t.throws(() => checkAssertion(1), { + instanceOf: TypeError, + message: 'options.assertion must be a string, or a Buffer', + }) +}) diff --git a/test/check_footer.test.js b/test/check_footer.test.js index bb118b7..4821b8a 100644 --- a/test/check_footer.test.js +++ b/test/check_footer.test.js @@ -2,9 +2,9 @@ const test = require('ava') const checkFooter = require('../lib/help/check_footer') -test('when not a buffer, string or an object', t => { - t.throws( - () => checkFooter(1), - { instanceOf: TypeError, message: 'options.footer must be a string, Buffer, or a plain object' } - ) +test('when not a buffer, string or an object', (t) => { + t.throws(() => checkFooter(1), { + instanceOf: TypeError, + message: 'options.footer must be a string, Buffer, or a plain object', + }) }) diff --git a/test/generic/decode.test.js b/test/generic/decode.test.js index 31a0926..ae678e9 100644 --- a/test/generic/decode.test.js +++ b/test/generic/decode.test.js @@ -2,57 +2,62 @@ const test = require('ava') const { decode, errors } = require('../../lib') -test('decode input must be a string', t => { - t.throws( - () => decode(1), - { instanceOf: TypeError, message: 'token must be a string' } - ) +test('decode input must be a string', (t) => { + t.throws(() => decode(1), { instanceOf: TypeError, message: 'token must be a string' }) }) -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' } - ) - t.throws( - () => decode('....'), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'token value is not a PASETO formatted value' } - ) +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', + }) + t.throws(() => decode('....'), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'token value is not a PASETO formatted value', + }) }) -test('decode must be a supported header', t => { - t.throws( - () => decode('v3..'), - { instanceOf: errors.PasetoNotSupported, code: 'ERR_PASETO_NOT_SUPPORTED', message: 'unsupported PASETO version' } - ) - t.throws( - () => decode('v2.foo.'), - { instanceOf: errors.PasetoNotSupported, code: 'ERR_PASETO_NOT_SUPPORTED', message: 'unsupported PASETO purpose' } - ) +test('decode must be a supported header', (t) => { + t.throws(() => decode('v0..'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported PASETO version', + }) + t.throws(() => decode('v2.foo.'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported PASETO purpose', + }) }) -test('parses the payload', t => { +test('parses the payload', (t) => { t.deepEqual( - decode('v2.public.eyJpYXQiOiIyMDE5LTA3LTAyVDEyOjEwOjE1LjMxNloifWqI1SxVOBO_wrYAonuNSr84VxkOgMZf4Jn1mVUXsz9lEhxY7TdoIbgfToHIBtsrK5BUW5DD3t8ebyLz6z718gY.Zm9v'), + decode( + 'v2.public.eyJpYXQiOiIyMDE5LTA3LTAyVDEyOjEwOjE1LjMxNloifWqI1SxVOBO_wrYAonuNSr84VxkOgMZf4Jn1mVUXsz9lEhxY7TdoIbgfToHIBtsrK5BUW5DD3t8ebyLz6z718gY.Zm9v', + ), { footer: Buffer.from('foo'), payload: { - iat: '2019-07-02T12:10:15.316Z' + iat: '2019-07-02T12:10:15.316Z', }, purpose: 'public', - version: 'v2' - } + version: 'v2', + }, ) }) -test('skips parsing the payload for local tokens', t => { +test('skips parsing the payload for local tokens', (t) => { t.deepEqual( - decode('v2.local.eyJpYXQiOiIyMDE5LTA3LTAyVDEyOjA4OjI1LjE5OVoifS3L7e7t5YUwI1uKlW15mNNCI-aJYDDOJyEEe567xxYTcMJ4vD4MJuJiTEd1buIMQ0JoBk6kZzyknKNZKa57dg8.Zm9v'), + decode( + 'v2.local.eyJpYXQiOiIyMDE5LTA3LTAyVDEyOjA4OjI1LjE5OVoifS3L7e7t5YUwI1uKlW15mNNCI-aJYDDOJyEEe567xxYTcMJ4vD4MJuJiTEd1buIMQ0JoBk6kZzyknKNZKa57dg8.Zm9v', + ), { footer: Buffer.from('foo'), payload: undefined, purpose: 'local', - version: 'v2' - } + version: 'v2', + }, ) }) diff --git a/test/generic/generate_key.test.js b/test/generic/generate_key.test.js index 7154b04..e5fe1d0 100644 --- a/test/generic/generate_key.test.js +++ b/test/generic/generate_key.test.js @@ -1,29 +1,59 @@ const test = require('ava') -const { V1, V2, errors } = require('../../lib') +const { V1, V2, V3, V4, errors } = require('../../lib') -test('V1 generateKey generates local', async t => { +test('V1 generateKey generates local', async (t) => { await t.notThrowsAsync(V1.generateKey('local')) }) -test('V1 generateKey generates public', async t => { +test('V1 generateKey generates public', async (t) => { await t.notThrowsAsync(V1.generateKey('public')) }) -test('V1 generateKey handles invalid purposes', async t => { - await t.throwsAsync( - V1.generateKey('foo'), - { instanceOf: errors.PasetoNotSupported, code: 'ERR_PASETO_NOT_SUPPORTED', message: 'unsupported v1 purpose' } - ) +test('V1 generateKey handles invalid purposes', async (t) => { + await t.throwsAsync(V1.generateKey('foo'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported v1 purpose', + }) }) -test('V2 generateKey generates public', async t => { +test('V2 generateKey generates public', async (t) => { await t.notThrowsAsync(V2.generateKey('public')) }) -test('V2 generateKey handles invalid purposes', async t => { - await t.throwsAsync( - V2.generateKey('foo'), - { instanceOf: errors.PasetoNotSupported, code: 'ERR_PASETO_NOT_SUPPORTED', message: 'unsupported v2 purpose' } - ) +test('V2 generateKey handles invalid purposes', async (t) => { + await t.throwsAsync(V2.generateKey('foo'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported v2 purpose', + }) +}) + +test('V3 generateKey generates local', async (t) => { + await t.notThrowsAsync(V3.generateKey('local')) +}) + +test('V3 generateKey generates public', async (t) => { + await t.notThrowsAsync(V3.generateKey('public')) +}) + +test('V3 generateKey handles invalid purposes', async (t) => { + await t.throwsAsync(V3.generateKey('foo'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported v3 purpose', + }) +}) + +test('V4 generateKey generates public', async (t) => { + await t.notThrowsAsync(V4.generateKey('public')) +}) + +test('V4 generateKey handles invalid purposes', async (t) => { + await t.throwsAsync(V4.generateKey('foo'), { + instanceOf: errors.PasetoNotSupported, + code: 'ERR_PASETO_NOT_SUPPORTED', + message: 'unsupported v4 purpose', + }) }) diff --git a/test/local/v1.test.js b/test/local/v1.test.js index cf4669b..748e8f7 100644 --- a/test/local/v1.test.js +++ b/test/local/v1.test.js @@ -1,19 +1,21 @@ const test = require('ava') const crypto = require('crypto') -const { V1: { encrypt, decrypt, generateKey }, errors } = require('../../lib') +const { + V1: { encrypt, decrypt, generateKey }, + errors, +} = require('../../lib') ;[Buffer.from('foo'), 'foo', { kid: 'foo' }].forEach((footer) => { - test(`footer can be a ${Buffer.isBuffer(footer) ? 'Buffer' : typeof footer}`, async t => { + test(`footer can be a ${Buffer.isBuffer(footer) ? 'Buffer' : typeof footer}`, async (t) => { const key = await generateKey('local') const paseto = await encrypt({}, key, { footer }) ;({ footer } = await decrypt(paseto, key, { complete: true })) t.true(Buffer.isBuffer(footer)) }) }) - ;[Buffer.from('foo'), { kid: 'foo' }].forEach((payload) => { - test(`payload can be a ${Buffer.isBuffer(payload) ? 'Buffer' : typeof payload}`, async t => { + test(`payload can be a ${Buffer.isBuffer(payload) ? 'Buffer' : typeof payload}`, async (t) => { const key = await generateKey('local') const paseto = await encrypt(payload, key) ;({ payload } = await decrypt(paseto, key, { complete: true, buffer: true })) @@ -21,22 +23,19 @@ const { V1: { encrypt, decrypt, generateKey }, errors } = require('../../lib') }) }) -test('decryption failed', async t => { - const [k1, k2] = await Promise.all([ - generateKey('local'), - generateKey('local') - ]) +test('decryption failed', async (t) => { + const [k1, k2] = await Promise.all([generateKey('local'), generateKey('local')]) const paseto = await encrypt({}, k1) await t.throwsAsync(() => decrypt(paseto, k2), { message: 'decryption failed', instanceOf: errors.PasetoDecryptionFailed, - code: 'ERR_PASETO_DECRYPTION_FAILED' + code: 'ERR_PASETO_DECRYPTION_FAILED', }) }) -test('not a v1.local paseto', async t => { +test('not a v1.local paseto', async (t) => { const key = await generateKey('local') let paseto = await encrypt({}, key) @@ -45,17 +44,17 @@ test('not a v1.local paseto', async t => { await t.throwsAsync(() => decrypt(paseto, key), { message: 'token is not a v1.local PASETO', instanceOf: errors.PasetoInvalid, - code: 'ERR_PASETO_INVALID' + code: 'ERR_PASETO_INVALID', }) await t.throwsAsync(() => decrypt('foobar', key), { message: 'token is not a v1.local PASETO', instanceOf: errors.PasetoInvalid, - code: 'ERR_PASETO_INVALID' + code: 'ERR_PASETO_INVALID', }) }) -test('invalid paseto', async t => { +test('invalid paseto', async (t) => { const key = await generateKey('local') const token = `${await encrypt({}, key, { footer: 'foo' })}.foo` @@ -63,68 +62,68 @@ test('invalid paseto', async t => { await t.throwsAsync(() => decrypt(token, key), { message: 'token value is not a PASETO formatted value', instanceOf: errors.PasetoInvalid, - code: 'ERR_PASETO_INVALID' + code: 'ERR_PASETO_INVALID', }) await t.throwsAsync(() => decrypt(3.12, key), { message: 'token must be a string, got: number', - instanceOf: TypeError + instanceOf: TypeError, }) }) -test('invalid key length', async t => { +test('invalid key length', async (t) => { const key = crypto.randomBytes(64) await t.throwsAsync(() => encrypt({}, key), { message: 'v1.local secret key must be 32 bytes long symmetric key', - instanceOf: TypeError + instanceOf: TypeError, }) }) -test('invalid key type', async t => { +test('invalid key type', async (t) => { const { privateKey } = crypto.generateKeyPairSync('ed25519') privateKey.symmetricKeySize = 32 await t.throwsAsync(() => encrypt({}, privateKey), { message: 'v1.local secret key must be 32 bytes long symmetric key', - instanceOf: TypeError + instanceOf: TypeError, }) }) -test('invalid payload', async t => { +test('invalid payload', async (t) => { const key = await generateKey('local') await t.throwsAsync(() => encrypt(1, key), { message: 'payload must be a Buffer or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) await t.throwsAsync(() => encrypt('foo', key), { message: 'payload must be a Buffer or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) class Foo {} await t.throwsAsync(() => encrypt(new Foo(), key), { message: 'payload must be a Buffer or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) await t.throwsAsync(() => encrypt([], key), { message: 'payload must be a Buffer or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) }) -test('invalid footer', async t => { +test('invalid footer', async (t) => { const key = await generateKey('local') await t.throwsAsync(() => encrypt({}, key, { footer: 1 }), { message: 'options.footer must be a string, Buffer, or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) class Foo {} await t.throwsAsync(() => encrypt({}, key, { footer: new Foo() }), { message: 'options.footer must be a string, Buffer, or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) await t.throwsAsync(() => encrypt({}, key, { footer: [] }), { message: 'options.footer must be a string, Buffer, or a plain object', - instanceOf: TypeError + instanceOf: TypeError, }) }) diff --git a/test/local/v3.test.js b/test/local/v3.test.js new file mode 100644 index 0000000..dc79d66 --- /dev/null +++ b/test/local/v3.test.js @@ -0,0 +1,129 @@ +const test = require('ava') +const crypto = require('crypto') + +const { + V3: { encrypt, decrypt, generateKey }, + errors, +} = require('../../lib') + +;[Buffer.from('foo'), 'foo', { kid: 'foo' }].forEach((footer) => { + test(`footer can be a ${Buffer.isBuffer(footer) ? 'Buffer' : typeof footer}`, async (t) => { + const key = await generateKey('local') + const paseto = await encrypt({}, key, { footer }) + ;({ footer } = await decrypt(paseto, key, { complete: true })) + t.true(Buffer.isBuffer(footer)) + }) +}) +;[Buffer.from('foo'), { kid: 'foo' }].forEach((payload) => { + test(`payload can be a ${Buffer.isBuffer(payload) ? 'Buffer' : typeof payload}`, async (t) => { + const key = await generateKey('local') + const paseto = await encrypt(payload, key) + ;({ payload } = await decrypt(paseto, key, { complete: true, buffer: true })) + t.true(Buffer.isBuffer(payload)) + }) +}) + +test('decryption failed', async (t) => { + const [k1, k2] = await Promise.all([generateKey('local'), generateKey('local')]) + + const paseto = await encrypt({}, k1) + + await t.throwsAsync(() => decrypt(paseto, k2), { + message: 'decryption failed', + instanceOf: errors.PasetoDecryptionFailed, + code: 'ERR_PASETO_DECRYPTION_FAILED', + }) +}) + +test('not a v3.local paseto', async (t) => { + const key = await generateKey('local') + + let paseto = await encrypt({}, key) + paseto = paseto.replace('v3.local', 'v2.local') + + await t.throwsAsync(() => decrypt(paseto, key), { + message: 'token is not a v3.local PASETO', + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + }) + + await t.throwsAsync(() => decrypt('foobar', key), { + message: 'token is not a v3.local PASETO', + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + }) +}) + +test('invalid paseto', async (t) => { + const key = await generateKey('local') + + const token = `${await encrypt({}, key, { footer: 'foo' })}.foo` + + await t.throwsAsync(() => decrypt(token, key), { + message: 'token value is not a PASETO formatted value', + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + }) + + await t.throwsAsync(() => decrypt(3.12, key), { + message: 'token must be a string, got: number', + instanceOf: TypeError, + }) +}) + +test('invalid key length', async (t) => { + const key = crypto.randomBytes(64) + + await t.throwsAsync(() => encrypt({}, key), { + message: 'v3.local secret key must be 32 bytes long symmetric key', + instanceOf: TypeError, + }) +}) + +test('invalid key type', async (t) => { + const { privateKey } = crypto.generateKeyPairSync('ed25519') + privateKey.symmetricKeySize = 32 + + await t.throwsAsync(() => encrypt({}, privateKey), { + message: 'v3.local secret key must be 32 bytes long symmetric key', + instanceOf: TypeError, + }) +}) + +test('invalid payload', async (t) => { + const key = await generateKey('local') + await t.throwsAsync(() => encrypt(1, key), { + message: 'payload must be a Buffer or a plain object', + instanceOf: TypeError, + }) + await t.throwsAsync(() => encrypt('foo', key), { + message: 'payload must be a Buffer or a plain object', + instanceOf: TypeError, + }) + class Foo {} + await t.throwsAsync(() => encrypt(new Foo(), key), { + message: 'payload must be a Buffer or a plain object', + instanceOf: TypeError, + }) + await t.throwsAsync(() => encrypt([], key), { + message: 'payload must be a Buffer or a plain object', + instanceOf: TypeError, + }) +}) + +test('invalid footer', async (t) => { + const key = await generateKey('local') + await t.throwsAsync(() => encrypt({}, key, { footer: 1 }), { + message: 'options.footer must be a string, Buffer, or a plain object', + instanceOf: TypeError, + }) + class Foo {} + await t.throwsAsync(() => encrypt({}, key, { footer: new Foo() }), { + message: 'options.footer must be a string, Buffer, or a plain object', + instanceOf: TypeError, + }) + await t.throwsAsync(() => encrypt({}, key, { footer: [] }), { + message: 'options.footer must be a string, Buffer, or a plain object', + instanceOf: TypeError, + }) +}) diff --git a/test/ms.test.js b/test/ms.test.js index 633f591..5a1768a 100644 --- a/test/ms.test.js +++ b/test/ms.test.js @@ -28,31 +28,34 @@ const values = { years: 31557600000, yr: 31557600000, yrs: 31557600000, - y: 31557600000 + y: 31557600000, } -test('invalid formats', t => { +test('invalid formats', (t) => { ;['-1w', '2.2.w', '2.w', '2.', '', '2 w ', ' 2w'].forEach((val) => { - t.throws(() => { - secs(val) - }, { instanceOf: TypeError }) + t.throws( + () => { + secs(val) + }, + { instanceOf: TypeError }, + ) }) }) Object.entries(values).forEach(([unit, value]) => { - test(`0 ${unit}`, t => { + test(`0 ${unit}`, (t) => { t.is(0, secs(`0 ${unit}`)) }) - test(`1 ${unit}`, t => { + test(`1 ${unit}`, (t) => { t.is(value, secs(`1 ${unit}`)) }) - test(`2${unit}`, t => { + test(`2${unit}`, (t) => { t.is(2 * value, secs(`2${unit}`)) }) - test(`2.5${unit}`, t => { + test(`2.5${unit}`, (t) => { t.is(Math.round(2.5 * value), secs(`2.5${unit}`)) }) }) diff --git a/test/parse_paseto_payload.test.js b/test/parse_paseto_payload.test.js index 0340225..e6d26aa 100644 --- a/test/parse_paseto_payload.test.js +++ b/test/parse_paseto_payload.test.js @@ -3,18 +3,20 @@ const test = require('ava') const parse = require('../lib/help/parse_paseto_payload') const errors = require('../lib/errors') -test('not a valid JSON', t => { - t.throws( - () => parse("{''}"), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'All PASETO payloads MUST be a JSON object' } - ) +test('not a valid JSON', (t) => { + t.throws(() => parse("{''}"), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) -test('top level is not an object', t => { +test('top level is not an object', (t) => { ;[1, true, false, null, []].forEach((value) => { - t.throws( - () => parse(JSON.stringify), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'All PASETO payloads MUST be a JSON object' } - ) + t.throws(() => parse(JSON.stringify), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) }) diff --git a/test/public.test.js b/test/public.test.js index 3135b29..39223b1 100644 --- a/test/public.test.js +++ b/test/public.test.js @@ -3,164 +3,217 @@ const crypto = require('crypto') const { promisify } = require('util') const generateKeyPair = promisify(crypto.generateKeyPair) -const { errors, V1, V2 } = require('../lib') +const { errors, V1, V2, V3, V4 } = require('../lib') -test('V1.sign needs a RSA key', async t => { +test('V1.sign needs a RSA key', async (t) => { const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) - return t.throwsAsync( - V1.sign({}, privateKey), - { instanceOf: TypeError, message: 'v1.public signing key must be a private RSA key' } - ) + return t.throwsAsync(V1.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v1.public signing key must be a private RSA key with 2048 bit modulus length', + }) }) -test('V1.sign needs a private key', async t => { +test('V1.sign needs a private key', async (t) => { const { publicKey } = await generateKeyPair('rsa', { modulusLength: 2048 }) - return t.throwsAsync( - V1.sign({}, publicKey), - { instanceOf: TypeError, message: 'v1.public signing key must be a private RSA key' } - ) + return t.throwsAsync(V1.sign({}, publicKey), { + instanceOf: TypeError, + message: 'v1.public signing key must be a private RSA key with 2048 bit modulus length', + }) }) -test('V2.sign needs a ed25519 key', async t => { +test('V3.sign needs an EC key', async (t) => { + const { privateKey } = await generateKeyPair('rsa', { modulusLength: 2048 }) + return t.throwsAsync(V3.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v3.public signing key must be a private EC P-384 key', + }) +}) + +test('V3.sign needs a private key', async (t) => { + const { publicKey } = await generateKeyPair('ec', { namedCurve: 'P-384' }) + return t.throwsAsync(V3.sign({}, publicKey), { + instanceOf: TypeError, + message: 'v3.public signing key must be a private EC P-384 key', + }) +}) + +test('V2.sign needs a ed25519 key', async (t) => { + const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) + return t.throwsAsync(V2.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v2.public signing key must be a private ed25519 key', + }) +}) + +test('V2.sign needs a private key', async (t) => { + const { publicKey } = await generateKeyPair('ed25519') + return t.throwsAsync(V2.sign({}, publicKey), { + instanceOf: TypeError, + message: 'v2.public signing key must be a private ed25519 key', + }) +}) + +test('V4.sign needs a ed25519 key', async (t) => { const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) - return t.throwsAsync( - V2.sign({}, privateKey), - { instanceOf: TypeError, message: 'v2.public signing key must be a private ed25519 key' } - ) + return t.throwsAsync(V4.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v4.public signing key must be a private ed25519 key', + }) }) -test('V2.sign needs a private key', async t => { +test('V4.sign needs a private key', async (t) => { const { publicKey } = await generateKeyPair('ed25519') - return t.throwsAsync( - V2.sign({}, publicKey), - { instanceOf: TypeError, message: 'v2.public signing key must be a private ed25519 key' } - ) + return t.throwsAsync(V4.sign({}, publicKey), { + instanceOf: TypeError, + message: 'v4.public signing key must be a private ed25519 key', + }) +}) + +test('V1.verify needs a RSA key', async (t) => { + const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) + return t.throwsAsync(V1.verify({}, privateKey), { + instanceOf: TypeError, + message: 'v1.public verify key must be a public RSA key with 2048 bit modulus length', + }) +}) + +test('V3.verify needs an EC key', async (t) => { + const { privateKey } = await generateKeyPair('rsa', { modulusLength: 2048 }) + return t.throwsAsync(V3.verify({}, privateKey), { + instanceOf: TypeError, + message: 'v3.public verify key must be a public EC P-384 key', + }) }) -test('V1.verify needs a RSA key', async t => { +test('V2.verify needs a ed25519 key', async (t) => { const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) - return t.throwsAsync( - V1.verify({}, privateKey), - { instanceOf: TypeError, message: 'v1.public verify key must be a public RSA key' } - ) + return t.throwsAsync(V2.verify({}, privateKey), { + instanceOf: TypeError, + message: 'v2.public verify key must be a public ed25519 key', + }) }) -test('V2.verify needs a ed25519 key', async t => { +test('V4.verify needs a ed25519 key', async (t) => { const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-256' }) - return t.throwsAsync( - V2.verify({}, privateKey), - { instanceOf: TypeError, message: 'v2.public verify key must be a public ed25519 key' } - ) + return t.throwsAsync(V4.verify({}, privateKey), { + instanceOf: TypeError, + message: 'v4.public verify key must be a public ed25519 key', + }) }) -test('token must be a string', async t => { +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' } - ) + return t.throwsAsync(V2.verify(1, k), { + instanceOf: TypeError, + message: 'token must be a string', + }) }) -test('token must be a a valid paseto', async t => { +test('token must be a a valid paseto', async (t) => { const k = await V2.generateKey('public') - return t.throwsAsync( - V2.verify('v2.public...', k), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'token value is not a PASETO formatted value' } - ) + return t.throwsAsync(V2.verify('v2.public...', k), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'token value is not a PASETO formatted value', + }) }) -test('invalid RSA key length for v1.public', async t => { +test('invalid RSA key length for v1.public', async (t) => { const { privateKey } = await generateKeyPair('rsa', { modulusLength: 1024 }) - await t.throwsAsync( - V1.sign({}, privateKey), - { instanceOf: TypeError, message: 'invalid v1.public signing key bit length' } - ) + await t.throwsAsync(V1.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v1.public signing key must be a private RSA key with 2048 bit modulus length', + }) }) -test('v1 must validate with the right key', async t => { +test('invalid EC curve for v3.public', async (t) => { + const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-521' }) + await t.throwsAsync(V3.sign({}, privateKey), { + instanceOf: TypeError, + message: 'v3.public signing key must be a private EC P-384 key', + }) +}) + +test('V1.validate needs a JSON payload', async (t) => { const k = await V1.generateKey('public') - const k2 = await V1.generateKey('public') - const token = await V1.sign({}, k) + const token = await V1.sign(Buffer.from('test'), k) - return t.throwsAsync( - V1.verify(token, k2), - { instanceOf: errors.PasetoVerificationFailed, code: 'ERR_PASETO_VERIFICATION_FAILED', message: 'invalid signature' } - ) + return t.throwsAsync(V1.verify(token, k), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) -test('v2 must validate with the right key', async t => { +test('V2.validate needs a JSON payload', async (t) => { const k = await V2.generateKey('public') - const k2 = await V2.generateKey('public') - const token = await V2.sign({}, k) + const token = await V2.sign(Buffer.from('test'), k) - return t.throwsAsync( - V2.verify(token, k2), - { instanceOf: errors.PasetoVerificationFailed, code: 'ERR_PASETO_VERIFICATION_FAILED', message: 'invalid signature' } - ) + return t.throwsAsync(V2.verify(token, k), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) -test('v2 doesnt validate v1', async t => { - const k = await V1.generateKey('public') - const k2 = await V2.generateKey('public') +test('V3.validate needs a JSON payload', async (t) => { + const k = await V3.generateKey('public') - const token = await V1.sign({}, k) + const token = await V3.sign(Buffer.from('test'), k) - return t.throwsAsync( - V2.verify(token, k2), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'token is not a v2.public token' } - ) + return t.throwsAsync(V3.verify(token, k), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) -test('v1 doesnt validate v2', async t => { - const k = await V1.generateKey('public') - const k2 = await V2.generateKey('public') +test('V4.validate needs a JSON payload', async (t) => { + const k = await V4.generateKey('public') - const token = await V2.sign({}, k2) + const token = await V4.sign(Buffer.from('test'), k) - return t.throwsAsync( - V1.verify(token, k), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'token is not a v1.public token' } - ) + return t.throwsAsync(V4.verify(token, k), { + instanceOf: errors.PasetoInvalid, + code: 'ERR_PASETO_INVALID', + message: 'All PASETO payloads MUST be a JSON object', + }) }) -test('V1.validate needs a JSON payload', async t => { +test('V1.sign can use Buffer as payload', async (t) => { const k = await V1.generateKey('public') const token = await V1.sign(Buffer.from('test'), k) - return t.throwsAsync( - V1.verify(token, k), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'All PASETO payloads MUST be a JSON object' } - ) + const payload = await V1.verify(token, k, { buffer: true }) + t.true(Buffer.isBuffer(payload)) }) -test('V2.validate needs a JSON payload', async t => { +test('V2.sign can use Buffer as payload', async (t) => { const k = await V2.generateKey('public') const token = await V2.sign(Buffer.from('test'), k) - return t.throwsAsync( - V2.verify(token, k), - { instanceOf: errors.PasetoInvalid, code: 'ERR_PASETO_INVALID', message: 'All PASETO payloads MUST be a JSON object' } - ) + const payload = await V2.verify(token, k, { buffer: true }) + t.true(Buffer.isBuffer(payload)) }) -test('V1.sign can use Buffer as payload', async t => { - const k = await V1.generateKey('public') +test('V3.sign can use Buffer as payload', async (t) => { + const k = await V3.generateKey('public') - const token = await V1.sign(Buffer.from('test'), k) + const token = await V3.sign(Buffer.from('test'), k) - const payload = await V1.verify(token, k, { buffer: true }) + const payload = await V3.verify(token, k, { buffer: true }) t.true(Buffer.isBuffer(payload)) }) -test('V2.sign can use Buffer as payload', async t => { - const k = await V2.generateKey('public') +test('V4.sign can use Buffer as payload', async (t) => { + const k = await V4.generateKey('public') - const token = await V2.sign(Buffer.from('test'), k) + const token = await V4.sign(Buffer.from('test'), k) - const payload = await V2.verify(token, k, { buffer: true }) + const payload = await V4.verify(token, k, { buffer: true }) t.true(Buffer.isBuffer(payload)) }) diff --git a/test/rfc/v1.local.test.js b/test/rfc/v1.local.test.js deleted file mode 100644 index 895f3f1..0000000 --- a/test/rfc/v1.local.test.js +++ /dev/null @@ -1,189 +0,0 @@ -const { createSecretKey } = require('crypto') - -const test = require('ava') - -const { decode, V1 } = require('../../lib') - -test('decrypt A.1.1.1. Test Vector v1-E-1', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUV' + - 'vn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcj' + - 'd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6go' + - 's8fnfjJO8oKiqQMaiBP_Cqncmqw8' - - const expected = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V1.decrypt(token, sk, { ignoreExp: true }), expected) - t.deepEqual(decode(token, { parse: false }), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('encrypt A.1.1.1. Test Vector v1-E-1', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUV' + - 'vn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcj' + - 'd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6go' + - 's8fnfjJO8oKiqQMaiBP_Cqncmqw8' - - const payload = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - const nonce = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('decrypt A.1.1.2. Test Vector v1-E-2', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.w_NOpjgte4bX-2i1JAiTQzHoGUVOgc2yqKqsnYGmaPaCu_KWUkR' + - 'GlCRnOvZZxeH4HTykY7AE_jkzSXAYBkQ1QnwvKS16uTXNfnmp8IRknY76I2m' + - '3S5qsM8klxWQQKFDuQHl8xXV0MwAoeFh9X6vbwIqrLlof3s4PMjRDwKsxYzk' + - 'Mr1RvfDI8emoPoW83q4Q60_xpHaw' - - const expected = { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V1.decrypt(token, sk, { ignoreExp: true }), expected) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('encrypt A.1.1.2. Test Vector v1-E-2', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.w_NOpjgte4bX-2i1JAiTQzHoGUVOgc2yqKqsnYGmaPaCu_KWUkR' + - 'GlCRnOvZZxeH4HTykY7AE_jkzSXAYBkQ1QnwvKS16uTXNfnmp8IRknY76I2m' + - '3S5qsM8klxWQQKFDuQHl8xXV0MwAoeFh9X6vbwIqrLlof3s4PMjRDwKsxYzk' + - 'Mr1RvfDI8emoPoW83q4Q60_xpHaw' - - const payload = { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' } - const nonce = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('decrypt A.1.1.3. Test Vector v1-E-3', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9c' + - 'v39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs' + - '0aFc3ejjORmKP4KUM339W3syBYyjKIOeWnsFQB6Yef-1ov9rvqt7TmwONUHe' + - 'JUYk4IK_JEdUeo_uFRqAIgHsiGCg' - - const expected = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V1.decrypt(token, sk, { ignoreExp: true }), expected) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('encrypt A.1.1.3. Test Vector v1-E-3', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9c' + - 'v39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs' + - '0aFc3ejjORmKP4KUM339W3syBYyjKIOeWnsFQB6Yef-1ov9rvqt7TmwONUHe' + - 'JUYk4IK_JEdUeo_uFRqAIgHsiGCg' - - const payload = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - const nonce = Buffer.from('26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('decrypt A.1.1.4. Test Vector v1-E-4', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbb' + - 'pOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEq' + - 'GNeeWXOyWWHoJQIe0d5nTdvejdt2Srz_5Q0QG4oiz1gB_wmv4U5pifedaZbH' + - 'XUTWXchFEi0etJ4u6tqgxZSklcec' - - const expected = { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V1.decrypt(token, sk, { ignoreExp: true }), expected) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('encrypt A.1.1.4. Test Vector v1-E-4', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbb' + - 'pOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEq' + - 'GNeeWXOyWWHoJQIe0d5nTdvejdt2Srz_5Q0QG4oiz1gB_wmv4U5pifedaZbH' + - 'XUTWXchFEi0etJ4u6tqgxZSklcec' - - const payload = { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' } - const nonce = Buffer.from('26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: undefined, payload: undefined }) -}) - -test('decrypt A.1.1.5. Test Vector v1-E-5', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9c' + - 'v39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs' + - '0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZ' + - 'EWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA' + - '2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9' - - const expected = { - payload: { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' }, - footer: Buffer.from(JSON.stringify({ kid: 'UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo' }), 'utf8'), - version: 'v1', - purpose: 'local' - } - - t.deepEqual(await V1.decrypt(token, sk, { complete: true, ignoreExp: true }), expected) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: expected.footer, payload: undefined }) -}) - -test('encrypt A.1.1.5. Test Vector v1-E-5', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9c' + - 'v39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs' + - '0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZ' + - 'EWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA' + - '2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9' - - const payload = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - const footer = JSON.stringify({ kid: 'UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo' }) - const nonce = Buffer.from('26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { footer, iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { footer, nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: Buffer.from(footer), payload: undefined }) -}) - -test('decrypt A.1.1.6. Test Vector v1-E-6', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbb' + - 'pOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEq' + - 'GNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9' + - 'v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA' + - '2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9' - - const expected = { - payload: { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' }, - footer: Buffer.from(JSON.stringify({ kid: 'UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo' }), 'utf8'), - version: 'v1', - purpose: 'local' - } - - t.deepEqual(await V1.decrypt(token, sk, { complete: true, ignoreExp: true }), expected) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: expected.footer, payload: undefined }) -}) - -test('encrypt A.1.1.6. Test Vector v1-E-6', async t => { - const sk = createSecretKey(Buffer.from('707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f', 'hex')) - const token = 'v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbb' + - 'pOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEq' + - 'GNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9' + - 'v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA' + - '2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9' - - const payload = { data: 'this is a secret message', exp: '2019-01-01T00:00:00+00:00' } - const footer = JSON.stringify({ kid: 'UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo' }) - const nonce = Buffer.from('26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2', 'hex') - - t.deepEqual(await V1.decrypt(await V1.encrypt(payload, sk, { footer, iat: false }), sk, { ignoreExp: true }), payload) - t.deepEqual(await V1.encrypt(payload, sk, { footer, nonce, iat: false }), token) - t.deepEqual(decode(token), { purpose: 'local', version: 'v1', footer: Buffer.from(footer), payload: undefined }) -}) diff --git a/test/rfc/v1.public.test.js b/test/rfc/v1.public.test.js deleted file mode 100644 index 934cb39..0000000 --- a/test/rfc/v1.public.test.js +++ /dev/null @@ -1,88 +0,0 @@ -const test = require('ava') - -const privateRsaKey = '-----BEGIN RSA PRIVATE KEY-----\n' + - 'MIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\n' + - 'GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n' + - '02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\n' + - 'AZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\n' + - 'kRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\n' + - 'idZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\n' + - 'qfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\n' + - 'WdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\n' + - 'A0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\n' + - 'q33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n' + - '1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n' + - '42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\n' + - 'FfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\n' + - 'rPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\n' + - 'AG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\n' + - 'xCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n' + - '/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\n' + - 'epTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n' + - '3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\n' + - 'B9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\n' + - 'b9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\n' + - 'x/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n' + - '3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\n' + - 'pcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\n' + - 'uVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n' + - '-----END RSA PRIVATE KEY-----' - -const { decode, V1 } = require('../../lib') - -test('A.1.2.1. Test Vector v1-S-1', async t => { - const token = 'v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiw' + - 'iZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9cIZKahKeGM5k' + - 'iAS_4D70Qbz9FIThZpxetJ6n6E6kXP_119SvQcnfCSfY_gG3D0Q2v7FEt' + - 'm2Cmj04lE6YdgiZ0RwA41WuOjXq7zSnmmHK9xOSH6_2yVgt207h1_LphJ' + - 'zVztmZzq05xxhZsV3nFPm2cCu8oPceWy-DBKjALuMZt_Xj6hWFFie96Sf' + - 'Q6i85lOsTX8Kc6SQaG-3CgThrJJ6W9DC-YfQ3lZ4TJUoY3QNYdtEgAvp1' + - 'QuWWK6xmIb8BwvkBPej5t88QUb7NcvZ15VyNw3qemQGn2ITSdpdDgwMtp' + - 'flZOeYdtuxQr1DSGO2aQyZl7s0WYn1IjdQFx6VjSQ4yfw' - const pem = '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p\n' + - '5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd\n' + - '74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+g\n' + - 'mLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU\n' + - '5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5\n' + - 'IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWc\n' + - 'p/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQAB\n' + - '-----END PUBLIC KEY-----' - const expected = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V1.verify(token, pem, { ignoreExp: true }), expected) - t.deepEqual(await V1.verify(await V1.sign(expected, privateRsaKey, { iat: false }), pem, { ignoreExp: true }), expected) - t.deepEqual(decode(token, { parse: false }), { purpose: 'public', version: 'v1', footer: undefined, payload: Buffer.from(JSON.stringify(expected)) }) -}) - -test('A.1.2.2. Test Vector v1-S-2', async t => { - const token = 'v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiw' + - 'iZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4mis' + - 'AuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X' + - '8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S' + - '1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthq' + - 'az7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJM' + - 'pixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C' + - '0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJ' + - 'kWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVx' + - 'biJ9' - const pem = '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p\n' + - '5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd\n' + - '74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+g\n' + - 'mLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU\n' + - '5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5\n' + - 'IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWc\n' + - 'p/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQAB\n' + - '-----END PUBLIC KEY-----' - const expected = { - payload: { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' }, - footer: Buffer.from(JSON.stringify({ kid: 'dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn' }), 'utf8'), - version: 'v1', - purpose: 'public' - } - - t.deepEqual(await V1.verify(token, pem, { complete: true, ignoreExp: true }), expected) - t.deepEqual(await V1.verify(await V1.sign(expected.payload, privateRsaKey, { footer: expected.footer, iat: false }), pem, { complete: true, ignoreExp: true }), expected) - t.deepEqual(decode(token, { parse: false }), { purpose: 'public', version: 'v1', footer: expected.footer, payload: Buffer.from(JSON.stringify(expected.payload)) }) -}) diff --git a/test/rfc/v2.public.test.js b/test/rfc/v2.public.test.js deleted file mode 100644 index fa3d9e3..0000000 --- a/test/rfc/v2.public.test.js +++ /dev/null @@ -1,66 +0,0 @@ -const test = require('ava') - -const { decode, V2 } = require('../../lib') - -test('verify A.2.2.1. Test Vector v2-S-1', async t => { - const token = 'v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi' + - 'wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt' + - 'Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj' + - 'JK2ZXC2SUYuOFM-Q_5Cw' - const pem = '-----BEGIN PUBLIC KEY-----\n' + - 'MCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n' + - '-----END PUBLIC KEY-----' - const expected = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V2.verify(token, pem, { ignoreExp: true }), expected) -}) - -test('sign A.2.2.1. Test Vector v2-S-1', async t => { - const token = 'v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi' + - 'wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt' + - 'Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj' + - 'JK2ZXC2SUYuOFM-Q_5Cw' - const pem = '-----BEGIN PRIVATE KEY-----\n' + - 'MC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n' + - '-----END PRIVATE KEY-----' - const payload = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - - t.deepEqual(await V2.sign(payload, pem, { iat: false }), token) - t.deepEqual(decode(token, { parse: false }), { purpose: 'public', version: 'v2', footer: undefined, payload: Buffer.from(JSON.stringify(payload)) }) -}) - -test('verify A.2.2.2. Test Vector v2-S-2', async t => { - const token = 'v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi' + - 'wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC' + - 'R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601' + - 'tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q' + - '3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9' - const pem = '-----BEGIN PUBLIC KEY-----\n' + - 'MCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n' + - '-----END PUBLIC KEY-----' - const expected = { - payload: { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' }, - footer: Buffer.from(JSON.stringify({ kid: 'zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN' }), 'utf8'), - version: 'v2', - purpose: 'public' - } - - t.deepEqual(await V2.verify(token, pem, { complete: true, ignoreExp: true }), expected) -}) - -test('sign A.2.2.2. Test Vector v2-S-2', async t => { - const token = 'v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi' + - 'wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC' + - 'R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601' + - 'tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q' + - '3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9' - const pem = '-----BEGIN PRIVATE KEY-----\n' + - 'MC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n' + - '-----END PRIVATE KEY-----' - - const payload = { data: 'this is a signed message', exp: '2019-01-01T00:00:00+00:00' } - const footer = JSON.stringify({ kid: 'zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN' }) - - t.deepEqual(await V2.sign(payload, pem, { footer, iat: false }), token) - t.deepEqual(decode(token, { parse: false }), { purpose: 'public', version: 'v2', footer: Buffer.from(footer), payload: Buffer.from(JSON.stringify(payload)) }) -}) diff --git a/test/smoke.test.js b/test/smoke.test.js new file mode 100644 index 0000000..55e8ea0 --- /dev/null +++ b/test/smoke.test.js @@ -0,0 +1,107 @@ +const { createPublicKey } = require('crypto') + +const test = require('ava') + +const { decode, errors, ...lib } = require('../lib') + +for (const [version, { sign, verify, encrypt, decrypt, generateKey }] of Object.entries(lib).filter( + ([key]) => key.startsWith('V'), +)) { + test(`${version.toLowerCase()}.public.`, async (t) => { + const sk = await generateKey('public') + const pk = createPublicKey(sk) + + const footer = 'footer' + const payload = { foo: 'bar' } + const signOptions = { footer } + const verifyOptions = {} + if (version === 'V3' || version === 'V4') { + signOptions.assertion = `${version.toLowerCase()}.public.` + verifyOptions.assertion = signOptions.assertion + } + + const [token] = await Promise.all([ + sign(payload, sk, { ...signOptions, iat: false }), + sign(Buffer.from(JSON.stringify(payload)), sk, signOptions), + ]) + + t.deepEqual(decode(token), { + payload, + purpose: 'public', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + + t.deepEqual(await verify(token, pk, { ...verifyOptions }), payload) + t.deepEqual(await verify(token, sk, { ...verifyOptions }), payload) + t.deepEqual(await verify(token, sk, { ...verifyOptions, complete: true }), { + payload, + purpose: 'public', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + t.deepEqual(await verify(token, sk, { ...verifyOptions, complete: true, buffer: true }), { + payload: Buffer.from(JSON.stringify(payload)), + purpose: 'public', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + + if (version === 'V3' || version === 'V4') { + await t.throwsAsync(verify(token, pk), { code: 'ERR_PASETO_VERIFICATION_FAILED' }) + } + + await t.throwsAsync(verify(token, await generateKey('public')), { + code: 'ERR_PASETO_VERIFICATION_FAILED', + }) + }) + + if (encrypt) { + test(`${version.toLowerCase()}.local.`, async (t) => { + const sk = await generateKey('local') + + const footer = 'footer' + const payload = { foo: 'bar' } + const encryptOptions = { footer } + const decryptOptions = {} + if (version === 'V3' || version === 'V4') { + encryptOptions.assertion = `${version.toLowerCase()}.local.` + decryptOptions.assertion = encryptOptions.assertion + } + + const [token] = await Promise.all([ + encrypt(payload, sk, { ...encryptOptions, iat: false }), + encrypt(Buffer.from(JSON.stringify(payload)), sk, encryptOptions), + ]) + + t.deepEqual(decode(token), { + payload: undefined, + purpose: 'local', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + + t.deepEqual(await decrypt(token, sk, { ...decryptOptions }), payload) + t.deepEqual(await decrypt(token, sk, { ...decryptOptions, complete: true }), { + payload, + purpose: 'local', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + t.deepEqual(await decrypt(token, sk, { ...decryptOptions, complete: true, buffer: true }), { + payload: Buffer.from(JSON.stringify(payload)), + purpose: 'local', + version: version.toLowerCase(), + footer: Buffer.from(footer), + }) + + if (version === 'V3' || version === 'V4') { + await t.throwsAsync(decrypt(token, sk), { code: 'ERR_PASETO_DECRYPTION_FAILED' }) + } + + await t.throwsAsync(decrypt(token, await generateKey('local')), { + code: 'ERR_PASETO_DECRYPTION_FAILED', + }) + }) + } +} diff --git a/test/tse.test.js b/test/tse.test.js index e465659..b180e32 100644 --- a/test/tse.test.js +++ b/test/tse.test.js @@ -2,6 +2,6 @@ const test = require('ava') const TSE = require('../lib/help/timing_safe_equal') -test('handles different length inputs', t => { +test('handles different length inputs', (t) => { t.false(TSE(Buffer.from('foo'), Buffer.from('foobar'))) }) diff --git a/test/vectors/v1.json b/test/vectors/v1.json new file mode 100644 index 0000000..990ea84 --- /dev/null +++ b/test/vectors/v1.json @@ -0,0 +1,149 @@ +{ + "name": "PASETO v1 Test Vectors", + "tests": [ + { + "name": "1-E-1", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-2", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.w_NOpjgte4bX-2i1JAiTQzHoGUVOgc2yqKqsnYGmaPaCu_KWUkRGlCRnOvZZxeH4HTykY7AE_jkzSXAYBkQ1QnwvKS16uTXNfnmp8IRknY76I2m3S5qsM8klxWQQKFDuQHl8xXV0MwAoeFh9X6vbwIqrLlof3s4PMjRDwKsxYzkMr1RvfDI8emoPoW83q4Q60_xpHaw", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-3", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3syBYyjKIOeWnsFQB6Yef-1ov9rvqt7TmwONUHeJUYk4IK_JEdUeo_uFRqAIgHsiGCg", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-4", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvejdt2Srz_5Q0QG4oiz1gB_wmv4U5pifedaZbHXUTWXchFEi0etJ4u6tqgxZSklcec", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-E-5", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "1-E-6", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "1-E-7", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-E-8", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-E-9", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvdgNpe3vI21jV2YL7WVG5p63_JxxzLckBu9azQ0GlDMdPxNAxoyvmU1wbpSbRB9Iw4.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "1-S-1", + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9cIZKahKeGM5kiAS_4D70Qbz9FIThZpxetJ6n6E6kXP_119SvQcnfCSfY_gG3D0Q2v7FEtm2Cmj04lE6YdgiZ0RwA41WuOjXq7zSnmmHK9xOSH6_2yVgt207h1_LphJzVztmZzq05xxhZsV3nFPm2cCu8oPceWy-DBKjALuMZt_Xj6hWFFie96SfQ6i85lOsTX8Kc6SQaG-3CgThrJJ6W9DC-YfQ3lZ4TJUoY3QNYdtEgAvp1QuWWK6xmIb8BwvkBPej5t88QUb7NcvZ15VyNw3qemQGn2ITSdpdDgwMtpflZOeYdtuxQr1DSGO2aQyZl7s0WYn1IjdQFx6VjSQ4yfw", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "1-S-2", + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "" + }, + { + "name": "1-S-3", + "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", + "secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", + "token": "v1.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9sBTIb0J_4misAuYc4-6P5iR1rQighzktpXhJ8gtrrp2MqSSDkbb8q5WZh3FhUYuW_rg2X8aflDlTWKAqJkM3otjYwtmfwfOhRyykxRL2AfmIika_A-_MaLp9F0iw4S1JetQQDV8GUHjosd87TZ20lT2JQLhxKjBNJSwWue8ucGhTgJcpOhXcthqaz7a2yudGyd0layzeWziBhdQpoBR6ryTdtIQX54hP59k3XCIxuYbB9qJMpixiPAEKBcjHT74sA-uukug9VgKO7heWHwJL4Rl9ad21xyNwaxAnwAJ7C0fN5oGv8Rl0dF11b3tRmsmbDoIokIM0Dba29x_T3YzOyg.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "discarded-anyway" + } + ] +} \ No newline at end of file diff --git a/test/vectors/v1.test.js b/test/vectors/v1.test.js new file mode 100644 index 0000000..ee5bdbe --- /dev/null +++ b/test/vectors/v1.test.js @@ -0,0 +1,58 @@ +const crypto = require('crypto') + +const test = require('ava') +const sinon = require('sinon').createSandbox() + +const { decode, V1 } = require('../../lib') +const vectors = require('./v1.json') + +test.afterEach(() => sinon.restore()) + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('1-E-'))) { + test.serial(`${vectors.name} - ${vector.name}`, async (t) => { + const sk = crypto.createSecretKey(Buffer.from(vector.key, 'hex')) + sinon.stub(crypto, 'randomBytes').returns(Buffer.from(vector.nonce, 'hex')) + const token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + + t.deepEqual(decode(token), { + payload: undefined, + purpose: 'local', + version: 'v1', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V1.decrypt(token, sk, { ignoreExp: true }), expected) + t.deepEqual(await V1.encrypt(expected, sk, { footer, iat: false }), token) + }) +} + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('1-S-'))) { + test(`${vectors.name} - ${vector.name}`, async (t) => { + const pk = crypto.createPublicKey(vector['public-key']) + const sk = crypto.createPrivateKey(vector['secret-key']) + let token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + + t.deepEqual(decode(token), { + payload: expected, + purpose: 'public', + version: 'v1', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V1.verify(token, pk, { ignoreExp: true }), expected) + t.deepEqual(await V1.verify(token, sk, { ignoreExp: true }), expected) + + token = await V1.sign(expected, sk, { footer, iat: false }) + + t.deepEqual(decode(token), { + payload: expected, + purpose: 'public', + version: 'v1', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V1.verify(token, pk, { ignoreExp: true }), expected) + t.deepEqual(await V1.verify(token, sk, { ignoreExp: true }), expected) + }) +} diff --git a/test/vectors/v2.json b/test/vectors/v2.json new file mode 100644 index 0000000..18399f4 --- /dev/null +++ b/test/vectors/v2.json @@ -0,0 +1,155 @@ +{ + "name": "PASETO v2 Test Vectors", + "tests": [ + { + "name": "2-E-1", + "nonce": "000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4PnW8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVODyfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-2", + "nonce": "000000000000000000000000000000000000000000000000", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-3", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-4", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DPbIxtjGvNRAwsLK7LcV8oQ", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-E-5", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-E-6", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-E-7", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-E-8", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-E-9", + "nonce": "45742c976d684ff84ebdc0de59809a97cda2f64c84fda19b", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUrIu3B6h232h62DoOJbyKBGPZG50XDZ6mbPtw.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": { + "data": "this is a secret message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "discarded-anyway" + }, + { + "name": "2-S-1", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGntTu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_DjJK2ZXC2SUYuOFM-Q_5Cw", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "2-S-2", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "2-S-3", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2019-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "discarded-anyway" + } + ] +} \ No newline at end of file diff --git a/test/vectors/v2.test.js b/test/vectors/v2.test.js new file mode 100644 index 0000000..68548d3 --- /dev/null +++ b/test/vectors/v2.test.js @@ -0,0 +1,71 @@ +const crypto = require('crypto') + +const test = require('ava') + +const { decode, V2 } = require('../../lib') +const vectors = require('./v2.json') + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('2-S-'))) { + async function testPublic(t, vector, pk, sk) { + const token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + + t.deepEqual(decode(token), { + payload: expected, + purpose: 'public', + version: 'v2', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V2.verify(token, pk, { ignoreExp: true }), expected) + t.deepEqual(await V2.verify(token, sk, { ignoreExp: true }), expected) + t.deepEqual(await V2.sign(expected, sk, { footer, iat: false }), token) + } + + test( + `${vectors.name} - ${vector.name} (bytesToKeyObject)`, + testPublic, + vector, + V2.bytesToKeyObject(Buffer.from(vector['public-key'], 'hex')), + V2.bytesToKeyObject(Buffer.from(vector['secret-key'], 'hex')), + ) + + test( + `${vectors.name} - ${vector.name} (raw)`, + testPublic, + vector, + Buffer.from(vector['public-key'], 'hex'), + Buffer.from(vector['secret-key'], 'hex'), + ) + + test( + `${vectors.name} - ${vector.name} (pem)`, + testPublic, + vector, + crypto.createPublicKey(vector['public-key-pem']), + crypto.createPrivateKey(vector['secret-key-pem']), + ) + + test(`${vectors.name} - ${vector.name} (key operations)`, (t) => { + const keyObjects = { + pk: crypto.createPublicKey(vector['public-key-pem']), + sk: crypto.createPrivateKey(vector['secret-key-pem']), + } + const raw = { + pk: Buffer.from(vector['public-key'], 'hex'), + sk: Buffer.from(vector['secret-key'], 'hex'), + } + + t.deepEqual(V2.keyObjectToBytes(keyObjects.pk), raw.pk) + t.deepEqual(V2.keyObjectToBytes(keyObjects.sk), raw.sk) + + t.deepEqual( + V2.bytesToKeyObject(raw.pk).export({ format: 'jwk' }), + keyObjects.pk.export({ format: 'jwk' }), + ) + t.deepEqual( + V2.bytesToKeyObject(raw.sk).export({ format: 'jwk' }), + keyObjects.sk.export({ format: 'jwk' }), + ) + }) +} diff --git a/test/vectors/v3.json b/test/vectors/v3.json new file mode 100644 index 0000000..6c407ff --- /dev/null +++ b/test/vectors/v3.json @@ -0,0 +1,155 @@ +{ + "name": "PASETO v3 Test Vectors", + "tests": [ + { + "name": "3-E-1", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeg", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAqhWxBMDgyBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9IzZfaZpReVpHlDSwfuygx1riVXYVs-UjcrG_apl9oz3jCVmmJbRuKn5ZfD8mHz2db0A", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-3", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlxnt5xyhQjFJomwnt7WW_7r2VT0G704ifult011-TgLCyQ2X8imQhniG_hAQ4BydM", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-4", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlBZa_gOpVj4gv0M9lV6Pwjp8JS_MmaZaTA1LLTULXybOBZ2S4xMbYqYmDRhh3IgEk", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-5", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-6", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmSeEMphEWHiwtDKJftg41O1F8Hat-8kQ82ZIAMFqkx9q5VkWlxZke9ZzMBbb3Znfo.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-7", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJkzWACWAIoVa0bz7EWSBoTEnS8MvGBYHHo6t6mJunPrFR9JKXFCc0obwz5N-pxFLOc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-7\"}" + }, + { + "name": "3-E-8", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-8\"}" + }, + { + "name": "3-E-9", + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlk1nli0_wijTH_vCuRwckEDc82QWK8-lG2fT9wQF271sgbVRVPjm0LwMQZkvvamqU.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"3-E-9\"}" + }, + { + "name": "3-S-1", + "public-key": "02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb", + "secret-key": "20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96", + "secret-key-pem": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAgNHYJYHR3rKj7+8XmIYRV8xmWaXku+LRm+qh73Gd5gUTISN0DZh7t\nWsYkYTQM6pagBwYFK4EEACKhZANiAAT7y3xp7hxgV5vnozQTSHjZxcW/NdVS2rY8\nAUA5ftFM72N9dyCSXERpnqMOcodMcvt8kgcrB8KcKee0HU23E79/s4CvEs8hBfnj\nSUd/gcAm08EjSIz06iWjrNy4NakxR3I=\n-----END EC PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+8t8ae4cYFeb56M0E0h42cXFvzXVUtq2\nPAFAOX7RTO9jfXcgklxEaZ6jDnKHTHL7fJIHKwfCnCnntB1NtxO/f7OArxLPIQX5\n40lHf4HAJtPBI0iM9Oolo6zcuDWpMUdy\n-----END PUBLIC KEY-----", + "token": "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9qqEwwrKHKi5lJ7b9MBKc0G4MGZy0ptUiMv3lAUAaz-JY_zjoqBSIxMxhfAoeNYiSyvfUErj76KOPWm1OeNnBPkTSespeSXDGaDfxeIrl3bRrPEIy7tLwLAIsRzsXkfph", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-S-2", + "public-key": "02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb", + "secret-key": "20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96", + "secret-key-pem": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAgNHYJYHR3rKj7+8XmIYRV8xmWaXku+LRm+qh73Gd5gUTISN0DZh7t\nWsYkYTQM6pagBwYFK4EEACKhZANiAAT7y3xp7hxgV5vnozQTSHjZxcW/NdVS2rY8\nAUA5ftFM72N9dyCSXERpnqMOcodMcvt8kgcrB8KcKee0HU23E79/s4CvEs8hBfnj\nSUd/gcAm08EjSIz06iWjrNy4NakxR3I=\n-----END EC PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+8t8ae4cYFeb56M0E0h42cXFvzXVUtq2\nPAFAOX7RTO9jfXcgklxEaZ6jDnKHTHL7fJIHKwfCnCnntB1NtxO/f7OArxLPIQX5\n40lHf4HAJtPBI0iM9Oolo6zcuDWpMUdy\n-----END PUBLIC KEY-----", + "token": "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "" + }, + { + "name": "3-S-3", + "public-key": "02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb", + "secret-key": "20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96", + "secret-key-pem": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAgNHYJYHR3rKj7+8XmIYRV8xmWaXku+LRm+qh73Gd5gUTISN0DZh7t\nWsYkYTQM6pagBwYFK4EEACKhZANiAAT7y3xp7hxgV5vnozQTSHjZxcW/NdVS2rY8\nAUA5ftFM72N9dyCSXERpnqMOcodMcvt8kgcrB8KcKee0HU23E79/s4CvEs8hBfnj\nSUd/gcAm08EjSIz06iWjrNy4NakxR3I=\n-----END EC PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+8t8ae4cYFeb56M0E0h42cXFvzXVUtq2\nPAFAOX7RTO9jfXcgklxEaZ6jDnKHTHL7fJIHKwfCnCnntB1NtxO/f7OArxLPIQX5\n40lHf4HAJtPBI0iM9Oolo6zcuDWpMUdy\n-----END PUBLIC KEY-----", + "token": "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ94SjWIbjmS7715GjLSnHnpJrC9Z-cnwK45dmvnVvCRQDCCKAXaKEopTajX0DKYx1Xqr6gcTdfqscLCAbiB4eOW9jlt-oNqdG8TjsYEi6aloBfTzF1DXff_45tFlnBukEX.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}", + "implicit-assertion": "{\"test-vector\":\"3-S-3\"}" + } + ] +} \ No newline at end of file diff --git a/test/vectors/v3.test.js b/test/vectors/v3.test.js new file mode 100644 index 0000000..73077e8 --- /dev/null +++ b/test/vectors/v3.test.js @@ -0,0 +1,106 @@ +const crypto = require('crypto') + +const test = require('ava') +const sinon = require('sinon').createSandbox() + +const { decode, V3 } = require('../../lib') +const vectors = require('./v3.json') + +test.afterEach(() => sinon.restore()) + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('3-E-'))) { + test.serial(`${vectors.name} - ${vector.name}`, async (t) => { + const sk = crypto.createSecretKey(Buffer.from(vector.key, 'hex')) + sinon.stub(crypto, 'randomBytes').returns(Buffer.from(vector.nonce, 'hex')) + const token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + const assertion = vector['implicit-assertion'] + + t.deepEqual(decode(token), { + payload: undefined, + purpose: 'local', + version: 'v3', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V3.decrypt(token, sk, { ignoreExp: true, assertion }), expected) + t.deepEqual(await V3.encrypt(expected, sk, { footer, iat: false, assertion }), token) + }) +} + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('3-S-'))) { + async function testPublic(t, vector, pk, sk) { + let token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + const assertion = vector['implicit-assertion'] + + token = await V3.sign(expected, sk, { footer, iat: false, assertion }) + + t.deepEqual(decode(token), { + payload: expected, + purpose: 'public', + version: 'v3', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V3.verify(token, pk, { ignoreExp: true, assertion }), expected) + t.deepEqual(await V3.verify(token, sk, { ignoreExp: true, assertion }), expected) + } + + test( + `${vectors.name} - ${vector.name} (bytesToKeyObject)`, + testPublic, + vector, + V3.bytesToKeyObject(Buffer.from(vector['public-key'], 'hex')), + V3.bytesToKeyObject(Buffer.from(vector['secret-key'], 'hex')), + ) + + test( + `${vectors.name} - ${vector.name} (raw)`, + testPublic, + vector, + Buffer.from(vector['public-key'], 'hex'), + Buffer.from(vector['secret-key'], 'hex'), + ) + + test( + `${vectors.name} - ${vector.name} (pem)`, + testPublic, + vector, + crypto.createPublicKey(vector['public-key-pem']), + crypto.createPrivateKey(vector['secret-key-pem']), + ) + + test(`${vectors.name} - ${vector.name} (key operations)`, (t) => { + const keyObjects = { + pk: crypto.createPublicKey(vector['public-key-pem']), + sk: crypto.createPrivateKey(vector['secret-key-pem']), + } + const raw = { + pk: Buffer.from(vector['public-key'], 'hex'), + sk: Buffer.from(vector['secret-key'], 'hex'), + } + + t.deepEqual(V3.keyObjectToBytes(keyObjects.pk), raw.pk) + t.deepEqual(V3.keyObjectToBytes(keyObjects.sk), raw.sk) + + t.deepEqual( + V3.bytesToKeyObject(raw.pk).export({ format: 'jwk' }), + keyObjects.pk.export({ format: 'jwk' }), + ) + t.deepEqual( + V3.bytesToKeyObject(raw.sk).export({ format: 'jwk' }), + keyObjects.sk.export({ format: 'jwk' }), + ) + + const { x, y } = keyObjects.pk.export({ format: 'jwk' }) + t.deepEqual( + V3.keyObjectToBytes( + V3.bytesToKeyObject( + Buffer.concat([Buffer.from([0x04]), Buffer.from(x, 'base64'), Buffer.from(y, 'base64')]), + ), + ), + raw.pk, + ) + }) +} diff --git a/test/vectors/v4.json b/test/vectors/v4.json new file mode 100644 index 0000000..45fcc08 --- /dev/null +++ b/test/vectors/v4.json @@ -0,0 +1,155 @@ +{ + "name": "PASETO v4 Test Vectors", + "tests": [ + { + "name": "4-E-1", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-E-2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvS2csCgglvpk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XIemu9chy3WVKvRBfg6t8wwYHK0ArLxxfZP73W_vfwt5A", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-E-3", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6-tyebyWG6Ov7kKvBdkrrAJ837lKP3iDag2hzUPHuMKA", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-E-4", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4gt6TiLm55vIH8c_lGxxZpE3AWlH4WTR0v45nsWoU3gQ", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-E-5", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "4-E-6", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6pWSA5HX2wjb3P-xLQg5K5feUCX4P2fpVK3ZLWFbMSxQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "4-E-7", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t40KCCWLA7GYL9KFHzKlwY9_RnIfRrMQpueydLEAZGGcA.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a secret message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"4-E-7\"}" + }, + { + "name": "4-E-8", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"4-E-8\"}" + }, + { + "name": "4-E-9", + "nonce": "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6tybdlmnMwcDMw0YxA_gFSE_IUWl78aMtOepFYSWYfQA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": { + "data": "this is a hidden message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"4-E-9\"}" + }, + { + "name": "4-S-1", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-S-2", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "4-S-3", + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": { + "data": "this is a signed message", + "exp": "2022-01-01T00:00:00+00:00" + }, + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"4-S-3\"}" + } + ] +} \ No newline at end of file diff --git a/test/vectors/v4.test.js b/test/vectors/v4.test.js new file mode 100644 index 0000000..46ce67c --- /dev/null +++ b/test/vectors/v4.test.js @@ -0,0 +1,72 @@ +const crypto = require('crypto') + +const test = require('ava') + +const { decode, V4 } = require('../../lib') +const vectors = require('./v4.json') + +for (const vector of vectors.tests.filter(({ name }) => name.startsWith('4-S-'))) { + async function testPublic(t, vector, pk, sk) { + const token = vector.token + const footer = vector.footer || undefined + const expected = vector.payload + const assertion = vector['implicit-assertion'] + + t.deepEqual(decode(token), { + payload: expected, + purpose: 'public', + version: 'v4', + footer: footer ? Buffer.from(footer) : undefined, + }) + t.deepEqual(await V4.verify(token, pk, { ignoreExp: true, assertion }), expected) + t.deepEqual(await V4.verify(token, sk, { ignoreExp: true, assertion }), expected) + t.deepEqual(await V4.sign(expected, sk, { footer, iat: false, assertion }), token) + } + + test( + `${vectors.name} - ${vector.name} (bytesToKeyObject)`, + testPublic, + vector, + V4.bytesToKeyObject(Buffer.from(vector['public-key'], 'hex')), + V4.bytesToKeyObject(Buffer.from(vector['secret-key'], 'hex')), + ) + + test( + `${vectors.name} - ${vector.name} (raw)`, + testPublic, + vector, + Buffer.from(vector['public-key'], 'hex'), + Buffer.from(vector['secret-key'], 'hex'), + ) + + test( + `${vectors.name} - ${vector.name} (pem)`, + testPublic, + vector, + crypto.createPublicKey(vector['public-key-pem']), + crypto.createPrivateKey(vector['secret-key-pem']), + ) + + test(`${vectors.name} - ${vector.name} (key operations)`, (t) => { + const keyObjects = { + pk: crypto.createPublicKey(vector['public-key-pem']), + sk: crypto.createPrivateKey(vector['secret-key-pem']), + } + const raw = { + pk: Buffer.from(vector['public-key'], 'hex'), + sk: Buffer.from(vector['secret-key'], 'hex'), + } + + t.deepEqual(V4.keyObjectToBytes(keyObjects.pk), raw.pk) + t.deepEqual(V4.keyObjectToBytes(keyObjects.sk), raw.sk) + + t.deepEqual( + V4.bytesToKeyObject(raw.pk).export({ format: 'jwk' }), + keyObjects.pk.export({ format: 'jwk' }), + ) + t.deepEqual( + V4.bytesToKeyObject(raw.sk).export({ format: 'jwk' }), + keyObjects.sk.export({ format: 'jwk' }), + ) + }) +} diff --git a/types/index.d.ts b/types/index.d.ts index bffadcf..58091b2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,460 +1,230 @@ /// // TypeScript Version: 3.6 - -import { KeyObject, PrivateKeyInput, PublicKeyInput } from 'crypto'; - +import { KeyObject, PrivateKeyInput, PublicKeyInput, JsonWebKeyInput } from 'crypto' export interface ProduceOptions { - /** PASETO Audience, "aud" claim value, if provided it will replace "aud" found in the payload */ - audience?: string; - /** - * PASETO Expiration Time, "exp" claim value, specified as string which is added to the current unix epoch timestamp, - * if provided it will replace Expiration Time found in the payload - * @example "24 hours", "20 m", "60s" - */ - expiresIn?: string; - /** PASETO footer */ - footer?: object | string | Buffer; - /** - * When true it pushes the "iat" to the PASETO payload - * @default true - */ - iat?: boolean; - /** PASETO Issuer, "iss" claim value, if provided it will replace "iss" found in the payload */ - issuer?: string; - /** Token ID, "jti" claim value, if provided it will replace "jti" found in the payload */ - jti?: string; - /** Key ID, "kid" claim value, if provided it will replace "kid" found in the payload */ - kid?: string; - /** - * PASETO Not Before, "nbf" claim value, specified as string which is added to the current unix epoch timestamp, - * if provided it will replace Not Before found in the payload - * @example "24 hours", "20 m", "60s" - */ - notBefore?: string; - /** - * Date object to be used instead of the current unix epoch timestamp. Default: 'new Date()' - * @default new Date() - */ - now?: Date; - /** PASETO subject, "sub" claim value, if provided it will replace "sub" found in the payload */ - subject?: string; + assertion?: string | Buffer + audience?: string + expiresIn?: string + footer?: object | string | Buffer + iat?: boolean + issuer?: string + jti?: string + kid?: string + notBefore?: string + now?: Date + subject?: string } - export interface ConsumeOptions { - /** Expected audience value. An exact match must be found in the payload */ - audience?: string; - /** - * Clock Tolerance for comparing timestamps, provided as timespan string - * @example "120s", "2 minutes", etc. - * @default no clock tolerance - */ - clockTolerance?: string; - /** - * When false only the parsed payload is returned, otherwise an object with a parsed payload and footer (as a Buffer) will be returned - * @default false - */ - complete?: TComplete; - /** - * When false the parsed payload is returned, otherwise the raw payload (as a Buffer) will be returned - * @default false - */ - buffer?: false; - /** - * When true will not be validating the "exp" claim value to be in the future from now - * @default false - */ - ignoreExp?: boolean; - /** - * When true will not be validating the "iat" claim value to be in the past from now - * @default false - */ - ignoreIat?: boolean; - /** - * When true will not be validating the "nbf" claim value to be in the past from now - * @default false - */ - ignoreNbf?: boolean; - /** Expected issuer value. An exact match must be found in the payload */ - issuer?: string; - /** - * When provided the payload is checked to have the "iat" claim and its value is validated not to be older than the provided timespan string - * @example "30m", "24 hours" - */ - maxTokenAge?: string; - /** - * Date object to be used instead of the current unix epoch timestamp - * @default new Date() - */ - now?: Date; - /** Expected subject value. An exact match must be found in the payload */ - subject?: string; + assertion?: string | Buffer + audience?: string + clockTolerance?: string + complete?: TComplete + buffer?: false + ignoreExp?: boolean + ignoreIat?: boolean + ignoreNbf?: boolean + issuer?: string + maxTokenAge?: string + now?: Date + subject?: string } - export interface CompleteResult { - /** PASETO footer */ - footer?: Buffer; - /** PASETO Payload claims */ - payload: object; - /** PASETO purpose */ - purpose: 'local' | 'public'; - /** Protocol version */ - version: string; + footer?: Buffer + payload: object + purpose: 'local' | 'public' + version: string } - export interface ConsumeOptionsBuffer { - /** - * When false only the parsed payload is returned, otherwise an object with a parsed payload and footer (as a Buffer) will be returned - * @default false - */ - complete?: TComplete; - /** - * When false the parsed payload is returned, otherwise the raw payload (as a Buffer) will be returned - * @default true - */ - buffer: true; + assertion?: string | Buffer + complete?: TComplete + buffer: true } - export interface CompleteResultBuffer { - /** PASETO footer */ - footer?: Buffer; - /** PASETO payload */ - payload: Buffer; - /** PASETO purpose */ - purpose: 'local' | 'public'; - /** Protocol version */ - version: string; + footer?: Buffer + payload: Buffer + purpose: 'local' | 'public' + version: string } - export interface DecodeResult { - /** PASETO footer */ - footer?: Buffer; - /** PASETO Payload claims */ - payload?: object; - /** PASETO purpose */ - purpose: 'local' | 'public'; - /** Protocol version */ - version: string; + footer?: Buffer + payload?: object + purpose: 'local' | 'public' + version: string } - export interface DecodeResultBuffer { - /** PASETO footer */ - footer?: Buffer; - /** PASETO payload */ - payload?: Buffer; - /** PASETO purpose */ - purpose: 'local' | 'public'; - /** Protocol version */ - version: string; + footer?: Buffer + payload?: Buffer + purpose: 'local' | 'public' + version: string } - -export function decode(token: string): DecodeResult; - +export function decode(token: string): DecodeResult export namespace V1 { - /** - * Serializes and signs the payload as a PASETO using the provided private key - * @example - * const { createPrivateKey } = require('crypto') - * const { V1 } = require('paseto') - * - * const key = createPrivateKey(privateKey) - * - * const payload = { - * 'urn:example:claim': 'foo' - * } - * - * (async () => { - * const token = await V1.sign(payload, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * expiresIn: '2 hours' - * }) - * // v1.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDctMDJUMTQ6MDI6MjIuNDg5WiIsImV4cCI6IjIwMTktMDctMDJUMTY6MDI6MjIuNDg5WiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifbCaLu19MdLxjrexKh4WTyKr6UoeXzDly_Po1ZNv4wD5CglfY84QqQYTGXLlcLAqZagM3cWJn6xge-lBlT63km6OtOsiWTaKOnYg4MBtQTKmLsjpehpPtDSl_39h2BenB-r911qjYwNNuaRukjrtSVKQtfxdoAoFKEz_eulsDTclEBV7bJrL9Bo0epkJhFShZ6-K8qNd6rTg6Q3YOZCheW1FqNjqfoUYJ9nqPZl2OVbcPdAW3HBeLJefmlL_QGVSRClE2MXOVDrcyf7vGZ0SIj3ylnr6jmEJpzG8o0ap7FblQZI3xp91e-gmw30o6njhSq1ZVWpLqp7FYzq0pknJzGE - * })() - */ - function sign( - /** PASETO Payload claims or payload */ - payload: object | Buffer, - /** The key to sign with. Alternatively any input that works for `crypto.createPrivateKey` */ - key: KeyObject | PrivateKeyInput, - options?: ProduceOptions, - ): Promise; - /** - * Serializes and encrypts the payload as a PASETO using the provided secret key - * @example - * const { createSecretKey } = require('crypto') - * const { V1 } = require('paseto') - * - * const key = createSecretKey(secret) - * - * const payload = { - * 'urn:example:claim': 'foo' - * } - * - * (async () => { - * const token = await V1.encrypt(payload, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * expiresIn: '2 hours' - * }) - * // v1.local.1X8AshBYnBXTevpH6s21lTZzPL8k-pVaRBsfU5uFfpDWAoG8NZAB5LwQgUpcsgAbZj-wpDMix1Mzw_viBbntWjqEZAVOe-BTMhVKSe43u3fUM2EfRcNFHzPVY_2I_CqGjhW2qs6twNvgv5kEhOiUnTSgZMtCn9h6L_KlKz8YrWcGdGypBYcs5ooMClKvOhb2_M8wHqG_PCgAkgO5PBbHk1g6UnTgGgztuEMrcchLd7UJqNDU2I7TyQ9x7ofvndE35ODYaf-SefrJb72tuXaUqFbkAwKPs77EwvnWE5dgo6bbsp5KMdxq - * })() - */ - function encrypt( - /** PASETO Payload claims or payload */ - payload: object | Buffer, - /** The secret key to encrypt with. Alternatively any input that works for `crypto.createSecretKey` */ - key: KeyObject | Buffer, - options?: ProduceOptions, - ): Promise; - - /** - * Verifies the claims and signature of a PASETO - * @example - * const { createPublicKey } = require('crypto') - * const { V1 } = require('paseto') - * - * const key = createPrivateKey(publicKey) - * - * const token = 'v1.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDctMDJUMTQ6MDI6MjIuNDg5WiIsImV4cCI6IjIwMTktMDctMDJUMTY6MDI6MjIuNDg5WiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifbCaLu19MdLxjrexKh4WTyKr6UoeXzDly_Po1ZNv4wD5CglfY84QqQYTGXLlcLAqZagM3cWJn6xge-lBlT63km6OtOsiWTaKOnYg4MBtQTKmLsjpehpPtDSl_39h2BenB-r911qjYwNNuaRukjrtSVKQtfxdoAoFKEz_eulsDTclEBV7bJrL9Bo0epkJhFShZ6-K8qNd6rTg6Q3YOZCheW1FqNjqfoUYJ9nqPZl2OVbcPdAW3HBeLJefmlL_QGVSRClE2MXOVDrcyf7vGZ0SIj3ylnr6jmEJpzG8o0ap7FblQZI3xp91e-gmw30o6njhSq1ZVWpLqp7FYzq0pknJzGE' - * - * (async () => { - * await V1.verify(token, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * clockTolerance: '1 min' - * }) - * // { - * // 'urn:example:claim': 'foo', - * // iat: '2019-07-02T14:02:22.489Z', - * // exp: '2019-07-02T16:02:22.489Z', - * // aud: 'urn:example:client', - * // iss: 'https://op.example.com' - * // } - * })() - */ - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptions, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptions, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptionsBuffer, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptionsBuffer, - ): Promise; - - /** - * Decrypts and validates the claims of a PASETO - * @example - * const { createSecretKey } = require('crypto') - * const { V1 } = require('paseto') - * - * const key = createSecretKey(secret) - * - * const token = 'v1.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDctMDJUMTQ6MDI6MjIuNDg5WiIsImV4cCI6IjIwMTktMDctMDJUMTY6MDI6MjIuNDg5WiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifbCaLu19MdLxjrexKh4WTyKr6UoeXzDly_Po1ZNv4wD5CglfY84QqQYTGXLlcLAqZagM3cWJn6xge-lBlT63km6OtOsiWTaKOnYg4MBtQTKmLsjpehpPtDSl_39h2BenB-r911qjYwNNuaRukjrtSVKQtfxdoAoFKEz_eulsDTclEBV7bJrL9Bo0epkJhFShZ6-K8qNd6rTg6Q3YOZCheW1FqNjqfoUYJ9nqPZl2OVbcPdAW3HBeLJefmlL_QGVSRClE2MXOVDrcyf7vGZ0SIj3ylnr6jmEJpzG8o0ap7FblQZI3xp91e-gmw30o6njhSq1ZVWpLqp7FYzq0pknJzGE' - * - * (async () => { - * await V1.decrypt(token, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * clockTolerance: '1 min' - * }) - * // { - * // 'urn:example:claim': 'foo', - * // iat: '2019-07-02T14:03:39.631Z', - * // exp: '2019-07-02T16:03:39.631Z', - * // aud: 'urn:example:client', - * // iss: 'https://op.example.com' - * // } - * })() - */ - function decrypt( - /** PASETO to decrypt and validate */ - token: string, - /** The secret key to decrypt with. Alternatively any input that works for `crypto.createSecretKey` */ - key: KeyObject | Buffer, - options?: ConsumeOptions, - ): Promise; - function decrypt( - /** PASETO to decrypt and validate */ - token: string, - /** The secret key to decrypt with. Alternatively any input that works for `crypto.createSecretKey` */ - key: KeyObject | Buffer, - options?: ConsumeOptions, - ): Promise; - function decrypt( - /** PASETO to decrypt and validate */ - token: string, - /** The secret key to decrypt with. Alternatively any input that works for `crypto.createSecretKey` */ - key: KeyObject | Buffer, - options?: ConsumeOptionsBuffer, - ): Promise; - function decrypt( - /** PASETO to decrypt and validate */ - token: string, - /** The secret key to decrypt with. Alternatively any input that works for `crypto.createSecretKey` */ - key: KeyObject | Buffer, - options?: ConsumeOptionsBuffer, - ): Promise; - - /** Generates a new secret or private key for a given purpose */ - function generateKey( - /** PASETO purpose */ - purpose: 'local' | 'public', - ): Promise; + function sign( + payload: object | Buffer, + key: KeyObject | PrivateKeyInput | JsonWebKeyInput, + options?: Omit, + ): Promise + function encrypt( + payload: object | Buffer, + key: KeyObject | Buffer, + options?: Omit, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: Omit, 'assertion'>, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: Omit, 'assertion'>, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: Omit, 'assertion'>, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: Omit, 'assertion'>, + ): Promise + function generateKey(purpose: 'local' | 'public'): Promise } - export namespace V2 { - /** - * Serializes and signs the payload as a PASETO using the provided private key - * @example - * const { createPrivateKey } = require('crypto') - * const { V2 } = require('paseto') - * - * const key = createPrivateKey(privateKey) - * - * const payload = { - * 'urn:example:claim': 'foo' - * } - * - * (async () => { - * const token = await V2.sign(payload, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * expiresIn: '2 hours' - * }) - * // v2.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDctMDJUMTM6MzY6MTIuMzgwWiIsImV4cCI6IjIwMTktMDctMDJUMTU6MzY6MTIuMzgwWiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifZfV2b1K3xbn8Az3aL24aPtqGRQ3dOf7DP3_GijBekGC2038REYwcyo1rv5o7OOjPuQ7-SqKhPKx0fn6hwm4nAw - * })() - */ - function sign( - /** PASETO Payload claims or payload */ - payload: object | Buffer, - /** The key to sign with. Alternatively any input that works for `crypto.createPrivateKey` */ - key: KeyObject | PrivateKeyInput, - options?: ProduceOptions, - ): Promise; - - /** - * Verifies the claims and signature of a PASETO - * @example - * const { createPublicKey } = require('crypto') - * const { V2 } = require('paseto') - * - * const key = createPrivateKey(publicKey) - * - * const token = 'v2.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDctMDJUMTM6MzY6MTIuMzgwWiIsImV4cCI6IjIwMTktMDctMDJUMTU6MzY6MTIuMzgwWiIsImF1ZCI6InVybjpleGFtcGxlOmNsaWVudCIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20ifZfV2b1K3xbn8Az3aL24aPtqGRQ3dOf7DP3_GijBekGC2038REYwcyo1rv5o7OOjPuQ7-SqKhPKx0fn6hwm4nAw' - * - * (async () => { - * await V2.verify(token, key, { - * audience: 'urn:example:client', - * issuer: 'https://op.example.com', - * clockTolerance: '1 min' - * }) - * // { - * // 'urn:example:claim': 'foo', - * // iat: '2019-07-02T13:36:12.380Z', - * // exp: '2019-07-02T15:36:12.380Z', - * // aud: 'urn:example:client', - * // iss: 'https://op.example.com' - * // } - * })() - */ - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptions, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptions, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptionsBuffer, - ): Promise; - function verify( - /** PASETO to verify */ - token: string, - /** The key to verify with. Alternatively any input that works for `crypto.createPublicKey` */ - key: KeyObject | PublicKeyInput, - options?: ConsumeOptionsBuffer, - ): Promise; - - /** Generates a new secret or private key for a given purpose */ - function generateKey( - /** PASETO purpose */ - purpose: 'public', - ): Promise; + function sign( + payload: object | Buffer, + key: KeyObject | PrivateKeyInput | JsonWebKeyInput, + options?: Omit, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: Omit, 'assertion'>, + ): Promise + function generateKey(purpose: 'public'): Promise +} +export namespace V3 { + function sign( + payload: object | Buffer, + key: KeyObject | PrivateKeyInput | JsonWebKeyInput, + options?: ProduceOptions, + ): Promise + function encrypt( + payload: object | Buffer, + key: KeyObject | Buffer, + options?: ProduceOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptionsBuffer, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptionsBuffer, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: ConsumeOptions, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: ConsumeOptions, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: ConsumeOptionsBuffer, + ): Promise + function decrypt( + token: string, + key: KeyObject | Buffer, + options?: ConsumeOptionsBuffer, + ): Promise + function generateKey(purpose: 'local' | 'public'): Promise +} +export namespace V4 { + function sign( + payload: object | Buffer, + key: KeyObject | PrivateKeyInput | JsonWebKeyInput, + options?: ProduceOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptions, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptionsBuffer, + ): Promise + function verify( + token: string, + key: KeyObject | PublicKeyInput | JsonWebKeyInput, + options?: ConsumeOptionsBuffer, + ): Promise + function generateKey(purpose: 'public'): Promise } - export namespace errors { - /** Base Error the others inherit from */ - class PasetoError extends Error {} - - /** - * Thrown when PASETO Claim is either of incorrect type or fails to validate by the provided options - * @example - * if (err.code === 'ERR_PASETO_CLAIM_INVALID') { - * // ... - * } - */ - class PasetoClaimInvalid extends PasetoError {} - /** - * Thrown when a PASETO decrypt operations are started but fail to decrypt. Only generic error message is provided - * @example - * if (err.code === 'ERR_PASETO_DECRYPTION_FAILED') { - * // ... - * } - */ - class PasetoDecryptionFailed extends PasetoError {} - /** - * Thrown when PASETO is not in a valid format - * @example - * if (err.code === 'ERR_PASETO_INVALID') { - * // ... - * } - */ - class PasetoInvalid extends PasetoError {} - /** - * Thrown when a particular feature, e.g. version, purpose or anything else is not supported - * @example - * if (err.code === 'ERR_PASETO_NOT_SUPPORTED') { - * // ... - * } - */ - class PasetoNotSupported extends PasetoError {} - /** - * Thrown when a PASETO verify operations are started but fail to verify. Only generic error message is provided - * @example - * if (err.code === 'ERR_PASETO_VERIFICATION_FAILED') { - * // ... - * } - */ - class PasetoVerificationFailed extends PasetoError {} + class PasetoError extends Error {} + class PasetoClaimInvalid extends PasetoError {} + class PasetoDecryptionFailed extends PasetoError {} + class PasetoInvalid extends PasetoError {} + class PasetoNotSupported extends PasetoError {} + class PasetoVerificationFailed extends PasetoError {} } diff --git a/types/paseto-tests.ts b/types/paseto-tests.ts index e67e77e..0cfb230 100644 --- a/types/paseto-tests.ts +++ b/types/paseto-tests.ts @@ -1,6 +1,5 @@ -import * as paseto from './index.d'; - -(async () => { +import * as paseto from './index.d' +;(async () => { { const key = await paseto.V2.generateKey('public') @@ -18,7 +17,7 @@ import * as paseto from './index.d'; kid: 'string', notBefore: 'string', now: new Date(), - subject: 'string' + subject: 'string', }) token.substring(0) @@ -63,7 +62,7 @@ import * as paseto from './index.d'; kid: 'string', notBefore: 'string', now: new Date(), - subject: 'string' + subject: 'string', }) token.substring(0) @@ -107,7 +106,7 @@ import * as paseto from './index.d'; kid: 'string', notBefore: 'string', now: new Date(), - subject: 'string' + subject: 'string', }) token.substring(0) diff --git a/types/tsconfig.json b/types/tsconfig.json index 64a89a6..1fa595c 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,23 +1,16 @@ { "compilerOptions": { "module": "commonjs", - "lib": [ - "es6" - ], + "lib": ["es6"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "baseUrl": "../", - "typeRoots": [ - "../" - ], + "typeRoots": ["../"], "types": [], "noEmit": true, "forceConsistentCasingInFileNames": true }, - "files": [ - "index.d.ts", - "paseto-tests.ts" - ] + "files": ["index.d.ts", "paseto-tests.ts"] }