From 90db8f0fddfab6bc4a0344137e3abe502cd80dd0 Mon Sep 17 00:00:00 2001 From: Francois Best Date: Wed, 1 Apr 2020 18:00:11 +0200 Subject: [PATCH] feat: Initial commit --- README.md | 16 +++---- package.json | 14 +++--- src/index.test.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 101 +++++++++++++++++++++++++++++++++++++++++- yarn.lock | 23 ++++++++++ 5 files changed, 247 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4f3db0e..c621838 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/package.json b/package.json index cb58e9d..9cd0e19 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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" @@ -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", diff --git a/src/index.test.ts b/src/index.test.ts index f1313cf..5de91f9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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') }) diff --git a/src/index.ts b/src/index.ts index b21024d..8e488e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { + const keyPair = nacl.sign.keyPair() + return { + public: serializePublicKey(keyPair.publicKey), + secret: serializeSecretKey(keyPair.secretKey), + raw: keyPair + } +} + +export function importKeys(secretKey: string): Readonly { + 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) +} diff --git a/yarn.lock b/yarn.lock index 99617c8..98c39d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"