This commit is contained in:
Andre Staltz 2023-06-12 14:22:15 +03:00
commit 73972ce951
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
16 changed files with 856 additions and 0 deletions

27
.github/workflows/node.js.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 2
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
.nyc_output
coverage
pnpm-lock.yaml

2
.prettierrc.yaml Normal file
View File

@ -0,0 +1,2 @@
semi: false
singleQuote: true

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2023 Andre 'Staltz' Medeiros
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# ppppp-keypair
Like `ssb-keys`, but for PPPPP.
API:
- `generate(curve?, seed?) => Keypair {curve, public, private}`
- `sign(keypair, msg, hmacKey?) => sig`
- `verify(keypair, msg, sig, hmacKey?) => boolean`
- `create(filepath, cb)`
- `load(filepath, cb)`
- `loadOrCreate(filepath, cb)`
- `createSync(filepath)`
- `loadSync(filepath)`
- `loadOrCreateSync(filepath)`

101
lib/curves/ed25519.js Normal file
View File

@ -0,0 +1,101 @@
// @ts-ignore
const sodium = require('sodium-universal')
const base58 = require('bs58')
/**
* @typedef {import('.').Keypair} Keypair
*
* @typedef {import('.').KeypairPublicSlice} KeypairPublicSlice
*
* @typedef {import('.').KeypairPrivateSlice} KeypairPrivateSlice
*/
const SEEDBYTES = sodium.crypto_sign_SEEDBYTES
const PUBLICKEYBYTES = sodium.crypto_sign_PUBLICKEYBYTES
const SECRETKEYBYTES = sodium.crypto_sign_SECRETKEYBYTES
const ed25519 = {
/**
* @param {(string | Buffer)=} seed
* @returns {Keypair}
*/
generate(seed) {
let seedBuf
if (seed) {
if (Buffer.isBuffer(seed)) {
// prettier-ignore
if (seed.length !== SEEDBYTES) throw new Error(`seed must be ${SEEDBYTES} bytes`)
seedBuf = seed
} else if (typeof seed === 'string') {
seedBuf = Buffer.alloc(SEEDBYTES)
const slice = seed.substring(0, SEEDBYTES)
Buffer.from(slice, 'utf-8').copy(seedBuf)
}
}
const publicKeyBuf = Buffer.alloc(PUBLICKEYBYTES)
const secretKeyBuf = Buffer.alloc(SECRETKEYBYTES)
if (seedBuf) {
sodium.crypto_sign_seed_keypair(publicKeyBuf, secretKeyBuf, seedBuf)
} else {
sodium.crypto_sign_keypair(publicKeyBuf, secretKeyBuf)
}
return {
curve: 'ed25519',
public: base58.encode(publicKeyBuf),
private: base58.encode(secretKeyBuf),
_public: publicKeyBuf,
_private: secretKeyBuf,
}
},
/**
* @param {Keypair} keypair
* @param {{indented?: boolean}=} opts
*/
toJSON(keypair, opts) {
const stringifiable = {
curve: keypair.curve,
public: keypair.public,
private: keypair.private,
}
if (opts?.indented) {
return JSON.stringify(stringifiable, null, 2)
} else {
return JSON.stringify(stringifiable)
}
},
/**
* @param {KeypairPrivateSlice} keypair
* @param {Buffer} message
* @returns {string}
*/
sign(keypair, message) {
if (!keypair._private && !keypair.private) {
throw new Error(`invalid ed25519 keypair with missing private key`)
}
keypair._private ??= Buffer.from(base58.decode(keypair.private))
const sig = Buffer.alloc(sodium.crypto_sign_BYTES)
sodium.crypto_sign_detached(sig, message, keypair._private)
return base58.encode(sig)
},
/**
* @param {KeypairPublicSlice} keypair
* @param {string} sig
* @param {Buffer} message
* @returns {boolean}
*/
verify(keypair, sig, message) {
if (!keypair._public && !keypair.public) {
throw new Error(`invalid ed25519 keypair with missing public key`)
}
keypair._public ??= Buffer.from(base58.decode(keypair.public))
const sigBuf = Buffer.from(base58.decode(sig))
return sodium.crypto_sign_verify_detached(sigBuf, message, keypair._public)
},
}
module.exports = ed25519

60
lib/curves/index.js Normal file
View File

@ -0,0 +1,60 @@
const ed25519 = require('./ed25519')
const curves = {
ed25519,
}
/**
* @typedef {keyof typeof curves} CurveName
*
* @typedef {{
* curve: CurveName,
* public: string,
* private: string,
* _public?: Buffer,
* _private?: Buffer,
* }} Keypair
*
* @typedef {(Pick<Keypair, 'curve' | 'public'> & {_public: never}) |
* (Pick<Keypair, 'curve' | '_public'> & {public: never})
* } KeypairPublicSlice
*
* @typedef {(Pick<Keypair, 'curve' | 'private'> & {_private: never}) |
* (Pick<Keypair, 'curve' | '_private'> & {private: never})
* } KeypairPrivateSlice
*
* @typedef {{
* generate: (seed?: Buffer | string) => Keypair,
* toJSON: (keypair: Keypair, opts?: {indented?: boolean}) => string,
* sign: (keypair: KeypairPrivateSlice, message: Buffer) => Buffer,
* verify: (keypair: KeypairPublicSlice, message: Buffer, sig: Buffer) => boolean,
* }} Curve
*/
/**
* @param {CurveName} curveName
*/
function getCurve(curveName) {
if (!curves[curveName]) {
// prettier-ignore
throw new Error(`Unknown curve "${curveName}" out of available "${Object.keys(curves).join(',')}"`)
}
return curves[curveName]
}
/**
* This function generates a keypair for the given curve. The seed is optional.
*
* @param {CurveName=} curveName
* @param {(Buffer | string)=} seed
* @returns {Keypair}
*/
function generate(curveName, seed) {
const curve = getCurve(curveName ?? 'ed25519')
return curve.generate(seed)
}
module.exports = {
getCurve,
generate,
}

97
lib/index.js Normal file
View File

@ -0,0 +1,97 @@
// @ts-ignore
const sodium = require('sodium-universal')
const base58 = require('bs58')
const { getCurve, generate } = require('./curves')
const StorageClass =
typeof window !== 'undefined'
? require('./storage/browser')
: require('./storage/node')
/**
* @typedef {import('./curves').Keypair} Keypair
* @typedef {import('./curves').KeypairPublicSlice} KeypairPublicSlice
* @typedef {import('./curves').KeypairPrivateSlice} KeypairPrivateSlice
* @typedef {import('./curves').CurveName} CurveName
*/
/**
* @param {any} x
* @returns {x is string}
*/
function isString(x) {
return typeof x === 'string'
}
/**
* @param {any} x
* @returns {x is Buffer}
*/
function isBuffer(x) {
return Buffer.isBuffer(x)
}
/**
* @param {Buffer} input
* @param {string | Buffer} key
* @returns {Buffer}
*/
function hmac(input, key) {
if (isString(key)) key = Buffer.from(base58.decode(key))
const output = Buffer.alloc(sodium.crypto_auth_BYTES)
sodium.crypto_auth(output, input, key)
return output
}
/**
* Takes a keypair object (where `.public` is allowed to be undefined), a
* message as a buffer (and an optional hmacKey) and returns a signature of the
* given message. The signature is string encoded in base58.
*
* @param {KeypairPrivateSlice} keypair
* @param {Buffer} msg
* @param {Buffer | string | undefined} hmacKey
* @returns {string}
*/
function sign(keypair, msg, hmacKey) {
if (!isBuffer(msg)) throw new Error('Signable message should be buffer')
const curve = getCurve(keypair.curve)
if (hmacKey) msg = hmac(msg, hmacKey)
return curve.sign(keypair, msg)
}
/**
* Takes a keypair object (where `private` is allowed to be undefined), a
* message buffer and its signature string (and an optional hmacKey), and
* returns true if the signature is valid for the message, false otherwise.
*
* @param {KeypairPublicSlice} keypair
* @param {Buffer} msg
* @param {string} sig
* @param {Buffer | string | undefined} hmacKey
* @returns {boolean}
*/
function verify(keypair, msg, sig, hmacKey) {
if (!isString(sig)) throw new Error('sig should be string')
if (!isBuffer(msg)) throw new Error('Signed message should be buffer')
const curve = getCurve(keypair.curve)
if (hmacKey) msg = hmac(msg, hmacKey)
return curve.verify(keypair, sig, msg)
}
const storage = new StorageClass()
module.exports = {
generate,
sign,
verify,
create: storage.create.bind(storage),
load: storage.load.bind(storage),
createSync: storage.createSync.bind(storage),
loadSync: storage.loadSync.bind(storage),
loadOrCreate: storage.loadOrCreate.bind(storage),
loadOrCreateSync: storage.loadOrCreateSync.bind(storage),
}

