Skip to content

Commit 90db8f0

Browse files
committed
feat: Initial commit
1 parent 6c575c1 commit 90db8f0

File tree

5 files changed

+247
-17
lines changed

5 files changed

+247
-17
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
# @chiffre/template-library
1+
# @chiffre/crypto-sign
22

3-
[![NPM](https://img.shields.io/npm/v/@chiffre/template-library?color=red)](https://www.npmjs.com/package/@chiffre/template-library)
4-
[![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)
5-
[![Continuous Integration](https://github.com/chiffre-io/template-library/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/template-library/actions)
6-
[![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)
7-
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/template-library)](https://dependabot.com)
3+
[![NPM](https://img.shields.io/npm/v/@chiffre/crypto-sign?color=red)](https://www.npmjs.com/package/@chiffre/crypto-sign)
4+
[![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)
5+
[![Continuous Integration](https://github.com/chiffre-io/crypto-sign/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/crypto-sign/actions)
6+
[![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)
7+
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/crypto-sign)](https://dependabot.com)
88

9-
Template for Chiffre libraries
9+
Lightweight serialization for TweetNaCl signatures
1010

1111
## License
1212

13-
[MIT](https://github.com/chiffre-io/template-library/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
13+
[MIT](https://github.com/chiffre-io/crypto-sign/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).

package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "@chiffre/template-library",
2+
"name": "@chiffre/crypto-sign",
33
"version": "0.0.0-semantically-released",
4-
"description": "Template for Chiffre libraries",
4+
"description": "Lightweight serialization for TweetNaCl signatures",
55
"main": "dist/index.js",
66
"license": "MIT",
77
"author": {
@@ -11,11 +11,12 @@
1111
},
1212
"repository": {
1313
"type": "git",
14-
"url": "https://github.com/chiffre-io/template-library"
14+
"url": "https://github.com/chiffre-io/crypto-sign"
1515
},
1616
"keywords": [
1717
"chiffre",
18-
"template"
18+
"tweetnacl",
19+
"signature"
1920
],
2021
"publishConfig": {
2122
"access": "public"
@@ -28,7 +29,10 @@
2829
"build": "run-s build:clean build:ts",
2930
"ci": "run-s build test"
3031
},
31-
"dependencies": {},
32+
"dependencies": {
33+
"@47ng/codec": "^0.5.0",
34+
"tweetnacl": "^1.0.3"
35+
},
3236
"devDependencies": {
3337
"@commitlint/config-conventional": "^8.3.4",
3438
"@types/jest": "^25.1.4",

src/index.test.ts

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,109 @@
1-
import hello from './index'
1+
import {
2+
generateKeys,
3+
importKeys,
4+
parsePublicKey,
5+
parseSecretKey,
6+
signUtf8String,
7+
publicKeyRegex,
8+
secretKeyRegex,
9+
signatureRegex,
10+
verifySignature
11+
} from './index'
212

3-
test('testing works', () => {
4-
expect(hello('World')).toEqual('Hello, World !')
13+
test('regex', () => {
14+
const keys = generateKeys()
15+
expect(keys.public).toMatch(publicKeyRegex)
16+
expect(keys.secret).toMatch(secretKeyRegex)
17+
expect(keys.public).not.toMatch(secretKeyRegex)
18+
expect(keys.secret).not.toMatch(publicKeyRegex)
19+
const message = signUtf8String('', keys.raw.secretKey)
20+
expect(message).toMatch(signatureRegex)
21+
})
22+
23+
test('generate keys', () => {
24+
const keys = generateKeys()
25+
expect(parsePublicKey(keys.public)).toEqual(keys.raw.publicKey)
26+
expect(parseSecretKey(keys.secret)).toEqual(keys.raw.secretKey)
27+
const copy = importKeys(keys.secret)
28+
expect(keys.public).toEqual(copy.public)
29+
expect(keys.secret).toEqual(copy.secret)
30+
expect(keys.public).not.toEqual(copy.secret)
31+
expect(keys.secret).not.toEqual(copy.public)
32+
expect(keys.raw.publicKey).toEqual(copy.raw.publicKey)
33+
expect(keys.raw.secretKey).toEqual(copy.raw.secretKey)
34+
expect(keys.raw.publicKey).not.toEqual(copy.raw.secretKey)
35+
expect(keys.raw.secretKey).not.toEqual(copy.raw.publicKey)
36+
})
37+
38+
test('codec', () => {
39+
const keys = generateKeys()
40+
const expected = 'Hello, World !'
41+
const signature = signUtf8String(expected, keys.raw.secretKey)
42+
const received = verifySignature(signature, keys.raw.publicKey)
43+
expect(received).toEqual(expected)
44+
})
45+
46+
test('Known test vector', () => {
47+
const secretKey =
48+
'ssk.IPwaySrr89g2ymWBrqC81qk7NCmenVN_gFmiz9gtAuTWGhwBv-mSUWbFeS9Zk00Iir8z2GM5Eue4v39FEpOiFw'
49+
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
50+
const message =
51+
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh=='
52+
const keys = importKeys(secretKey)
53+
expect(keys.public).toEqual(publicKey)
54+
const expected = 'Hello, World !'
55+
const received = verifySignature(message, keys.raw.publicKey)
56+
expect(received).toEqual(expected)
57+
})
58+
59+
test('Known test vector, no padding', () => {
60+
const secretKey =
61+
'ssk.IPwaySrr89g2ymWBrqC81qk7NCmenVN_gFmiz9gtAuTWGhwBv-mSUWbFeS9Zk00Iir8z2GM5Eue4v39FEpOiFw'
62+
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
63+
const message =
64+
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh'
65+
const keys = importKeys(secretKey)
66+
expect(keys.public).toEqual(publicKey)
67+
const expected = 'Hello, World !'
68+
const received = verifySignature(message, keys.raw.publicKey)
69+
expect(received).toEqual(expected)
70+
})
71+
72+
// Failure cases --
73+
74+
test('Invalid public key parsing', () => {
75+
const run = () => parsePublicKey('not a public key')
76+
expect(run).toThrowError('Invalid public key format')
77+
})
78+
79+
test('Invalid secret key parsing', () => {
80+
const run = () => parseSecretKey('not a secret key')
81+
expect(run).toThrowError('Invalid secret key format')
82+
})
83+
84+
test('Invalid secret key parsing', () => {
85+
const run = () => parseSecretKey('not a secret key')
86+
expect(run).toThrowError('Invalid secret key format')
87+
})
88+
89+
test('Import keys from invalid secret key', () => {
90+
const run = () => importKeys('not a secret key')
91+
expect(run).toThrowError('Invalid secret key format')
92+
})
93+
94+
test('Verify signature from wrong public key', () => {
95+
const publicKey = 'spk.thisisnotthecorrectpublickeyforthismessage_'
96+
const message =
97+
'v1.naclsig.5InC2t-TYSFwIFOv-M2nY2zvPYD_ZVExkUqx3bxBYwJSVMvJOZqrJnrUmLuXZsmUHNO6xHX_WdbphwIih_2wD0hlbGxvLCBXb3JsZCAh'
98+
const pk = parsePublicKey(publicKey)
99+
const run = () => verifySignature(message, pk)
100+
expect(run).toThrowError('Failed to verify signature')
101+
})
102+
103+
test('Verify signature with invalid format', () => {
104+
const publicKey = 'spk.1hocAb_pklFmxXkvWZNNCIq_M9hjORLnuL9_RRKTohc'
105+
const message = 'not an actual message'
106+
const pk = parsePublicKey(publicKey)
107+
const run = () => verifySignature(message, pk)
108+
expect(run).toThrowError('Invalid secret key format')
5109
})

src/index.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,100 @@
1-
export default (name: string) => `Hello, ${name} !`
1+
import nacl from 'tweetnacl'
2+
import utf8 from '@47ng/codec/dist/utf8'
3+
import b64 from '@47ng/codec/dist/b64'
4+
5+
// --
6+
7+
export interface SignatureKeys {
8+
public: string
9+
secret: string
10+
raw: nacl.SignKeyPair
11+
}
12+
13+
// --
14+
15+
export const publicKeyRegex = /^spk\.([a-zA-Z0-9-_]{43})$/
16+
export const secretKeyRegex = /^ssk\.([a-zA-Z0-9-_]{86})$/
17+
export const signatureRegex = /^v1\.naclsig\.([a-zA-Z0-9-_]{86,}={0,2})$/
18+
19+
// --
20+
21+
export function serializePublicKey(key: Uint8Array) {
22+
return `spk.${b64.encode(key).replace(/\=/g, '')}`
23+
}
24+
25+
export function serializeSecretKey(key: Uint8Array) {
26+
return `ssk.${b64.encode(key).replace(/\=/g, '')}`
27+
}
28+
29+
export function serializeSignature(input: Uint8Array) {
30+
return `v1.naclsig.${b64.encode(input).replace(/\=/g, '')}`
31+
}
32+
33+
// --
34+
35+
export function parsePublicKey(key: string): Uint8Array {
36+
const match = key.match(publicKeyRegex)
37+
if (!match) {
38+
throw new Error('Invalid public key format')
39+
}
40+
return b64.decode(match[1])
41+
}
42+
43+
export function parseSecretKey(key: string): Uint8Array {
44+
const match = key.match(secretKeyRegex)
45+
if (!match) {
46+
throw new Error('Invalid secret key format')
47+
}
48+
return b64.decode(match[1])
49+
}
50+
51+
export function parseSignature(sig: string): Uint8Array {
52+
const match = sig.match(signatureRegex)
53+
if (!match) {
54+
throw new Error('Invalid secret key format')
55+
}
56+
return b64.decode(match[1])
57+
}
58+
59+
// --
60+
61+
export function generateKeys(): Readonly<SignatureKeys> {
62+
const keyPair = nacl.sign.keyPair()
63+
return {
64+
public: serializePublicKey(keyPair.publicKey),
65+
secret: serializeSecretKey(keyPair.secretKey),
66+
raw: keyPair
67+
}
68+
}
69+
70+
export function importKeys(secretKey: string): Readonly<SignatureKeys> {
71+
const secretKeyBuffer = parseSecretKey(secretKey)
72+
const keyPair = nacl.sign.keyPair.fromSecretKey(secretKeyBuffer)
73+
return {
74+
public: serializePublicKey(keyPair.publicKey),
75+
secret: serializeSecretKey(keyPair.secretKey),
76+
raw: keyPair
77+
}
78+
}
79+
80+
// --
81+
82+
export function signUtf8String(input: string, secretKey: Uint8Array) {
83+
return signBuffer(utf8.encode(input), secretKey)
84+
}
85+
86+
export function signBuffer(input: Uint8Array, secretKey: Uint8Array) {
87+
const sig = nacl.sign(input, secretKey)
88+
return serializeSignature(sig)
89+
}
90+
91+
// --
92+
93+
export function verifySignature(input: string, publicKey: Uint8Array) {
94+
const sig = parseSignature(input)
95+
const msg = nacl.sign.open(sig, publicKey)
96+
if (msg === null) {
97+
throw new Error('Failed to verify signature')
98+
}
99+
return utf8.decode(msg)
100+
}

yarn.lock

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
# yarn lockfile v1
33

44

5+
"@47ng/codec@^0.5.0":
6+
version "0.5.0"
7+
resolved "https://registry.yarnpkg.com/@47ng/codec/-/codec-0.5.0.tgz#28294e5c76170ed9a1403a5bbbb9e129c11b94ce"
8+
integrity sha512-k/+OCuagLqv52RLrbzrRv+4qL68Y/1xm1EGu4N0nUXTjzVHZ6YQmo/aWNDAChcL+CP+fyNq1BDqLttHDzbfzSA==
9+
dependencies:
10+
"@stablelib/base64" "^1.0.0"
11+
"@stablelib/hex" "^1.0.0"
12+
513
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
614
version "7.8.3"
715
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
@@ -486,6 +494,16 @@
486494
dependencies:
487495
type-detect "4.0.8"
488496

497+
"@stablelib/base64@^1.0.0":
498+
version "1.0.0"
499+
resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.0.tgz#e08ba78078c731cbbb244530b1750659c52ba7cb"
500+
integrity sha512-s/wTc/3+vYSalh4gfayJrupzhT7SDBqNtiYOeEMlkSDqL/8cExh5FAeTzLpmYq+7BLLv36EjBL5xrb0bUHWJWQ==
501+
502+
"@stablelib/hex@^1.0.0":
503+
version "1.0.0"
504+
resolved "https://registry.yarnpkg.com/@stablelib/hex/-/hex-1.0.0.tgz#9f2d21d412803e72a3bbc0ab4690e9bda0ca91cf"
505+
integrity sha512-EJ9oGiuaFw/Y0cBATTxo73sgqOgdnSmZ9ftU9FND9SD51OM8wvAfS78uPy3oBNmLWc/sZK5twMbEFf/A4T2F8A==
506+
489507
"@types/babel__core@^7.1.0":
490508
version "7.1.6"
491509
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:
41514169
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
41524170
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
41534171

4172+
tweetnacl@^1.0.3:
4173+
version "1.0.3"
4174+
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
4175+
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
4176+
41544177
type-check@~0.3.2:
41554178
version "0.3.2"
41564179
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"

0 commit comments

Comments
 (0)