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