74
lib/storage/browser.js Normal file
View File

@ -0,0 +1,74 @@
const { generate, getCurve } = require('../curves')
const Storage = require('./common')
/**
* @typedef {import('../curves').Keypair} Keypair
*
* @typedef {(...args: [Error] | [null, Keypair]) => void} Callback
*/
class BrowserStorage extends Storage {
constructor() {
super()
}
/**
* @param {string} identifier
* @returns {Keypair}
*/
createSync(identifier) {
const keypair = generate()
const curve = getCurve(keypair.curve)
const jsonStr = curve.toJSON(keypair, { indented: false })
localStorage.setItem(identifier, jsonStr)
return keypair
}
/**
* @param {string} identifier
* @returns {Keypair}
*/
loadSync(identifier) {
const item = localStorage.getItem(identifier)
if (!item) {
throw new Error(`No keypair found at localStorage "${identifier}"`)
}
try {
return JSON.parse(item)
} catch {
throw new Error(`Malformed keypair JSON in localStorage ${identifier}`)
}
}
/**
* @param {string} identifier
* @param {Callback} cb
*/
create(identifier, cb) {
let keypair
try {
keypair = this.createSync(identifier)
} catch (err) {
cb(/** @type {Error} */ (err))
return
}
cb(null, keypair)
}
/**
* @param {string} identifier
* @param {Callback} cb
*/
load(identifier, cb) {
let keypair
try {
keypair = this.loadSync(identifier)
} catch (err) {
cb(/** @type {Error} */ (err))
return
}
cb(null, keypair)
}
}
module.exports = BrowserStorage

66
lib/storage/common.js Normal file
View File

@ -0,0 +1,66 @@
/**
* @typedef {import('../curves').Keypair} Keypair
*
* @typedef {import('../curves').CurveName} CurveName
*
* @typedef {(...args: [Error] | [null, Keypair]) => void} Callback
*/
class Storage {
/**
* @param {string} identifier
* @param {Callback} cb
*/
load(identifier, cb) {
throw new Error('load() missing an implementation')
}
/**
* @param {string} identifier
* @param {Callback} cb
*/
create(identifier, cb) {
throw new Error('create() missing an implementation')
}
/**
* @param {string} identifier
* @returns {Keypair}
*/
loadSync(identifier) {
throw new Error('loadSync() missing an implementation')
}
/**
* @param {string} identifier
* @returns {Keypair}
*/
createSync(identifier) {
throw new Error('createSync() missing an implementation')
}
/**
* @param {string} identifier
* @param {Callback} cb
*/
loadOrCreate(identifier, cb) {
this.load(identifier, (err, keypair) => {
if (!err) return cb(null, keypair)
else this.create(identifier, cb)
})
}
/**
* @param {string} identifier
* @returns {Keypair}
*/
loadOrCreateSync(identifier) {
try {
return this.loadSync(identifier)
} catch {
return this.createSync(identifier)
}
}
}
module.exports = Storage

