Skip to content

Commit

Permalink
feat: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed Apr 1, 2020
1 parent 6c575c1 commit 90db8f0
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 17 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# @chiffre/template-library
# @chiffre/crypto-sign

[![NPM](https://img.shields.io/npm/v/@chiffre/template-library?color=red)](https://www.npmjs.com/package/@chiffre/template-library)
[![MIT License](https://img.shields.io/github/license/chiffre-io/template-library.svg?color=blue)](https://github.com/chiffre-io/template-library/blob/next/LICENSE)
[![Continuous Integration](https://github.com/chiffre-io/template-library/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/template-library/actions)
[![Coverage Status](https://coveralls.io/repos/github/chiffre-io/template-library/badge.svg?branch=next)](https://coveralls.io/github/chiffre-io/template-library?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/template-library)](https://dependabot.com)
[![NPM](https://img.shields.io/npm/v/@chiffre/crypto-sign?color=red)](https://www.npmjs.com/package/@chiffre/crypto-sign)
[![MIT License](https://img.shields.io/github/license/chiffre-io/crypto-sign.svg?color=blue)](https://github.com/chiffre-io/crypto-sign/blob/next/LICENSE)
[![Continuous Integration](https://github.com/chiffre-io/crypto-sign/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/crypto-sign/actions)
[![Coverage Status](https://coveralls.io/repos/github/chiffre-io/crypto-sign/badge.svg?branch=next)](https://coveralls.io/github/chiffre-io/crypto-sign?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/crypto-sign)](https://dependabot.com)

Template for Chiffre libraries
Lightweight serialization for TweetNaCl signatures

## License

[MIT](https://github.com/chiffre-io/template-library/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
[MIT](https://github.com/chiffre-io/crypto-sign/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@chiffre/template-library",
"name": "@chiffre/crypto-sign",
"version": "0.0.0-semantically-released",
"description": "Template for Chiffre libraries",
"description": "Lightweight serialization for TweetNaCl signatures",
"main": "dist/index.js",
"license": "MIT",
"author": {
Expand All @@ -11,11 +11,12 @@
},
"repository": {
"type": "git",
"url": "https://github.com/chiffre-io/template-library"
"url": "https://github.com/chiffre-io/crypto-sign"
},
"keywords": [
"chiffre",
"template"
"tweetnacl",
"signature"
],
"publishConfig": {
"access": "public"
Expand All @@ -28,7 +29,10 @@
"build": "run-s build:clean build:ts",
"ci": "run-s build test"
},
"dependencies": {},
"dependencies": {
"@47ng/codec": "^0.5.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.1.4",
Expand Down
110 changes: 107 additions & 3 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,109 @@
import hello from './index'
import {
generateKeys,
importKeys,
parsePublicKey,
parseSecretKey,
signUtf8String,
publicKeyRegex,
secretKeyRegex,
signatureRegex,
verifySignature
} from './index'

test('testing works', () => {
expect(hello('World')).toEqual('Hello, World !')
test('regex', () => {
const keys = generateKeys()
expect(keys.public).toMatch(publicKeyRegex)
expect(keys.secret).toMatch(secretKeyRegex)
expect(keys.public).not.toMatch(secretKeyRegex)
expect(keys.secret).not.toMatch(publicKeyRegex)
const message = signUtf8String('', keys.raw.secretKey)
expect(message).toMatch(signatureRegex)
})

test('generate keys', () => {
const keys = generateKeys()
expect(parsePublicKey(keys.public)).toEqual(keys.raw.publicKey)
expect(parseSecretKey(keys.secret)).toEqual(keys.raw.secretKey)
const copy = importKeys(keys.secret)
expect(keys.public).toEqual(copy.public)
expect(keys.secret).toEqual(copy.secret)
expect(keys.public).not.toEqual(copy.secret)
expect(keys.secret).not.toEqual(copy.public)
expect(keys.raw.publicKey).toEqual(copy.raw.publicKey)
expect(keys.raw.secretKey).toEqual(copy.raw.secretKey)
expect(keys.raw.publicKey).not.toEqual(copy.raw.secretKey)
expect(keys.raw.secretKey).not.toEqual(copy.raw.publicKey)
})

test('codec', () => {
const keys = generateKeys()
const expected = 'Hello, World !'
const signature = signUtf8String(expected, keys.raw.secretKey)
const received = verifySignature(signature, keys.raw.publicKey)
expect(received).toEqual(expected)
})

test('Known test vector', () => {
const secretKey =
'ssk.IPwaySrr89g2ymWBrqC81qk7NCmenVN_gFmiz9gtAuTWGhwBv-mSUWbFeS9Zk00Iir8z2GM5Eue4v39FEpOiFw'
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
const message =
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh=='
const keys = importKeys(secretKey)
expect(keys.public).toEqual(publicKey)
const expected = 'Hello, World !'
const received = verifySignature(message, keys.raw.publicKey)
expect(received).toEqual(expected)
})

test('Known test vector, no padding', () => {
const secretKey =
'ssk.IPwaySrr89g2ymWBrqC81qk7NCmenVN_gFmiz9gtAuTWGhwBv-mSUWbFeS9Zk00Iir8z2GM5Eue4v39FEpOiFw'
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
const message =
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh'
const keys = importKeys(secretKey)
expect(keys.public).toEqual(publicKey)
const expected = 'Hello, World !'
const received = verifySignature(message, keys.raw.publicKey)
expect(received).toEqual(expected)
})

// Failure cases --

test('Invalid public key parsing', () => {
const run = () => parsePublicKey('not a public key')
expect(run).toThrowError('Invalid public key format')
})

test('Invalid secret key parsing', () => {
const run = () => parseSecretKey('not a secret key')
expect(run).toThrowError('Invalid secret key format')
})

test('Invalid secret key parsing', () => {
const run = () => parseSecretKey('not a secret key')
expect(run).toThrowError('Invalid secret key format')
})

test('Import keys from invalid secret key', () => {
const run = () => importKeys('not a secret key')
expect(run).toThrowError('Invalid secret key format')
})

test('Verify signature from wrong public key', () => {
const publicKey = 'spk.thisisnotthecorrectpublickeyforthismessage_'
const message =
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh'
const pk = parsePublicKey(publicKey)
const run = () => verifySignature(message, pk)
expect(run).toThrowError('Failed to verify signature')
})

test('Verify signature with invalid format', () => {
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
const message = 'not an actual message'
const pk = parsePublicKey(publicKey)
const run = () => verifySignature(message, pk)
expect(run).toThrowError('Invalid secret key format')
})
101 changes: 100 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,100 @@
export default (name: string) => `Hello, ${name} !`
import nacl from 'tweetnacl'
import utf8 from '@47ng/codec/dist/utf8'
import b64 from '@47ng/codec/dist/b64'

// --

export interface SignatureKeys {
public: string
secret: string
raw: nacl.SignKeyPair
}

// --

export const publicKeyRegex = /^spk\.([a-zA-Z0-9-_]{43})$/
export const secretKeyRegex = /^ssk\.([a-zA-Z0-9-_]{86})$/
export const signatureRegex = /^v1\.naclsig\.([a-zA-Z0-9-_]{86,}={0,2})$/

// --

export function serializePublicKey(key: Uint8Array) {
return `spk.${b64.encode(key).replace(/\=/g, '')}`
}

export function serializeSecretKey(key: Uint8Array) {
return `ssk.${b64.encode(key).replace(/\=/g, '')}`
}

export function serializeSignature(input: Uint8Array) {
return `v1.naclsig.${b64.encode(input).replace(/\=/g, '')}`
}

// --

export function parsePublicKey(key: string): Uint8Array {
const match = key.match(publicKeyRegex)
if (!match) {
throw new Error('Invalid public key format')
}
return b64.decode(match[1])
}

export function parseSecretKey(key: string): Uint8Array {
const match = key.match(secretKeyRegex)
if (!match) {
throw new Error('Invalid secret key format')
}
return b64.decode(match[1])
}

export function parseSignature(sig: string): Uint8Array {
const match = sig.match(signatureRegex)
if (!match) {
throw new Error('Invalid secret key format')
}
return b64.decode(match[1])
}

// --

export function generateKeys(): Readonly<SignatureKeys> {
const keyPair = nacl.sign.keyPair()
return {
public: serializePublicKey(keyPair.publicKey),
secret: serializeSecretKey(keyPair.secretKey),
raw: keyPair
}
}

export function importKeys(secretKey: string): Readonly<SignatureKeys> {
const secretKeyBuffer = parseSecretKey(secretKey)
const keyPair = nacl.sign.keyPair.fromSecretKey(secretKeyBuffer)
return {
public: serializePublicKey(keyPair.publicKey),
secret: serializeSecretKey(keyPair.secretKey),
raw: keyPair
}
}

// --

export function signUtf8String(input: string, secretKey: Uint8Array) {
return signBuffer(utf8.encode(input), secretKey)
}

export function signBuffer(input: Uint8Array, secretKey: Uint8Array) {
const sig = nacl.sign(input, secretKey)
return serializeSignature(sig)
}

// --

export function verifySignature(input: string, publicKey: Uint8Array) {
const sig = parseSignature(input)
const msg = nacl.sign.open(sig, publicKey)
if (msg === null) {
throw new Error('Failed to verify signature')
}
return utf8.decode(msg)
}
23 changes: 23 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
# yarn lockfile v1


"@47ng/codec@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@47ng/codec/-/codec-0.5.0.tgz#28294e5c76170ed9a1403a5bbbb9e129c11b94ce"
integrity sha512-k/+OCuagLqv52RLrbzrRv+4qL68Y/1xm1EGu4N0nUXTjzVHZ6YQmo/aWNDAChcL+CP+fyNq1BDqLttHDzbfzSA==
dependencies:
"@stablelib/base64" "^1.0.0"
"@stablelib/hex" "^1.0.0"

"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
Expand Down Expand Up @@ -486,6 +494,16 @@
dependencies:
type-detect "4.0.8"

"@stablelib/base64@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.0.tgz#e08ba78078c731cbbb244530b1750659c52ba7cb"
integrity sha512-s/wTc/3+vYSalh4gfayJrupzhT7SDBqNtiYOeEMlkSDqL/8cExh5FAeTzLpmYq+7BLLv36EjBL5xrb0bUHWJWQ==

"@stablelib/hex@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@stablelib/hex/-/hex-1.0.0.tgz#9f2d21d412803e72a3bbc0ab4690e9bda0ca91cf"
integrity sha512-EJ9oGiuaFw/Y0cBATTxo73sgqOgdnSmZ9ftU9FND9SD51OM8wvAfS78uPy3oBNmLWc/sZK5twMbEFf/A4T2F8A==

"@types/babel__core@^7.1.0":
version "7.1.6"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610"
Expand Down Expand Up @@ -4151,6 +4169,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=

tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==

type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
Expand Down

0 comments on commit 90db8f0

Please sign in to comment.