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