Skip to content

Commit

Permalink
fix: remove parseKey, change ES256K and Ed25519 signers to Uint8Array…
Browse files Browse the repository at this point in the history
… only (#224)

* remove parseKey from Signers as well as base64, 58, and hex matchers

* update README and export functions that convert hex, base64, and base58 to bytes

* fix documentation for ES256KSigner and EdDSASigner

fixes #222

BREAKING CHANGE: The Signer classes exported by this library no longer accept private keys with string encodings, only `Uint8Array`. This reduces the potential ambiguity between different formats. Some utility methods are exported that allow users to convert some popular encodings to raw `Uint8Array`.
  • Loading branch information
bshambaugh committed Apr 4, 2022
1 parent 8082083 commit 9132caf
Show file tree
Hide file tree
Showing 13 changed files with 40 additions and 100 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ purposes only.

```js
const didJWT = require('did-jwt')
const signer = didJWT.ES256KSigner('278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f')
const signer = didJWT.ES256KSigner(didJWT.hexToBytes('278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'))

let jwt = await didJWT.createJWT(
{ aud: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74', exp: 1957463421, name: 'uPort Developer' },
Expand Down
15 changes: 8 additions & 7 deletions src/__tests__/ES256KSigner.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { hexToBytes, base58ToBytes, base64ToBytes } from '../util'
import { ES256KSigner } from '../signers/ES256KSigner'

describe('Secp256k1 Signer', () => {
it('signs data, given a hex private key', async () => {
expect.assertions(1)
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
const signer = ES256KSigner(privateKey)
const signer = ES256KSigner(hexToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
Expand All @@ -14,7 +15,7 @@ describe('Secp256k1 Signer', () => {
it('signs data: privateKey with 0x prefix', async () => {
expect.assertions(1)
const privateKey = '0x278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
const signer = ES256KSigner(privateKey)
const signer = ES256KSigner(hexToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
Expand All @@ -24,7 +25,7 @@ describe('Secp256k1 Signer', () => {
it('signs data: privateKey base58', async () => {
expect.assertions(1)
const privateKey = '3fMGokRKc5yGVqbCXyGNTrp3vP1cXs86tsVSVwzhNvXQ'
const signer = ES256KSigner(privateKey)
const signer = ES256KSigner(base58ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
Expand All @@ -34,7 +35,7 @@ describe('Secp256k1 Signer', () => {
it('signs data: privateKey base64url', async () => {
expect.assertions(1)
const privateKey = 'J4pd5wDin6ro5A42bsUBK17GPTbsd-iiQXFUzB0lOD8'
const signer = ES256KSigner(privateKey)
const signer = ES256KSigner(base64ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
Expand All @@ -44,7 +45,7 @@ describe('Secp256k1 Signer', () => {
it('signs data: privateKey base64', async () => {
expect.assertions(1)
const privateKey = 'J4pd5wDin6ro5A42bsUBK17GPTbsd+iiQXFUzB0lOD8='
const signer = ES256KSigner(privateKey)
const signer = ES256KSigner(base64ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
Expand All @@ -55,7 +56,7 @@ describe('Secp256k1 Signer', () => {
expect.assertions(1)
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d2538'
expect(() => {
ES256KSigner(privateKey)
ES256KSigner(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})

Expand All @@ -64,7 +65,7 @@ describe('Secp256k1 Signer', () => {
const privateKey =
'278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
expect(() => {
ES256KSigner(privateKey)
ES256KSigner(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})
})
15 changes: 8 additions & 7 deletions src/__tests__/EdDSASigner.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { base64ToBytes, base58ToBytes, hexToBytes } from '../util'
import { EdDSASigner } from '../signers/EdDSASigner'

describe('EdDSASigner', () => {
it('signs data with base64 key', async () => {
expect.assertions(1)
const privKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ=='
const signer = EdDSASigner(privKey)
const signer = EdDSASigner(base64ToBytes(privKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return expect(signer(plaintext)).resolves.toEqual(
'1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw'
Expand All @@ -14,7 +15,7 @@ describe('EdDSASigner', () => {
it('signs data with base64url key', async () => {
expect.assertions(1)
const privKey = 'nlXR4aofRVuLqtn9-XVQNlX4s1nVQvp-TOhBBtYls1IG-sHyIkDP_WN-rWZHGIQp-v2pyct-rkM4asF_YRFQdQ'
const signer = EdDSASigner(privKey)
const signer = EdDSASigner(base64ToBytes(privKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return expect(signer(plaintext)).resolves.toEqual(
'1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw'
Expand All @@ -24,7 +25,7 @@ describe('EdDSASigner', () => {
it('signs data with base58 key', async () => {
expect.assertions(1)
const privKey = '4AcB6rb1mUBf82U7pBzPZ53ZAQycdi4Q1LWoUREvHSRXBRo9Sus9bzCJPKVTQQeDpjHMJN7fBAGWKEnJw5SPbaC4'
const signer = EdDSASigner(privKey)
const signer = EdDSASigner(base58ToBytes(privKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return expect(signer(plaintext)).resolves.toEqual(
'1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw'
Expand All @@ -35,7 +36,7 @@ describe('EdDSASigner', () => {
expect.assertions(1)
const privKey =
'9e55d1e1aa1f455b8baad9fdf975503655f8b359d542fa7e4ce84106d625b35206fac1f22240cffd637ead6647188429fafda9c9cb7eae43386ac17f61115075'
const signer = EdDSASigner(privKey)
const signer = EdDSASigner(hexToBytes(privKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return expect(signer(plaintext)).resolves.toEqual(
'1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw'
Expand All @@ -46,7 +47,7 @@ describe('EdDSASigner', () => {
expect.assertions(1)
const privKey =
'0x9e55d1e1aa1f455b8baad9fdf975503655f8b359d542fa7e4ce84106d625b35206fac1f22240cffd637ead6647188429fafda9c9cb7eae43386ac17f61115075'
const signer = EdDSASigner(privKey)
const signer = EdDSASigner(hexToBytes(privKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return expect(signer(plaintext)).resolves.toEqual(
'1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw'
Expand All @@ -57,7 +58,7 @@ describe('EdDSASigner', () => {
expect.assertions(1)
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
expect(() => {
EdDSASigner(privateKey)
EdDSASigner(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})

Expand All @@ -66,7 +67,7 @@ describe('EdDSASigner', () => {
const privateKey =
'9e55d1e1aa1f455b8baad9fdf975503655f8b359d542fa7e4ce84106d625b35206fac1f22240cffd637ead6647188429fafda9c9cb7eae43386ac17f611150'
expect(() => {
EdDSASigner(privateKey)
EdDSASigner(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})
})
7 changes: 4 additions & 3 deletions src/__tests__/JWT.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hexToBytes, base64ToBytes } from '../util'
import { VerificationMethod } from 'did-resolver'
import { TokenVerifier } from 'jsontokens'
import MockDate from 'mockdate'
Expand Down Expand Up @@ -29,8 +30,8 @@ const alg = 'ES256K'
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
const publicKey = '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479'
const verifier = new TokenVerifier(alg, publicKey)
const signer = ES256KSigner(privateKey)
const recoverySigner = ES256KSigner(privateKey, true)
const signer = ES256KSigner(hexToBytes(privateKey))
const recoverySigner = ES256KSigner(hexToBytes(privateKey), true)

const publicKeyJwk = {
crv: 'secp256k1',
Expand Down Expand Up @@ -192,7 +193,7 @@ describe('createJWT()', () => {
describe('Ed25519', () => {
const ed25519PrivateKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ=='
const did = 'did:nacl:BvrB8iJAz_1jfq1mRxiEKfr9qcnLfq5DOGrBf2ERUHU'
const signer = EdDSASigner(ed25519PrivateKey)
const signer = EdDSASigner(base64ToBytes(ed25519PrivateKey))
const alg = 'Ed25519'
const resolver = {
resolve: jest.fn().mockReturnValue({
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/VerifierAlgorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ const eip155 = toEthereumAddress(publicKey)
const bip122 = toBip122Address(publicKey)
const cosmosPrefix = 'example'
const cosmos = toCosmosAddressWithoutPrefix(publicKey, cosmosPrefix)
const signer = ES256KSigner(privateKey)
const recoverySigner = ES256KSigner(privateKey, true)
const signer = ES256KSigner(hexToBytes(privateKey))
const recoverySigner = ES256KSigner(hexToBytes(privateKey), true)

const ed25519PrivateKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ=='
const edSigner = EdDSASigner(ed25519PrivateKey)
const edSigner = EdDSASigner(base64ToBytes(ed25519PrivateKey))
const edKp = nacl.sign.keyPair.fromSecretKey(base64ToBytes(ed25519PrivateKey))
const edPublicKey = bytesToBase64(edKp.publicKey)
const edPublicKey2 = bytesToBase64(nacl.sign.keyPair().publicKey)
Expand Down
37 changes: 0 additions & 37 deletions src/__tests__/util.test.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ export {
}

export { JWTOptions, JWTVerifyOptions } from './JWT'

export { base64ToBytes, base58ToBytes, hexToBytes } from './util'
8 changes: 4 additions & 4 deletions src/signers/ES256KSigner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseKey, leftpad } from '../util'
import { leftpad } from '../util'
import { toJose } from '../util'
import { Signer } from '../JWT'
import { sha256 } from '../Digest'
Expand All @@ -17,12 +17,12 @@ const secp256k1 = new elliptic.ec('secp256k1')
* const signature: string = await sign(data)
* ```
*
* @param {String} privateKey a private key as `Uint8Array` or encoded as `base64`, `base58`, or `hex` string
* @param {String} privateKey a private key as `Uint8Array`
* @param {Boolean} recoverable an optional flag to add the recovery param to the generated signatures
* @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>`
*/
export function ES256KSigner(privateKey: string | Uint8Array, recoverable = false): Signer {
const privateKeyBytes: Uint8Array = parseKey(privateKey)
export function ES256KSigner(privateKey: Uint8Array, recoverable = false): Signer {
const privateKeyBytes: Uint8Array = privateKey
if (privateKeyBytes.length !== 32) {
throw new Error(`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`)
}
Expand Down
8 changes: 4 additions & 4 deletions src/signers/EdDSASigner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sign } from '@stablelib/ed25519'
import { Signer } from '../JWT'
import { bytesToBase64url, parseKey, stringToBytes } from '../util'
import { bytesToBase64url, stringToBytes } from '../util'

/**
* Creates a configured signer function for signing data using the EdDSA (Ed25519) algorithm.
Expand All @@ -13,11 +13,11 @@ import { bytesToBase64url, parseKey, stringToBytes } from '../util'
* const signature: string = await sign(data)
* ```
*
* @param {String} secretKey a 64 byte secret key as `Uint8Array` or encoded as `base64`, `base58`, or `hex` string
* @param {String} secretKey a 64 byte secret key as `Uint8Array`
* @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>`
*/
export function EdDSASigner(secretKey: string | Uint8Array): Signer {
const privateKeyBytes: Uint8Array = parseKey(secretKey)
export function EdDSASigner(secretKey: Uint8Array): Signer {
const privateKeyBytes: Uint8Array = secretKey
if (privateKeyBytes.length !== 64) {
throw new Error(`bad_key: Invalid private key format. Expecting 64 bytes, but got ${privateKeyBytes.length}`)
}
Expand Down
3 changes: 2 additions & 1 deletion src/signers/EllipticSigner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hexToBytes } from '../util'
import { Signer } from '../JWT'
import { ES256KSigner } from './ES256KSigner'

Expand All @@ -17,7 +18,7 @@ import { ES256KSigner } from './ES256KSigner'
* @return {Function} a configured signer function
*/
function EllipticSigner(hexPrivateKey: string): Signer {
return ES256KSigner(hexPrivateKey)
return ES256KSigner(hexToBytes(hexPrivateKey))
}

export default EllipticSigner
3 changes: 2 additions & 1 deletion src/signers/NaclSigner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EdDSASigner as EdDSASigner } from './EdDSASigner'
import { Signer } from '../JWT'
import { base64ToBytes } from '../util'

/**
* @deprecated Please use EdDSASigner
Expand All @@ -20,7 +21,7 @@ import { Signer } from '../JWT'
*/

function NaclSigner(base64PrivateKey: string): Signer {
return EdDSASigner(base64PrivateKey)
return EdDSASigner(base64ToBytes(base64PrivateKey))
}

export default NaclSigner
4 changes: 2 additions & 2 deletions src/signers/SimpleSigner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fromJose } from '../util'
import { fromJose, hexToBytes } from '../util'
import { Signer } from '../JWT'
import { ES256KSigner } from './ES256KSigner'

Expand All @@ -16,7 +16,7 @@ import { ES256KSigner } from './ES256KSigner'
* @return {Function} a configured signer function
*/
function SimpleSigner(hexPrivateKey: string): Signer {
const signer = ES256KSigner(hexPrivateKey, true)
const signer = ES256KSigner(hexToBytes(hexPrivateKey), true)
return async (data) => {
const signature = (await signer(data)) as string
return fromJose(signature)
Expand Down
30 changes: 0 additions & 30 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,36 +84,6 @@ export function toSealed(ciphertext: string, tag: string): Uint8Array {
return u8a.concat([base64ToBytes(ciphertext), base64ToBytes(tag)])
}

const hexMatcher = /^(0x)?([a-fA-F0-9]{64}|[a-fA-F0-9]{128})$/
const base58Matcher = /^([1-9A-HJ-NP-Za-km-z]{44}|[1-9A-HJ-NP-Za-km-z]{88})$/
const base64Matcher = /^([0-9a-zA-Z=\-_+/]{43}|[0-9a-zA-Z=\-_+/]{86})(={0,2})$/

/**
* Parses a private key and returns the Uint8Array representation.
* This method uses an heuristic to determine the key encoding to then be able to parse it into 32 or 64 bytes.
*
* @param input a 32 or 64 byte key presented either as a Uint8Array or as a hex, base64, or base58btc encoded string
*
* @throws TypeError('Invalid private key format') if the key doesn't match any of the accepted formats or length
*/
export function parseKey(input: string | Uint8Array): Uint8Array {
if (typeof input === 'string') {
if (hexMatcher.test(input)) {
return hexToBytes(input)
} else if (base58Matcher.test(input)) {
return base58ToBytes(input)
} else if (base64Matcher.test(input)) {
return base64ToBytes(input)
} else {
throw TypeError('bad_key: Invalid private key format')
}
} else if (input instanceof Uint8Array) {
return input
} else {
throw TypeError('bad_key: Invalid private key format')
}
}

export function leftpad(data: string, size = 64): string {
if (data.length === size) return data
return '0'.repeat(size - data.length) + data
Expand Down

0 comments on commit 9132caf

Please sign in to comment.