Files: ab0a28e502ed6f2e1c7cfb70e57b53ff0e9a99cc / index.js
7627 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('chloride').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 | var keys = JSON.parse(private) |
124 | console.log(keys) |
125 | if(!hasSigil(keys.id)) keys.id = '@' + keys.public |
126 | return keys |
127 | } catch (_) {} |
128 | |
129 | //else, reconstruct legacy curve... |
130 | |
131 | var curve = getTag(private) |
132 | |
133 | if(curve !== 'k256') |
134 | throw new Error('expected legacy curve (k256) but found:' + curve) |
135 | |
136 | return keysToJSON(ecc.restore(toBuffer(private)), 'k256') |
137 | } |
138 | |
139 | var toNameFile = exports.toNameFile = function (namefile) { |
140 | if(isObject(namefile)) |
141 | return path.join(namefile.path, 'secret') |
142 | return namefile |
143 | } |
144 | |
145 | exports.load = function(namefile, cb) { |
146 | namefile = toNameFile(namefile) |
147 | fs.readFile(namefile, 'ascii', function(err, privateKeyStr) { |
148 | if (err) return cb(err) |
149 | try { cb(null, reconstructKeys(privateKeyStr)) } |
150 | catch (e) { cb(err) } |
151 | }) |
152 | } |
153 | |
154 | exports.loadSync = function(namefile) { |
155 | namefile = toNameFile(namefile) |
156 | return reconstructKeys(fs.readFileSync(namefile, 'ascii')) |
157 | } |
158 | |
159 | exports.create = function(namefile, curve, legacy, cb) { |
160 | if(isFunction(legacy)) |
161 | cb = legacy, legacy = null |
162 | if(isFunction(curve)) |
163 | cb = curve, curve = null |
164 | |
165 | namefile = toNameFile(namefile) |
166 | var keys = exports.generate(curve) |
167 | var keyfile = constructKeys(keys, legacy) |
168 | mkdirp(path.dirname(namefile), function (err) { |
169 | if(err) return cb(err) |
170 | fs.writeFile(namefile, keyfile, function(err) { |
171 | if (err) return cb(err) |
172 | cb(null, keys) |
173 | }) |
174 | }) |
175 | } |
176 | |
177 | exports.createSync = function(namefile, curve, legacy) { |
178 | namefile = toNameFile(namefile) |
179 | var keys = exports.generate(curve) |
180 | var keyfile = constructKeys(keys, legacy) |
181 | mkdirp.sync(path.dirname(namefile)) |
182 | fs.writeFileSync(namefile, keyfile) |
183 | return keys |
184 | } |
185 | |
186 | exports.loadOrCreate = function (namefile, cb) { |
187 | namefile = toNameFile(namefile) |
188 | exports.load(namefile, function (err, keys) { |
189 | if(!err) return cb(null, keys) |
190 | exports.create(namefile, cb) |
191 | }) |
192 | } |
193 | |
194 | exports.loadOrCreateSync = function (namefile) { |
195 | namefile = toNameFile(namefile) |
196 | try { |
197 | return exports.loadSync(namefile) |
198 | } catch (err) { |
199 | return exports.createSync(namefile) |
200 | } |
201 | } |
202 | |
203 | |
204 | // DIGITAL SIGNATURES |
205 | |
206 | var curves = { |
207 | ed25519 : require('./sodium'), |
208 | k256 : ecc //LEGACY |
209 | } |
210 | |
211 | function getCurve(keys) { |
212 | var curve = keys.curve |
213 | |
214 | if(!keys.curve && isString(keys.public)) |
215 | keys = keys.public |
216 | |
217 | if(!curve && isString(keys)) |
218 | curve = getTag(keys) |
219 | |
220 | if(!curves[curve]) { |
221 | throw new Error( |
222 | 'unkown curve:' + curve + |
223 | ' expected: '+Object.keys(curves) |
224 | ) |
225 | } |
226 | |
227 | return curve |
228 | } |
229 | |
230 | //this should return a key pair: |
231 | // {curve: curve, public: Buffer, private: Buffer} |
232 | |
233 | exports.generate = function (curve, seed) { |
234 | curve = curve || 'ed25519' |
235 | |
236 | if(!curves[curve]) |
237 | throw new Error('unknown curve:'+curve) |
238 | |
239 | return keysToJSON(curves[curve].generate(seed), curve) |
240 | } |
241 | |
242 | //takes a public key and a hash and returns a signature. |
243 | //(a signature must be a node buffer) |
244 | |
245 | exports.sign = function (keys, msg) { |
246 | if(isString(msg)) |
247 | msg = new Buffer(msg) |
248 | if(!isBuffer(msg)) |
249 | throw new Error('msg should be buffer') |
250 | var curve = getCurve(keys) |
251 | |
252 | return curves[curve] |
253 | .sign(toBuffer(keys.private || keys), msg) |
254 | .toString('base64')+'.sig.'+curve |
255 | |
256 | } |
257 | |
258 | //takes a public key, signature, and a hash |
259 | //and returns true if the signature was valid. |
260 | exports.verify = function (keys, sig, msg) { |
261 | if(isObject(sig)) |
262 | throw new Error('signature should be base64 string, did you mean verifyObj(public, signed_obj)') |
263 | return curves[getCurve(keys)].verify( |
264 | toBuffer(keys.public || keys), |
265 | toBuffer(sig), |
266 | isBuffer(msg) ? msg : new Buffer(msg) |
267 | ) |
268 | } |
269 | |
270 | // OTHER CRYTPO FUNCTIONS |
271 | |
272 | exports.hmac = function (data, key) { |
273 | return createHmac(createHash, 64, key) |
274 | .update(data).digest('base64')+'.sha256.hmac' |
275 | } |
276 | |
277 | exports.signObj = function (keys, obj) { |
278 | var _obj = clone(obj) |
279 | var b = new Buffer(JSON.stringify(_obj, null, 2)) |
280 | _obj.signature = exports.sign(keys, b) |
281 | return _obj |
282 | } |
283 | |
284 | exports.verifyObj = function (keys, obj) { |
285 | obj = clone(obj) |
286 | var sig = obj.signature |
287 | delete obj.signature |
288 | var b = new Buffer(JSON.stringify(obj, null, 2)) |
289 | return exports.verify(keys, sig, b) |
290 | } |
291 | |
292 | exports.box = function (msg, recipients) { |
293 | msg = new Buffer(JSON.stringify(msg)) |
294 | |
295 | recipients = recipients.map(function (keys) { |
296 | var public = keys.public || keys |
297 | return sodium.crypto_sign_ed25519_pk_to_curve25519(toBuffer(public)) |
298 | }) |
299 | |
300 | //it's since the nonce is 24 bytes (a multiple of 3) |
301 | //it's possible to concatenate the base64 strings |
302 | //and still have a valid base64 string. |
303 | return pb.multibox(msg, recipients).toString('base64')+'.box' |
304 | } |
305 | |
306 | exports.unbox = function (boxed, keys) { |
307 | boxed = toBuffer(boxed) |
308 | var sk = sodium.crypto_sign_ed25519_sk_to_curve25519(toBuffer(keys.private || keys)) |
309 | |
310 | var msg = pb.multibox_open(boxed, sk) |
311 | if(msg) return JSON.parse(''+msg) |
312 | } |
313 |
Built with git-ssb-web