Skip to content

Commit

Permalink
feat: support PASERK in key generation
Browse files Browse the repository at this point in the history
resolves #25
  • Loading branch information
panva committed Oct 18, 2021
1 parent f41bfc3 commit bffbda4
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 90 deletions.
38 changes: 26 additions & 12 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ If you or your business use paseto, please consider becoming a [sponsor][support
<!-- TOC V4 START -->
- [V4.sign(payload, key[, options])](#v4signpayload-key-options)
- [V4.verify(token, key[, options])](#v4verifytoken-key-options)
- [V4.generateKey(purpose)](#v4generatekeypurpose)
- [V4.generateKey(purpose[, options])](#v4generatekeypurpose-options)
- [V4.bytesToKeyObject(bytes)](#v4bytestokeyobjectbytes)
- [V4.keyObjectToBytes(keyObject)](#v4keyobjecttobyteskeyobject)
<!-- TOC V4 END -->
Expand Down Expand Up @@ -156,12 +156,15 @@ const token = 'v4.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMjEtMDc

---

#### V4.generateKey(purpose)
#### V4.generateKey(purpose[, options])

Generates a new secret or private key for a given purpose.

- `purpose`: `<string>` PASETO purpose, only 'public' is supported.
- Returns: `Promise<KeyObject>`
- `options`: `<Object>`
- `format`: `'keyobject'` (default) &vert; `'paserk'`.
- Returns: `Promise<KeyObject>` (when format is `'keyobject'`)
- Returns: `Promise<{ publicKey: string, secretKey: string }>` (when format is `'paserk'`)

---

Expand Down Expand Up @@ -207,7 +210,7 @@ Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public
- [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.generateKey(purpose[, options])](#v3generatekeypurpose-options)
- [V3.bytesToKeyObject(bytes)](#v3bytestokeyobjectbytes)
- [V3.keyObjectToBytes(keyObject)](#v3keyobjecttobyteskeyobject)
<!-- TOC V3 END -->
Expand Down Expand Up @@ -459,12 +462,16 @@ const token = 'v3.local.aY9txiwDEjnQpCUe2muaPlFSEHH7OTYcjv4GTEyiFvecI7Y4-0_msLxp

---

#### V3.generateKey(purpose)
#### V3.generateKey(purpose[, options])

Generates a new secret or private key for a given purpose.

- `purpose`: `<string>` PASETO purpose, either 'local' or 'public'
- Returns: `Promise<KeyObject>`
- `options`: `<Object>`
- `format`: `'keyobject'` (default) &vert; `'paserk'`.
- Returns: `Promise<KeyObject>` (when format is `'keyobject'`)
- Returns: `Promise<{ publicKey: string, secretKey: string }>` (when format is `'paserk'` and purpose is `'public'`)
- Returns: `Promise<{ string }>` (when format is `'paserk'` and purpose is `'local'`)

---

Expand Down Expand Up @@ -508,7 +515,7 @@ Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public
<!-- TOC V2 START -->
- [V2.sign(payload, key[, options])](#v2signpayload-key-options)
- [V2.verify(token, key[, options])](#v2verifytoken-key-options)
- [V2.generateKey(purpose)](#v2generatekeypurpose)
- [V2.generateKey(purpose[, options])](#v2generatekeypurpose-options)
- [V2.bytesToKeyObject(bytes)](#v2bytestokeyobjectbytes)
- [V2.keyObjectToBytes(keyObject)](#v2keyobjecttobyteskeyobject)
<!-- TOC V2 END -->
Expand Down Expand Up @@ -640,12 +647,15 @@ const token = 'v2.public.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6ImZvbyIsImlhdCI6IjIwMTktMDc

---

#### V2.generateKey(purpose)
#### V2.generateKey(purpose[, options])

Generates a new secret or private key for a given purpose.

- `purpose`: `<string>` PASETO purpose, only 'public' is supported.
- Returns: `Promise<KeyObject>`
- `options`: `<Object>`
- `format`: `'keyobject'` (default) &vert; `'paserk'`.
- Returns: `Promise<KeyObject>` (when format is `'keyobject'`)
- Returns: `Promise<{ publicKey: string, secretKey: string }>` (when format is `'paserk'`)

---

Expand Down Expand Up @@ -691,7 +701,7 @@ Use `crypto.createPublicKey(keyObject)` to turn a private KeyObject to a public
- [V1.verify(token, key[, options])](#v1verifytoken-key-options)
- [V1.encrypt(payload, key[, options])](#v1encryptpayload-key-options)
- [V1.decrypt(token, key[, options])](#v1decrypttoken-key-options)
- [V1.generateKey(purpose)](#v1generatekeypurpose)
- [V1.generateKey(purpose[, options])](#v1generatekeypurpose-options)
<!-- TOC V1 END -->


Expand Down Expand Up @@ -935,12 +945,16 @@ const token = 'v1.local.1X8AshBYnBXTevpH6s21lTZzPL8k-pVaRBsfU5uFfpDWAoG8NZAB5LwQ

---

#### V1.generateKey(purpose)
#### V1.generateKey(purpose[, options])

Generates a new secret or private key for a given purpose.

- `purpose`: `<string>` PASETO purpose, either 'local' or 'public'
- Returns: `Promise<KeyObject>`
- `options`: `<Object>`
- `format`: `'keyobject'` (default) &vert; `'paserk'`.
- Returns: `Promise<KeyObject>` (when format is `'keyobject'`)
- Returns: `Promise<{ publicKey: string, secretKey: string }>` (when format is `'paserk'` and purpose is `'public'`)
- Returns: `Promise<{ string }>` (when format is `'paserk'` and purpose is `'local'`)

---

Expand Down
24 changes: 20 additions & 4 deletions lib/v1/key.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,28 @@ const { PasetoNotSupported } = require('../errors')
const generateKeyPair = promisify(crypto.generateKeyPair)
const generateSecretKey = promisify(crypto.generateKey)

async function generateKey(purpose) {
async function generateKey(purpose, { format = 'keyobject' } = {}) {
if (format !== 'keyobject' && format !== 'paserk') throw new TypeError('invalid format')
switch (purpose) {
case 'local':
return generateSecretKey('aes', { length: 256 })
case 'local': {
const keyobject = await generateSecretKey('aes', { length: 256 })
if (format === 'paserk') {
return `k1.local.${keyobject.export().toString('base64url')}`
}
return keyobject
}
case 'public': {
const { privateKey } = await generateKeyPair('rsa', { modulusLength: 2048 })
const { privateKey, publicKey } = await generateKeyPair('rsa', { modulusLength: 2048 })
if (format === 'paserk') {
return {
secretKey: `k1.secret.${privateKey
.export({ format: 'der', type: 'pkcs1' })
.toString('base64url')}`,
publicKey: `k1.public.${publicKey
.export({ format: 'der', type: 'pkcs1' })
.toString('base64url')}`,
}
}
return privateKey
}
default:
Expand Down
11 changes: 9 additions & 2 deletions lib/v2/key.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ function _checkPublicKey(v, key) {
return key
}

async function _generateKey(v, purpose) {
async function _generateKey(v, purpose, { format = 'keyobject' } = {}) {
if (format !== 'keyobject' && format !== 'paserk') throw new TypeError('invalid format')
switch (purpose) {
case 'public': {
const { privateKey } = await generateKeyPair('ed25519')
const { privateKey, publicKey } = await generateKeyPair('ed25519')
if (format === 'paserk') {
return {
secretKey: `k${v.substr(1)}.secret.${keyObjectToBytes(privateKey).toString('base64url')}`,
publicKey: `k${v.substr(1)}.public.${keyObjectToBytes(publicKey).toString('base64url')}`,
}
}
return privateKey
}
default:
Expand Down
20 changes: 16 additions & 4 deletions lib/v3/key.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ const compressPk = require('../help/compress_pk')
const generateKeyPair = promisify(crypto.generateKeyPair)
const generateSecretKey = promisify(crypto.generateKey)

async function generateKey(purpose) {
async function generateKey(purpose, { format = 'keyobject' } = {}) {
if (format !== 'keyobject' && format !== 'paserk') throw new TypeError('invalid format')
switch (purpose) {
case 'local':
return generateSecretKey('aes', { length: 256 })
case 'local': {
const keyobject = await generateSecretKey('aes', { length: 256 })
if (format === 'paserk') {
return `k3.local.${keyobject.export().toString('base64url')}`
}
return keyobject
}
case 'public': {
const { privateKey } = await generateKeyPair('ec', { namedCurve: 'P-384' })
const { privateKey, publicKey } = await generateKeyPair('ec', { namedCurve: 'P-384' })
if (format === 'paserk') {
return {
secretKey: `k3.secret.${keyObjectToBytes(privateKey).toString('base64url')}`,
publicKey: `k3.public.${keyObjectToBytes(publicKey).toString('base64url')}`,
}
}
return privateKey
}
default:
Expand Down
140 changes: 72 additions & 68 deletions test/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,100 +8,104 @@ for (const [version, { sign, verify, encrypt, decrypt, generateKey }] of Object.
([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')

for (const [pk, sk] of [
await generateKey('public').then((sk) => [createPublicKey(sk), sk]),
await generateKey('public', { format: 'paserk' }).then((kp) => [kp.publicKey, kp.secretKey]),
]) {
const footer = 'footer'
const payload = { foo: 'bar' }
const encryptOptions = { footer }
const decryptOptions = {}
const signOptions = { footer }
const verifyOptions = {}
if (version === 'V3' || version === 'V4') {
encryptOptions.assertion = `${version.toLowerCase()}.local.`
decryptOptions.assertion = encryptOptions.assertion
signOptions.assertion = `${version.toLowerCase()}.public.`
verifyOptions.assertion = signOptions.assertion
}

const [token] = await Promise.all([
encrypt(payload, sk, { ...encryptOptions, iat: false }),
encrypt(Buffer.from(JSON.stringify(payload)), sk, encryptOptions),
sign(payload, sk, { ...signOptions, iat: false }),
sign(Buffer.from(JSON.stringify(payload)), sk, signOptions),
])

t.deepEqual(decode(token), {
payload: undefined,
purpose: 'local',
payload,
purpose: 'public',
version: version.toLowerCase(),
footer: Buffer.from(footer),
})

t.deepEqual(await decrypt(token, sk, { ...decryptOptions }), payload)
t.deepEqual(await decrypt(token, sk, { ...decryptOptions, complete: true }), {
t.deepEqual(await verify(token, pk, { ...verifyOptions }), payload)
t.deepEqual(await verify(token, pk, { ...verifyOptions, complete: true }), {
payload,
purpose: 'local',
purpose: 'public',
version: version.toLowerCase(),
footer: Buffer.from(footer),
})
t.deepEqual(await decrypt(token, sk, { ...decryptOptions, complete: true, buffer: true }), {
t.deepEqual(await verify(token, pk, { ...verifyOptions, complete: true, buffer: true }), {
payload: Buffer.from(JSON.stringify(payload)),
purpose: 'local',
purpose: 'public',
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(verify(token, pk), { code: 'ERR_PASETO_VERIFICATION_FAILED' })
}

await t.throwsAsync(decrypt(token, await generateKey('local')), {
code: 'ERR_PASETO_DECRYPTION_FAILED',
await t.throwsAsync(verify(token, await generateKey('public')), {
code: 'ERR_PASETO_VERIFICATION_FAILED',
})
}
})

if (encrypt) {
test(`${version.toLowerCase()}.local.`, async (t) => {
for (const sk of [
await generateKey('local'),
await generateKey('local', { format: 'paserk' }),
]) {
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',
})
}
})
}
}
Loading

0 comments on commit bffbda4

Please sign in to comment.