git ssb

1+

Dominic / ssb-keys



Commit 11bfac9638f9dbd5a12974097b021f5c4ac5815f

Merge pull request #14 from dominictarr/ecma-nacl

tracking PR: add nacl crypto
Dominic Tarr committed on 6/18/2015, 5:31:59 PM
Parent: cdc3f0eab07ea57c208f285cade01f106d98acb3
Parent: 1aac9545053acc3a652c65cbe53ade54bb9f304d

Files changed

index.jschanged
package.jsonchanged
test/index.jschanged
browser-sodium.jsadded
eccjs.jsadded
sodium.jsadded
index.jsView
@@ -1,15 +1,17 @@
1-var fs = require('fs')
2-var crypto = require('crypto')
3-var ecc = require('eccjs')
4-var k256 = ecc.curves.k256
5-var Blake2s = require('blake2s')
6-var mkdirp = require('mkdirp')
7-var path = require('path')
8-var curve = ecc.curves.k256
1+var fs = require('fs')
2+var mkdirp = require('mkdirp')
3+var path = require('path')
4+var deepEqual = require('deep-equal')
5+
6+var crypto = require('crypto')
97 var createHmac = require('hmac')
10-var deepEqual = require('deep-equal')
8+var Blake2s = require('blake2s')
119
10+var ecc = require('./eccjs')
11+
12+//UTILS
13+
1214 function clone (obj) {
1315 var _obj = {}
1416 for(var k in obj) {
1517 if(Object.hasOwnProperty.call(obj, k))
@@ -21,17 +23,20 @@
2123 function hash (data, enc) {
2224 return new Blake2s().update(data, enc).digest('base64') + '.blake2s'
2325 }
2426
25-
2627 function isHash (data) {
2728 return isString(data) && /^[A-Za-z0-9\/+]{43}=\.blake2s$/.test(data)
2829 }
2930
3031 function isObject (o) {
3132 return 'object' === typeof o
3233 }
3334
35+function isFunction (f) {
36+ return 'function' === typeof f
37+}
38+
3439 exports.isHash = isHash
3540 exports.hash = hash
3641
3742 function isString(s) {
@@ -39,74 +44,85 @@
3944 }
4045
4146 function empty(v) { return !!v }
4247
43-function constructKeys() {
44- var privateKey = crypto.randomBytes(32)
45- var k = keysToBase64(ecc.restore(k256, privateKey))
46- k.keyfile = [
48+function toBuffer(buf) {
49+ if(buf == null) return buf
50+ if(Buffer.isBuffer(buf)) throw new Error('already a buffer')
51+ var i = buf.indexOf('.')
52+ return new Buffer(buf.substring(0, ~i ? i : buf.length), 'base64')
53+}
54+
55+function toUint8(buf) {
56+ return new Uint8Array(toBuffer(buf))
57+}
58+
59+function getTag (string) {
60+ var i = string.indexOf('.')
61+ return string.substring(i+1)
62+}
63+
64+exports.getTag = getTag
65+
66+function tag (key, tag) {
67+ if(!tag) throw new Error('no tag for:' + key.toString('base64'))
68+ return key.toString('base64')+'.' + tag.replace(/^\./, '')
69+}
70+
71+function keysToJSON(keys, curve) {
72+ curve = (keys.curve || curve)
73+
74+ var pub = tag(keys.public.toString('base64'), curve)
75+ return {
76+ curve: curve,
77+ public: pub,
78+ private: keys.private ? tag(keys.private.toString('base64'), curve) : undefined,
79+ id: hash(pub)
80+ }
81+}
82+
83+//(DE)SERIALIZE KEYS
84+
85+function constructKeys(keys, legacy) {
86+ if(!keys) throw new Error('*must* pass in keys')
87+
88+ return [
4789 '# this is your SECRET name.',
4890 '# this name gives you magical powers.',
4991 '# with it you can mark your messages so that your friends can verify',
5092 '# that they really did come from you.',
5193 '#',
5294 '# if any one learns this name, they can use it to destroy your identity',
5395 '# NEVER show this to anyone!!!',
5496 '',
55- k.private.toString('hex'),
97+ legacy ? keys.private : JSON.stringify(keys, null, 2),
5698 '',
5799 '# WARNING! It\'s vital that you DO NOT edit OR share your secret name',
58100 '# instead, share your public name',
59- '# your public name: ' + k.id.toString('hex')
101+ '# your public name: ' + keys.id
60102 ].join('\n')
61- return k
62103 }
63104
105+function reconstructKeys(keyfile) {
106+ var private = keyfile
107+ .replace(/\s*\#[^\n]*/g, '')
108+ .split('\n').filter(empty).join('')
64109
65-function toBuffer(buf) {
66- if(buf == null) return buf
67- return new Buffer(buf.substring(0, buf.indexOf('.')), 'base64')
68-}
110+ //if the key is in JSON format, we are good.
111+ try {
112+ return JSON.parse(private)
113+ } catch (_) {}
69114
70-function keysToBase64 (keys) {
71- var pub = tag(keys.public, 'k256')
72- return {
73- public: pub,
74- private: tag(keys.private, 'k256'),
75- id: hash(pub)
76- }
77-}
115+ //else, reconstruct legacy curve...
78116
79-function hashToBuffer(hash) {
80- if(!isHash(hash)) throw new Error('sign expects a hash')
81- return toBuffer(hash)
82-}
117+ var curve = getTag(private)
83118
84-function keysToBuffer(key) {
85- return isString(key) ? toBuffer(key) : {
86- public: toBuffer(key.public),
87- private: toBuffer(key.private)
88- }
89-}
119+ if(curve !== 'k256')
120+ throw new Error('expected legacy curve (k256) but found:' + curve)
90121
91-function reconstructKeys(privateKeyStr) {
92- privateKeyStr = privateKeyStr
93- .replace(/\s*\#[^\n]*/g, '')
94- .split('\n').filter(empty).join('')
95-
96- var privateKey = (
97- !/\./.test(privateKeyStr)
98- ? new Buffer(privateKeyStr, 'hex')
99- : toBuffer(privateKeyStr)
100- )
101-
102- return keysToBase64(ecc.restore(k256, privateKey))
122+ return keysToJSON(ecc.restore(toBuffer(private)), 'k256')
103123 }
104124
105-function tag (key, tag) {
106- return key.toString('base64')+'.' + tag.replace(/^\./, '')
107-}
108-
109125 var toNameFile = exports.toNameFile = function (namefile) {
110126 if(isObject(namefile))
111127 return path.join(namefile.path, 'secret')
112128 return namefile
@@ -125,28 +141,33 @@
125141 namefile = toNameFile(namefile)
126142 return reconstructKeys(fs.readFileSync(namefile, 'ascii'))
127143 }
128144
129-exports.create = function(namefile, cb) {
145+exports.create = function(namefile, curve, legacy, cb) {
146+ if(isFunction(legacy))
147+ cb = legacy, legacy = null
148+ if(isFunction(curve))
149+ cb = curve, curve = null
150+
130151 namefile = toNameFile(namefile)
131- var k = constructKeys()
152+ var keys = exports.generate(curve)
153+ var keyfile = constructKeys(keys, legacy)
132154 mkdirp(path.dirname(namefile), function (err) {
133155 if(err) return cb(err)
134- fs.writeFile(namefile, k.keyfile, function(err) {
156+ fs.writeFile(namefile, keyfile, function(err) {
135157 if (err) return cb(err)
136- delete k.keyfile
137- cb(null, k)
158+ cb(null, keys)
138159 })
139160 })
140161 }
141162
142-exports.createSync = function(namefile) {
163+exports.createSync = function(namefile, curve, legacy) {
143164 namefile = toNameFile(namefile)
144- var k = constructKeys()
165+ var keys = exports.generate(curve)
166+ var keyfile = constructKeys(keys, legacy)
145167 mkdirp.sync(path.dirname(namefile))
146- fs.writeFileSync(namefile, k.keyfile)
147- delete k.keyfile
148- return k
168+ fs.writeFileSync(namefile, keyfile)
169+ return keys
149170 }
150171
151172 exports.loadOrCreate = function (namefile, cb) {
152173 namefile = toNameFile(namefile)
@@ -154,8 +175,9 @@
154175 if(!err) return cb(null, keys)
155176 exports.create(namefile, cb)
156177 })
157178 }
179+
158180 exports.loadOrCreateSync = function (namefile) {
159181 namefile = toNameFile(namefile)
160182 try {
161183 return exports.loadSync(namefile)
@@ -163,31 +185,76 @@
163185 return exports.createSync(namefile)
164186 }
165187 }
166188
189+
190+// DIGITAL SIGNATURES
191+
192+var curves = {
193+ ed25519 : require('./sodium'),
194+ k256 : ecc //LEGACY
195+}
196+
197+function getCurve(keys) {
198+ var curve = keys.curve
199+
200+ if(!keys.curve && isString(keys.public))
201+ keys = keys.public
202+
203+ if(!curve && isString(keys))
204+ curve = getTag(keys)
205+
206+ if(!curves[curve]) {
207+ throw new Error(
208+ 'unkown curve:' + curve +
209+ ' expected: '+Object.keys(curves)
210+ )
211+ }
212+
213+ return curve
214+}
215+
167216 //this should return a key pair:
168-// {public: Buffer, private: Buffer}
217+// {curve: curve, public: Buffer, private: Buffer}
169218
170-exports.generate = function () {
171- return keysToBase64(ecc.restore(curve, crypto.randomBytes(32)))
219+exports.generate = function (curve, seed) {
220+ curve = curve || 'ed25519'
221+
222+ if(!curves[curve])
223+ throw new Error('unknown curve:'+curve)
224+
225+ return keysToJSON(curves[curve].generate(seed), curve)
172226 }
173227
174228 //takes a public key and a hash and returns a signature.
175229 //(a signature must be a node buffer)
230+
176231 exports.sign = function (keys, hash) {
232+ if(isObject(hash))
233+ throw new Error('hash should be base64 string, did you mean signObj(private, unsigned_obj)')
177234 var hashTag = hash.substring(hash.indexOf('.'))
178- return tag(
179- ecc.sign(curve, keysToBuffer(keys), hashToBuffer(hash)),
180- hashTag + '.k256'
181- )
235+ var curve = getCurve(keys)
236+
237+ return curves[curve]
238+ .sign(toBuffer(keys.private || keys), toBuffer(hash))
239+ .toString('base64')+'.blake2s.'+curve
240+
182241 }
183242
184243 //takes a public key, signature, and a hash
185244 //and returns true if the signature was valid.
186-exports.verify = function (pub, sig, hash) {
187- return ecc.verify(curve, keysToBuffer(pub), toBuffer(sig), hashToBuffer(hash))
245+exports.verify = function (keys, sig, hash) {
246+ if(isObject(sig))
247+ throw new Error('signature should be base64 string, did you mean verifyObj(public, signed_obj)')
248+ return curves[getCurve(keys)].verify(
249+ toBuffer(keys.public || keys),
250+ toBuffer(sig),
251+ toBuffer(hash)
252+ )
188253 }
189254
255+// OTHER CRYTPO FUNCTIONS
256+
190257 function createHash() {
191258 return new Blake2s()
192259 }
193260
@@ -212,8 +279,10 @@
212279 var h = hash(str, 'utf8')
213280 return exports.verify(keys, sig, h)
214281 }
215282
283+//TODO: remove these (use asymmetric auth for everything)
284+
216285 exports.signObjHmac = function (secret, obj) {
217286 obj = clone(obj)
218287 var str = JSON.stringify(obj, null, 2)
219288 obj.hmac = exports.hmac(str, secret)
@@ -236,15 +305,4 @@
236305 public: keys.public
237306 })
238307 }
239308
240-exports.codec = {
241- decode: function (string) {
242- return JSON.parse(string)
243- },
244- encode: function (obj) {
245- return JSON.stringify(obj, null, 2)
246- },
247- buffer: false
248-}
249-
250-exports.keys = exports
package.jsonView
@@ -7,17 +7,22 @@
77 "type": "git",
88 "url": "git://github.com/ssbc/ssb-crypto.git"
99 },
1010 "dependencies": {
11+ "blake2s": "~1.0.0",
12+ "deep-equal": "~0.2.1",
1113 "eccjs": "git://github.com/dominictarr/eccjs.git#586f6d47507184a2efe84684ed0a30605cbc43a5",
12- "blake2s": "~1.0.0",
14+ "hmac": "~1.0.1",
15+ "libsodium-wrappers": "^0.2.8",
1316 "mkdirp": "~0.5.0",
14- "hmac": "~1.0.1",
15- "deep-equal": "~0.2.1"
17+ "sodium": "^1.0.17"
1618 },
1719 "devDependencies": {
1820 "tape": "~3.0.0"
1921 },
22+ "browser": {
23+ "./sodium": "./browser-sodium"
24+ },
2025 "scripts": {
2126 "test": "set -e; for t in test/*.js; do node $t; done"
2227 },
2328 "author": "Paul Frazee <pfrazee@gmail.com>",
test/index.jsView
@@ -1,7 +1,7 @@
11 var tape = require('tape')
22 var ssbkeys = require('../')
3-
3+var crypto = require('crypto')
44 var path = require('path').join(__dirname, 'keyfile')
55
66 tape('create and load async', function (t) {
77 try { require('fs').unlinkSync(path) } catch(e) {}
@@ -25,4 +25,122 @@
2525 t.equal(k1.private.toString('hex'), k2.private.toString('hex'))
2626 t.equal(k1.public.toString('hex'), k2.public.toString('hex'))
2727 t.end()
2828 })
29+
30+
31+tape('sign and verify', function (t) {
32+
33+ var keys = ssbkeys.generate()
34+ var msg = ssbkeys.hash("HELLO THERE?")
35+ var sig = ssbkeys.sign(keys, msg)
36+ console.log('public', keys.public)
37+ console.log('sig', sig)
38+ t.ok(sig)
39+ t.equal(ssbkeys.getTag(sig), 'blake2s.ed25519')
40+ t.ok(ssbkeys.verify(keys, sig, msg))
41+
42+ t.end()
43+
44+})
45+
46+tape('sign and verify, call with keys directly', function (t) {
47+
48+ var keys = ssbkeys.generate()
49+ var msg = ssbkeys.hash("HELLO THERE?")
50+ var sig = ssbkeys.sign(keys.private, msg)
51+ console.log('public', keys.public)
52+ console.log('sig', sig)
53+ t.ok(sig)
54+ t.equal(ssbkeys.getTag(sig), 'blake2s.ed25519')
55+ t.ok(ssbkeys.verify(keys.public, sig, msg))
56+
57+ t.end()
58+
59+})
60+
61+tape('sign and verify a javascript object', function (t) {
62+
63+ var obj = require('../package.json')
64+
65+ console.log(obj)
66+
67+ var keys = ssbkeys.generate()
68+ var sig = ssbkeys.signObj(keys.private, obj)
69+ console.log(sig)
70+ t.ok(sig)
71+ t.ok(ssbkeys.verifyObj(keys, sig, obj))
72+ t.end()
73+
74+})
75+
76+tape('test legacy curve: k256', function (t) {
77+ var keys = ssbkeys.generate('k256')
78+
79+ var msg = ssbkeys.hash("LEGACY SYSTEMS")
80+ var sig = ssbkeys.sign(keys, msg)
81+
82+ console.log('public', keys.public)
83+ console.log('sig', sig)
84+
85+ t.ok(sig)
86+ t.equal(ssbkeys.getTag(sig), 'blake2s.k256')
87+ t.ok(ssbkeys.verify(keys, sig, msg))
88+
89+ t.end()
90+})
91+
92+tape('create and load async, legacy', function (t) {
93+ try { require('fs').unlinkSync(path) } catch(e) {}
94+ ssbkeys.create(path, 'k256', function(err, k1) {
95+ if (err) throw err
96+ ssbkeys.load(path, function(err, k2) {
97+ if (err) throw err
98+
99+ t.equal(k2.curve, 'k256')
100+ t.equal(k1.id, k2.id)
101+ t.equal(k1.private, k2.private)
102+ t.equal(k1.public, k2.public)
103+
104+ t.end()
105+ })
106+ })
107+})
108+
109+tape('create and load sync, legacy', function (t) {
110+ try { require('fs').unlinkSync(path) } catch(e) {}
111+ var k1 = ssbkeys.createSync(path, 'k256', true)
112+ var k2 = ssbkeys.loadSync(path)
113+
114+ console.log(k2)
115+
116+ t.equal(k2.curve, 'k256')
117+ t.equal(k1.id, k2.id)
118+ t.equal(k1.private, k2.private)
119+ t.equal(k1.public, k2.public)
120+
121+ t.end()
122+})
123+
124+tape('seeded keys, ed25519', function (t) {
125+
126+ var seed = crypto.randomBytes(32)
127+ var k1 = ssbkeys.generate('ed25519', seed)
128+ var k2 = ssbkeys.generate('ed25519', seed)
129+
130+ t.deepEqual(k1, k2)
131+
132+ t.end()
133+
134+})
135+
136+tape('seeded keys, k256', function (t) {
137+
138+ var seed = crypto.randomBytes(32)
139+ var k1 = ssbkeys.generate('k256', seed)
140+ var k2 = ssbkeys.generate('k256', seed)
141+
142+ t.deepEqual(k1, k2)
143+
144+ t.end()
145+
146+})
browser-sodium.jsView
@@ -1,0 +1,35 @@
1+
2+var sodium = require('libsodium-wrappers')
3+var crypto = require('crypto')
4+
5+var B = Buffer
6+
7+function Ui8 (b) {
8+ return new Uint8Array(b)
9+}
10+
11+module.exports = {
12+
13+ curves: ['ed25519'],
14+
15+ generate: function () {
16+ var keys = sodium.crypto_sign_keypair()
17+ return {
18+ curve: 'ed25519',
19+ public: B(keys.publicKey),
20+
21+ //so that this works with either sodium
22+ //or libsodium-wrappers (in browser)
23+ private: B(keys.privateKey || keys.secretKey)
24+ }
25+ },
26+
27+ sign: function (private, message) {
28+ return B(sodium.crypto_sign_detached(Ui8(message), Ui8(private)))
29+ },
30+
31+ verify: function (public, sig, message) {
32+ return sodium.crypto_sign_verify_detached(Ui8(sig), Ui8(message), Ui8(public))
33+ }
34+
35+}
eccjs.jsView
@@ -1,0 +1,39 @@
1+
2+
3+var ecc = require('eccjs')
4+var crypto = require('crypto')
5+
6+var curve = ecc.curves.k256
7+
8+module.exports = {
9+
10+ curves: ['k256'],
11+
12+ generate: function (seed) {
13+ //we use eccjs.restore here, instead of eccjs.generate
14+ //because we trust node's random generator much more than
15+ //sjcl's (via crypto-browserify's polyfil this uses
16+ //webcrypto's random generator in the browser)
17+
18+ var keys = ecc.restore(curve, seed || crypto.randomBytes(32))
19+
20+ return {
21+ curve: 'k256',
22+ public: keys.public,
23+ private: keys.private
24+ }
25+ },
26+
27+ sign: function (private, message) {
28+ return ecc.sign(curve, private, message)
29+ },
30+
31+ verify: function (public, sig, message) {
32+ return ecc.verify(curve, public, sig, message)
33+ },
34+
35+ restore: function (seed) {
36+ return ecc.restore(curve, seed)
37+ }
38+
39+}
sodium.jsView
@@ -1,0 +1,29 @@
1+
2+var sodium = require('sodium').api
3+var crypto = require('crypto')
4+
5+module.exports = {
6+
7+ curves: ['ed25519'],
8+
9+ generate: function (seed) {
10+ var keys = sodium.crypto_sign_seed_keypair(seed || crypto.randomBytes(32))
11+ return {
12+ curve: 'ed25519',
13+ public: keys.publicKey,
14+
15+ //so that this works with either sodium
16+ //or libsodium-wrappers (in browser)
17+ private: keys.privateKey || keys.secretKey
18+ }
19+ },
20+
21+ sign: function (private, message) {
22+ return sodium.crypto_sign_detached(message, private)
23+ },
24+
25+ verify: function (public, sig, message) {
26+ return sodium.crypto_sign_verify_detached(sig, message, public)
27+ }
28+
29+}

Built with git-ssb-web