126
lib/storage/node.js Normal file
View File

@ -0,0 +1,126 @@
const fs = require('node:fs')
const path = require('node:path')
const { mkdirp } = require('mkdirp')
const { generate, getCurve } = require('../curves')
const Storage = require('./common')
/**
* @typedef {import('../curves').Keypair} Keypair
*
* @typedef {(...args: [Error] | [null, Keypair]) => void} Callback
*/
class NodeStorage extends Storage {
constructor() {
super()
}
/** @type {BufferEncoding} */
#fileEncoding = 'ascii'
/** @type {fs.WriteFileOptions} */
#fileWriteOpts = { mode: 0x100, flag: 'wx', encoding: this.#fileEncoding }
/**
* @param {Keypair} keypair
* @returns {string}
*/
#toFileContents(keypair) {
const curve = getCurve(keypair.curve)
const jsonStr = curve.toJSON(keypair, { indented: true })
return `# WARNING: Never show this to anyone.
# WARNING: Never edit it or use it on multiple devices at once.
#
# This is your SECRET, it gives you magical powers. With your secret you can
# sign your messages so that your friends can verify that the messages came
# from you. If anyone learns your secret, they can use it to impersonate you.
#
# If you use this secret on more than one device you will create a fork and
# your friends will stop replicating your content.
#
${jsonStr}
#
# The only part of this file that's safe to share is your public name:
#
# ${keypair.public}`
}
/**
* @param {string} contents
* @return {Keypair}
*/
#fromFileContents(contents) {
const json = contents
.replace(/\s*#[^\n]*/g, '')
.split('\n')
.filter((x) => !!x)
.join('')
try {
const keypair = JSON.parse(json)
return keypair
} catch {
throw new Error(`Malformed keypair JSON in file contents`)
}
}
/**
* @param {string} filename
* @returns {Keypair}
*/
createSync(filename) {
const keypair = generate()
const fileContents = this.#toFileContents(keypair)
mkdirp.sync(path.dirname(filename))
fs.writeFileSync(filename, fileContents, this.#fileWriteOpts)
return keypair
}
/**
* @param {string} filename
* @returns {Keypair}
*/
loadSync(filename) {
const fileContents = fs.readFileSync(filename, this.#fileEncoding)
const keypair = this.#fromFileContents(fileContents)
return keypair
}
/**
* @param {string} filename
* @param {Callback} cb
*/
create(filename, cb) {
const keypair = generate()
const fileContents = this.#toFileContents(keypair)
mkdirp(path.dirname(filename)).then(() => {
fs.writeFile(filename, fileContents, this.#fileWriteOpts, (err) => {
if (err) cb(err)
else cb(null, keypair)
})
}, cb)
}
/**
* @param {string} filename
* @param {Callback} cb
*/
load(filename, cb) {
fs.readFile(filename, this.#fileEncoding, (err, fileContents) => {
if (err) return cb(err)
/** @type {Keypair} */
let keypair
try {
keypair = this.#fromFileContents(fileContents)
} catch (err) {
cb(/** @type {Error} */ (err))
return
}
cb(null, keypair)
})
}
}
module.exports = NodeStorage

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "ppppp-keypair",
"description": "Keyfile operations for PPPPP",
"version": "0.0.1",
"homepage": "https://github.com/staltz/ppppp-keypair",
"repository": {
"type": "git",
"url": "git://github.com/staltz/ppppp-keypair.git"
},
"author": "Andre 'Staltz' Medeiros <contact@staltz.com>",
"license": "MIT",
"main": "lib/index.js",
"files": [
"lib/**/*"
],
"engines": {
"node": ">=16"
},
"exports": {
".": {
"require": "./lib/index.js"
}
},
"dependencies": {
"bs58": "^5.0.0",
"sodium-universal": "^4.0.0",
"mkdirp": "~3.0.1"
},
"devDependencies": {
"@types/node": "^20.2.5",
"c8": "^7.11.0",
"husky": "^4.3.0",
"prettier": "^2.6.2",
"pretty-quick": "^3.1.3",
"typescript": "^5.0.2"
},
"scripts": {
"build": "tsc",
"test": "node --test",
"format-code": "prettier --write \"*.js\" \"(test|lib)/*.js\"",
"format-code-staged": "pretty-quick --staged --pattern \"*.js\" --pattern \"(test|lib)/*.js\"",
"coverage": "c8 --reporter=lcov npm run test"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}

