mirror of https://codeberg.org/pzp/pzp-keypair.git
init
This commit is contained in:
commit
73972ce951
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
.nyc_output
|
||||
coverage
|
||||
pnpm-lock.yaml
|
|
@ -0,0 +1,2 @@
|
|||
semi: false
|
||||
singleQuote: true
|
|
@ -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.
|
|
@ -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)`
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
|
@ -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)
|
||||
})
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue