git ssb

1+

Dominic / ssb-keys



Tree: 741b9da9e7c2fe66c219256d3fb18b1051b2f7f3

Files: 741b9da9e7c2fe66c219256d3fb18b1051b2f7f3 / index.js

8094 bytesRaw
1var fs = require('fs')
2var mkdirp = require('mkdirp')
3var path = require('path')
4var deepEqual = require('deep-equal')
5
6var crypto = require('crypto')
7var createHmac = require('hmac')
8
9var ecc = require('./eccjs')
10var sodium = require('sodium').api
11var isRef = require('ssb-ref')
12
13var pb = require('private-box')
14
15//UTILS
16
17function clone (obj) {
18 var _obj = {}
19 for(var k in obj) {
20 if(Object.hasOwnProperty.call(obj, k))
21 _obj[k] = obj[k]
22 }
23 return _obj
24}
25
26function hash (data, enc) {
27 return crypto.createHash('sha256').update(data,enc).digest('base64')+'.sha256'
28}
29
30
31var isHash = isRef.isHash
32var isFeedId = isRef.isFeedId
33
34exports.hash = hash
35exports.isHash = isHash
36
37function isObject (o) {
38 return 'object' === typeof o
39}
40
41function isFunction (f) {
42 return 'function' === typeof f
43}
44
45function isString(s) {
46 return 'string' === typeof s
47}
48
49function empty(v) { return !!v }
50
51function toBuffer(buf) {
52 if(buf == null) return buf
53 if(Buffer.isBuffer(buf)) throw new Error('already a buffer')
54 var i = buf.indexOf('.')
55 return new Buffer(buf.substring(0, ~i ? i : buf.length), 'base64')
56}
57
58function toUint8(buf) {
59 return new Uint8Array(toBuffer(buf))
60}
61
62function getTag (string) {
63 var i = string.indexOf('.')
64 return string.substring(i+1)
65}
66
67exports.getTag = getTag
68
69function tag (key, tag) {
70 if(!tag) throw new Error('no tag for:' + key.toString('base64'))
71 return key.toString('base64')+'.' + tag.replace(/^\./, '')
72}
73
74function keysToJSON(keys, curve) {
75 curve = (keys.curve || curve)
76
77 var pub = tag(keys.public.toString('base64'), curve)
78 return {
79 curve: curve,
80 public: pub,
81 private: keys.private ? tag(keys.private.toString('base64'), curve) : undefined,
82 id: curve === 'ed25519' ? pub : hash(pub)
83 }
84}
85
86//(DE)SERIALIZE KEYS
87
88function constructKeys(keys, legacy) {
89 if(!keys) throw new Error('*must* pass in keys')
90
91 return [
92 '# this is your SECRET name.',
93 '# this name gives you magical powers.',
94 '# with it you can mark your messages so that your friends can verify',
95 '# that they really did come from you.',
96 '#',
97 '# if any one learns this name, they can use it to destroy your identity',
98 '# NEVER show this to anyone!!!',
99 '',
100 legacy ? keys.private : JSON.stringify(keys, null, 2),
101 '',
102 '# WARNING! It\'s vital that you DO NOT edit OR share your secret name',
103 '# instead, share your public name',
104 '# your public name: ' + keys.id
105 ].join('\n')
106}
107
108function reconstructKeys(keyfile) {
109 var private = keyfile
110 .replace(/\s*\#[^\n]*/g, '')
111 .split('\n').filter(empty).join('')
112
113 //if the key is in JSON format, we are good.
114 try {
115 return JSON.parse(private)
116 } catch (_) {}
117
118 //else, reconstruct legacy curve...
119
120 var curve = getTag(private)
121
122 if(curve !== 'k256')
123 throw new Error('expected legacy curve (k256) but found:' + curve)
124
125 return keysToJSON(ecc.restore(toBuffer(private)), 'k256')
126}
127
128var toNameFile = exports.toNameFile = function (namefile) {
129 if(isObject(namefile))
130 return path.join(namefile.path, 'secret')
131 return namefile
132}
133
134exports.load = function(namefile, cb) {
135 namefile = toNameFile(namefile)
136 fs.readFile(namefile, 'ascii', function(err, privateKeyStr) {
137 if (err) return cb(err)
138 try { cb(null, reconstructKeys(privateKeyStr)) }
139 catch (e) { cb(err) }
140 })
141}
142
143exports.loadSync = function(namefile) {
144 namefile = toNameFile(namefile)
145 return reconstructKeys(fs.readFileSync(namefile, 'ascii'))
146}
147
148exports.create = function(namefile, curve, legacy, cb) {
149 if(isFunction(legacy))
150 cb = legacy, legacy = null
151 if(isFunction(curve))
152 cb = curve, curve = null
153
154 namefile = toNameFile(namefile)
155 var keys = exports.generate(curve)
156 var keyfile = constructKeys(keys, legacy)
157 mkdirp(path.dirname(namefile), function (err) {
158 if(err) return cb(err)
159 fs.writeFile(namefile, keyfile, function(err) {
160 if (err) return cb(err)
161 cb(null, keys)
162 })
163 })
164}
165
166exports.createSync = function(namefile, curve, legacy) {
167 namefile = toNameFile(namefile)
168 var keys = exports.generate(curve)
169 var keyfile = constructKeys(keys, legacy)
170 mkdirp.sync(path.dirname(namefile))
171 fs.writeFileSync(namefile, keyfile)
172 return keys
173}
174
175exports.loadOrCreate = function (namefile, cb) {
176 namefile = toNameFile(namefile)
177 exports.load(namefile, function (err, keys) {
178 if(!err) return cb(null, keys)
179 exports.create(namefile, cb)
180 })
181}
182
183exports.loadOrCreateSync = function (namefile) {
184 namefile = toNameFile(namefile)
185 try {
186 return exports.loadSync(namefile)
187 } catch (err) {
188 return exports.createSync(namefile)
189 }
190}
191
192
193// DIGITAL SIGNATURES
194
195var curves = {
196 ed25519 : require('./sodium'),
197 k256 : ecc //LEGACY
198}
199
200function getCurve(keys) {
201 var curve = keys.curve
202
203 if(!keys.curve && isString(keys.public))
204 keys = keys.public
205
206 if(!curve && isString(keys))
207 curve = getTag(keys)
208
209 if(!curves[curve]) {
210 throw new Error(
211 'unkown curve:' + curve +
212 ' expected: '+Object.keys(curves)
213 )
214 }
215
216 return curve
217}
218
219//this should return a key pair:
220// {curve: curve, public: Buffer, private: Buffer}
221
222exports.generate = function (curve, seed) {
223 curve = curve || 'ed25519'
224
225 if(!curves[curve])
226 throw new Error('unknown curve:'+curve)
227
228 return keysToJSON(curves[curve].generate(seed), curve)
229}
230
231//takes a public key and a hash and returns a signature.
232//(a signature must be a node buffer)
233
234exports.sign = function (keys, hash) {
235 if(isObject(hash))
236 throw new Error('hash should be base64 string, did you mean signObj(private, unsigned_obj)')
237 var hashTag = hash.substring(hash.indexOf('.'))
238 var curve = getCurve(keys)
239
240 return curves[curve]
241 .sign(toBuffer(keys.private || keys), toBuffer(hash))
242 .toString('base64')+'.sha256.'+curve
243
244}
245
246//takes a public key, signature, and a hash
247//and returns true if the signature was valid.
248exports.verify = function (keys, sig, hash) {
249 if(isObject(sig))
250 throw new Error('signature should be base64 string, did you mean verifyObj(public, signed_obj)')
251 return curves[getCurve(keys)].verify(
252 toBuffer(keys.public || keys),
253 toBuffer(sig),
254 toBuffer(hash)
255 )
256}
257
258// OTHER CRYTPO FUNCTIONS
259
260exports.hmac = function (data, key) {
261 return createHmac(createHash, 64, key)
262 .update(data).digest('base64')+'.sha256.hmac'
263}
264
265exports.signObj = function (keys, obj) {
266 var _obj = clone(obj)
267 var str = JSON.stringify(_obj, null, 2)
268 var h = hash(str, 'utf8')
269 _obj.signature = exports.sign(keys, h)
270 return _obj
271}
272
273exports.verifyObj = function (keys, obj) {
274 obj = clone(obj)
275 var sig = obj.signature
276 delete obj.signature
277 var str = JSON.stringify(obj, null, 2)
278 var h = hash(str, 'utf8')
279 return exports.verify(keys, sig, h)
280}
281
282//TODO: remove these (use asymmetric auth for everything)
283
284//exports.signObjHmac = function (secret, obj) {
285// obj = clone(obj)
286// var str = JSON.stringify(obj, null, 2)
287// obj.hmac = exports.hmac(str, secret)
288// return obj
289//}
290//
291//exports.verifyObjHmac = function (secret, obj) {
292// obj = clone(obj)
293// var hmac = obj.hmac
294// delete obj.hmac
295// var str = JSON.stringify(obj, null, 2)
296// var _hmac = exports.hmac(str, secret)
297// return deepEqual(hmac, _hmac)
298//}
299//
300//exports.createAuth = function (keys, role) {
301// return exports.signObj(keys, {
302// role: role || 'client',
303// ts: Date.now(),
304// public: keys.public
305// })
306//}
307
308exports.box = function (msg, recipients) {
309 msg = new Buffer(JSON.stringify(msg))
310
311 recipients = recipients.map(function (keys) {
312 var public = keys.public || keys
313 return sodium.crypto_sign_ed25519_pk_to_curve25519(toBuffer(public))
314 })
315
316 //it's since the nonce is 24 bytes (a multiple of 3)
317 //it's possible to concatenate the base64 strings
318 //and still have a valid base64 string.
319 return pb.multibox(msg, recipients).toString('base64')+'.box'
320}
321
322exports.unbox = function (boxed, keys) {
323 boxed = toBuffer(boxed)
324 var sk = sodium.crypto_sign_ed25519_sk_to_curve25519(toBuffer(keys.private || keys))
325
326 var msg = pb.multibox_open(boxed, sk)
327 if(msg) return JSON.parse(''+msg)
328}
329

Built with git-ssb-web