130
test/fs.test.js Normal file
View File

@ -0,0 +1,130 @@
const test = require('node:test')
const assert = require('node:assert')
const fs = require('fs')
const os = require('os')
const path = require('path')
const Keypair = require('../lib/index')
const keyPath = path.join(os.tmpdir(), `ppppp-keypair-${Date.now()}`)
test('loadSync()', (t) => {
const keypair = Keypair.generate('ed25519')
fs.writeFileSync(keyPath, JSON.stringify(keypair))
const keypair2 = Keypair.loadSync(keyPath)
assert.ok(keypair2.public)
assert.equal(keypair2.public, keypair.public)
fs.unlinkSync(keyPath)
})
test('load()', (t, done) => {
const keypair = Keypair.generate('ed25519')
fs.writeFileSync(keyPath, JSON.stringify(keypair))
Keypair.load(keyPath, (err, keypair2) => {
assert.ifError(err)
assert.ok(keypair2.public)
assert.equal(keypair2.public, keypair.public)
fs.unlinkSync(keyPath)
done()
})
})
test('create() then load()', (t, done) => {
Keypair.create(keyPath, (err, k1) => {
assert.ifError(err)
Keypair.load(keyPath, (err, k2) => {
assert.ifError(err)
assert.equal(k1.private, k2.private)
assert.equal(k1.public, k2.public)
fs.unlinkSync(keyPath)
done()
})
})
})
test('createSync() then loadSync()', (t) => {
const k1 = Keypair.createSync(keyPath)
const k2 = Keypair.loadSync(keyPath)
assert.equal(k1.private, k2.private)
assert.equal(k1.public, k2.public)
fs.unlinkSync(keyPath)
})
test('create()/createSync() avoid overwriting existing keys', (t, done) => {
fs.writeFileSync(keyPath, 'this file intentionally left blank', 'utf8')
assert.throws(() => {
Keypair.createSync(keyPath)
})
Keypair.create(keyPath, (err) => {
assert.ok(err)
done()
})
})
test('loadOrCreate() can load', (t, done) => {
const keyPath = path.join(os.tmpdir(), `ssb-keys-1-${Date.now()}`)
const keypair = Keypair.generate('ed25519')
fs.writeFileSync(keyPath, JSON.stringify(keypair))
Keypair.loadOrCreate(keyPath, (err, keypair2) => {
assert.ifError(err)
assert.ok(keypair2.public)
assert.equal(keypair2.public, keypair.public)
fs.unlinkSync(keyPath)
done()
})
})
test('loadOrCreate() can create', (t, done) => {
const keyPath = path.join(os.tmpdir(), `ssb-keys-2-${Date.now()}`)
assert.equal(fs.existsSync(keyPath), false)
Keypair.loadOrCreate(keyPath, (err, keypair) => {
assert.ifError(err)
assert.ok(keypair.public.length > 20, 'keys.public is a long string')
assert.ok(keypair.private.length > 20, 'keys.private is a long string')
assert.equal(typeof keypair.curve, 'string', 'keys.curve is a string')
fs.unlinkSync(keyPath)
done()
})
})
test('loadOrCreateSync() can load', (t) => {
const keyPath = path.join(os.tmpdir(), `ssb-keys-3-${Date.now()}`)
const keypair = Keypair.generate('ed25519')
fs.writeFileSync(keyPath, JSON.stringify(keypair))
const keypair2 = Keypair.loadOrCreateSync(keyPath)
assert.ok(keypair2.public)
assert.equal(keypair2.public, keypair.public)
fs.unlinkSync(keyPath)
})
test('loadOrCreateSync() can create', (t) => {
const keyPath = path.join(os.tmpdir(), `ssb-keys-4-${Date.now()}`)
assert.equal(fs.existsSync(keyPath), false)
const keypair = Keypair.loadOrCreateSync(keyPath)
assert.ok(keypair.public.length > 20, 'keys.public is a long string')
assert.ok(keypair.private.length > 20, 'keys.private is a long string')
assert.ok(keypair.curve, 'keys.curve is a string')
fs.unlinkSync(keyPath)
})
test('loadOrCreate() doesnt create dir for fully-specified path', (t, done) => {
const keyPath = path.join(os.tmpdir(), `ssb-keys-5-${Date.now()}`)
assert.equal(fs.existsSync(keyPath), false)
Keypair.loadOrCreate(keyPath, (err) => {
assert.ifError(err)
assert.ok(fs.lstatSync(keyPath).isFile())
Keypair.loadOrCreate(keyPath, (err, keypair) => {
assert.ifError(err)
assert.ok(keypair.public.length > 20)
fs.unlinkSync(keyPath)
done()
})
})
})

