Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ in the below methods, `keys` is an object of the following form:

``` js
{
"curve": "ed25519",
"feedType": "ed25519",
"public": "<base64_public_key>.ed25519",
"private": "<base64_private_key>.ed25519",
"id": "@<base64_public_key>.ed25519"
Expand All @@ -56,8 +56,8 @@ Comment lines are prefixed with `#` after removing them the result is valid JSON
### hash (data, encoding) => id
Returns the sha256 hash of a given data. If encoding is not provided then it is assumed to be _binary_.

### getTag (ssb_id) => tag
The SSB ids contain a tag at the end. This function returns it.
### getFeedType (ssbId) => feedType
Each SSB ID contains a feed type at the end. This function returns it.
So if you have a string like `@gaQw6zD4pHrg8zmrqku24zTSAINhRg=.ed25519` this function would return `ed25519`.
This is useful as SSB start providing features for different encryption methods and cyphers.

Expand Down Expand Up @@ -85,14 +85,16 @@ If a sync file access method is not available, `loadOrCreate` can be called with
callback. that callback will be called with `cb(null, keys)`. If loading
the keys errored, new keys are created.

### generate(curve, seed) => keys
### generate(feedType, seed) => keys

generate a key, with optional seed.
curve defaults to `ed25519` (and no other type is currently supported)
feed type defaults to `ed25519` (and no other type is currently supported)
seed should be a 32 byte buffer.

`keys` is an object as described in [`keys`](#keys) section.

New feed types can be added with `ssbKeys.use()`.

### signObj(keys, hmac_key?, obj)

signs a javascript object, and then adds a signature property to it.
Expand Down Expand Up @@ -145,6 +147,10 @@ symmetrically encrypt an object with `key` (a buffer)

symmetrically decrypt an object with `key` (a buffer)

### use(feedTypeName, { generate, sign, verify }) => ssbKeys

add new feed type to be used with `ssbKeys.generate()`

### LICENSE

MIT
Expand Down
95 changes: 59 additions & 36 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
'use strict'
var sodium = require('chloride')

var pb = require('private-box')

var u = require('./util')

var isBuffer = Buffer.isBuffer

//UTILS
Expand All @@ -22,52 +19,72 @@ var hmac = sodium.crypto_auth

exports.hash = u.hash

exports.getTag = u.getTag

function isObject (o) {
return 'object' === typeof o
}

function isFunction (f) {
return 'function' === typeof f
}

function isString(s) {
return 'string' === typeof s
}

var curves = {}
curves.ed25519 = require('./sodium')
const feedTypes = {
ed25519: require('./sodium'),
}

exports.use = (name, object) => {
if (typeof name !== 'string' || name.length === 0) {
throw new Error(`Invalid name: "${name}", expected string with non-zero length`)
}

const requiredMethods = [
'generate',
'sign',
'verify'
]

const isNotObject = typeof object !== 'object'
const isInvalidObject = isNotObject || requiredMethods.every(methodName => {
typeof object[methodName] === 'function'
})

if (isInvalidObject) {
const expectedMethods = requiredMethods.join(', ')
throw new Error(`Invalid objectMissing required methods, expected: ${expectedMethods}`)
}

if (feedTypes[name] != null) {
throw new Error(`Duplicate feed type: "${name}"`)
}

feedTypes[name] = object
}

function getCurve(keys) {
var curve = keys.curve
function getFeedType(keys) {
let { feedType } = keys

if(!keys.curve && isString(keys.public))
if(!keys.feedType && isString(keys.public))
keys = keys.public

if(!curve && isString(keys))
curve = u.getTag(keys)
if(!feedType && isString(keys))
feedType = u.getSuffix(keys)

if(!curves[curve]) {
throw new Error(
'unkown curve:' + curve +
' expected: '+Object.keys(curves)
)
if(!feedTypes[feedType]) {
throw new Error(`unkown feed type: "${feedType}", expected: "${Object.keys(feedTypes)}"`)
}

return curve
return feedType
}

//this should return a key pair:
// {curve: curve, public: Buffer, private: Buffer}
// { feedType: string, public: Buffer, private: Buffer}
exports.generate = function (feedType, seed) {
feedType = feedType || 'ed25519'

exports.generate = function (curve, seed) {
curve = curve || 'ed25519'
if(feedTypes[feedType] == null)
throw new Error(`unknown feed type: "${feedType}"`)

if(!curves[curve])
throw new Error('unknown curve:'+curve)

return u.keysToJSON(curves[curve].generate(seed), curve)
return u.keysToJSON(feedTypes[feedType].generate(seed), feedType)
}

//import functions for loading/saving keys from storage
Expand Down Expand Up @@ -99,11 +116,14 @@ function sign (keys, msg) {
msg = new Buffer(msg)
if(!isBuffer(msg))
throw new Error('msg should be buffer')
var curve = getCurve(keys)
var feedType = getFeedType(keys)

return curves[curve]
const prefix = feedTypes[feedType]
.sign(u.toBuffer(keys.private || keys), msg)
.toString('base64')+'.sig.'+curve
.toString('base64')
const suffix = `.sig.${feedType}`

return prefix + suffix

}

Expand All @@ -112,7 +132,7 @@ function sign (keys, msg) {
function verify (keys, sig, msg) {
if(isObject(sig))
throw new Error('signature should be base64 string, did you mean verifyObj(public, signed_obj)')
return curves[getCurve(keys)].verify(
return feedTypes[getFeedType(keys)].verify(
u.toBuffer(keys.public || keys),
u.toBuffer(sig),
isBuffer(msg) ? msg : new Buffer(msg)
Expand Down Expand Up @@ -163,7 +183,9 @@ exports.unboxBody = function (boxed, key) {
var msg = pb.multibox_open_body(boxed, key)
try {
return JSON.parse(''+msg)
} catch (_) { }
} catch (_) {
return undefined
}
}

exports.unbox = function (boxed, keys) {
Expand All @@ -174,8 +196,9 @@ exports.unbox = function (boxed, keys) {
try {
var msg = pb.multibox_open(boxed, sk)
return JSON.parse(''+msg)
} catch (_) { }
return
} catch (_) {
return undefined
}
}

exports.secretBox = function secretBox (data, key) {
Expand All @@ -188,4 +211,4 @@ exports.secretUnbox = function secretUnbox (ctxt, key) {
var ptxt = sodium.crypto_secretbox_open_easy(ctxt, key.slice(0, 24), key)
if(!ptxt) return
return JSON.parse(ptxt.toString())
}
}
13 changes: 6 additions & 7 deletions local-storage.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
'use strict'
var u = require('./util')

function isFunction (f) {
return 'function' == typeof f
}

module.exports = function (generate) {

function create (filename, curve, legacy) {
var keys = generate(curve, legacy)
function create (filename, feedType, legacy) {
var keys = generate(feedType, legacy)
localStorage[filename] = JSON.stringify(keys)
return keys
}
Expand All @@ -19,12 +18,12 @@ module.exports = function (generate) {

return {
createSync: create,
create: function(filename, curve, legacy, cb) {
create: function(filename, feedType, legacy, cb) {
if(isFunction(legacy))
cb = legacy, legacy = null
if(isFunction(curve))
cb = curve, curve = null
cb(null, create(filename, curve, legacy))
if(isFunction(feedType))
cb = feedType, feedType = null
cb(null, create(filename, feedType, legacy))
},
loadSync: load,
load: function (filename, cb) {
Expand Down
7 changes: 0 additions & 7 deletions sodium.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,24 @@
var sodium = require('chloride')

module.exports = {

curves: ['ed25519'],

generate: function (seed) {
if(!seed) sodium.randombytes(seed = new Buffer(32))

var keys = seed ? sodium.crypto_sign_seed_keypair(seed) : sodium.crypto_sign_keypair()
return {
curve: 'ed25519',
public: keys.publicKey,

//so that this works with either sodium
//or libsodium-wrappers (in browser)
private: keys.privateKey || keys.secretKey
}
},

sign: function (privateKey, message) {
return sodium.crypto_sign_detached(message, privateKey)
},

verify: function (publicKey, sig, message) {
return sodium.crypto_sign_verify_detached(sig, message, publicKey)
}

}


Expand Down
14 changes: 7 additions & 7 deletions storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module.exports = function (generate) {

function reconstructKeys(keyfile) {
var privateKey = keyfile
.replace(/\s*\#[^\n]*/g, '')
.replace(/\s*#[^\n]*/g, '')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 I would honestly not fuck with the regex (unless you wrote tight tests).
This is outside the scope of this PR and could just be tidying which has side-effects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just tidying -- unused backslashes are a footgun IMO but I could make this change in another PR. Good call. I think we had a similar discussion about removing unused backslashes a little while ago too: ssbc/ssb-schema-definitions#3 (review)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true I didn't see the followup till just now.
Yeah think a different PR would be good. It's probably fine, but this is just in the class of things it's quite easy to make mistakes on.

.split('\n').filter(empty).join('')

//if the key is in JSON format, we are good.
Expand All @@ -78,14 +78,14 @@ module.exports = function (generate) {
return reconstructKeys(fs.readFileSync(filename, 'ascii'))
}

exports.create = function(filename, curve, legacy, cb) {
exports.create = function(filename, feedType, legacy, cb) {
if(isFunction(legacy))
cb = legacy, legacy = null
if(isFunction(curve))
cb = curve, curve = null
if(isFunction(feedType))
cb = feedType, feedType = null

filename = toFile(filename)
var keys = generate(curve)
var keys = generate(feedType)
var keyfile = constructKeys(keys, legacy)
mkdirp(path.dirname(filename), function (err) {
if(err) return cb(err)
Expand All @@ -96,9 +96,9 @@ module.exports = function (generate) {
})
}

exports.createSync = function(filename, curve, legacy) {
exports.createSync = function(filename, feedType, legacy) {
filename = toFile(filename)
var keys = generate(curve)
var keys = generate(feedType)
var keyfile = constructKeys(keys, legacy)
mkdirp.sync(path.dirname(filename))
fs.writeFileSync(filename, keyfile, {mode: 0x100, flag: 'wx'})
Expand Down
1 change: 0 additions & 1 deletion test/box-unbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ tape('box, unbox', function (t) {
tape('return undefined for invalid content', function (t) {

var alice = ssbkeys.generate()
var bob = ssbkeys.generate()

var msg = ssbkeys.unbox('this is invalid content', alice.private)
t.equal(msg, undefined)
Expand Down
1 change: 0 additions & 1 deletion test/fs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
var tape = require('tape')
var ssbkeys = require('../')
var crypto = require('crypto')
var path = '/tmp/ssb-keys_'+Date.now()
var fs = require('fs')

Expand Down
Loading