37
test/generate.test.js Normal file
View File

@ -0,0 +1,37 @@
const test = require('node:test')
const assert = require('node:assert')
const Keypair = require('../lib/index')
test('generate() default', (t) => {
const keypair = Keypair.generate()
assert.equal(keypair.curve, 'ed25519')
assert.equal(typeof keypair.public, 'string')
assert.equal(typeof keypair.private, 'string')
assert.equal(Buffer.isBuffer(keypair._public), true)
assert.equal(Buffer.isBuffer(keypair._private), true)
assert.deepEqual(Object.keys(keypair), [
'curve',
'public',
'private',
'_public',
'_private',
])
})
test('generate() with ed25519 curve', (t) => {
const keypair = Keypair.generate('ed25519')
assert.equal(keypair.curve, 'ed25519')
assert.equal(typeof keypair.public, 'string')
assert.equal(typeof keypair.private, 'string')
})
test('generate() with unknown curve', (t) => {
assert.throws(() => {
Keypair.generate('foobar')
}, /Unknown curve "foobar"/)
})
test('generate() with seed', (t) => {
const keypair = Keypair.generate('ed25519', 'alice')
assert.equal(keypair.public, '4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW')
})

33
test/signing.test.js Normal file
View File

@ -0,0 +1,33 @@
const test = require('node:test')
const assert = require('node:assert')
const Keypair = require('../lib/index')
const crypto = require('crypto')
test('sign()/verify() does not work on strings', (t) => {
const str = 'ppppp'
const keypair = Keypair.generate()
assert.throws(() => {
Keypair.sign(keypair, str)
})
})
test('sign()/verify() a buffer without hmac key', (t) => {
const buf = Buffer.from('ppppp')
const keypair = Keypair.generate()
const sig = Keypair.sign(keypair, buf)
assert.ok(sig)
const { public, curve } = keypair
assert.ok(Keypair.verify({ public, curve }, buf, sig))
})
test('sign()/verify a buffer with hmac key', (t) => {
const str = Buffer.from('ppppp')
const keypair = Keypair.generate()
const hmac_key = crypto.randomBytes(32)
const hmac_key2 = crypto.randomBytes(32)
const sig = Keypair.sign(keypair, str, hmac_key)
assert.ok(sig)
assert.equal(Keypair.verify(keypair, str, sig, hmac_key), true)
assert.equal(Keypair.verify(keypair, str, sig, hmac_key2), false)
})

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"include": ["lib/**/*.js"],
"exclude": ["coverage/", "node_modules/", "test/"],
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2021", "dom"],
"module": "node16",
"skipLibCheck": true,
"strict": true,
"target": "es2021"
}
}