Files: f39581dcdc2e1fb69fc7d8390596cf5a47d9f1bd / index.js
7326 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 | var Blake2s = require('blake2s') |
9 | |
10 | var ecc = require('./eccjs') |
11 | |
12 | //UTILS |
13 | |
14 | function clone (obj) { |
15 | var _obj = {} |
16 | for(var k in obj) { |
17 | if(Object.hasOwnProperty.call(obj, k)) |
18 | _obj[k] = obj[k] |
19 | } |
20 | return _obj |
21 | } |
22 | |
23 | function hash (data, enc) { |
24 | return new Blake2s().update(data, enc).digest('base64') + '.blake2s' |
25 | } |
26 | |
27 | function isHash (data) { |
28 | return isString(data) && /^[A-Za-z0-9\/+]{43}=\.blake2s$/.test(data) |
29 | } |
30 | |
31 | function isObject (o) { |
32 | return 'object' === typeof o |
33 | } |
34 | |
35 | function isFunction (f) { |
36 | return 'function' === typeof f |
37 | } |
38 | |
39 | exports.isHash = isHash |
40 | exports.hash = hash |
41 | |
42 | function isString(s) { |
43 | return 'string' === typeof s |
44 | } |
45 | |
46 | function empty(v) { return !!v } |
47 | |
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 [ |
89 | '# this is your SECRET name.', |
90 | '# this name gives you magical powers.', |
91 | '# with it you can mark your messages so that your friends can verify', |
92 | '# that they really did come from you.', |
93 | '#', |
94 | '# if any one learns this name, they can use it to destroy your identity', |
95 | '# NEVER show this to anyone!!!', |
96 | '', |
97 | legacy ? keys.private : JSON.stringify(keys, null, 2), |
98 | '', |
99 | '# WARNING! It\'s vital that you DO NOT edit OR share your secret name', |
100 | '# instead, share your public name', |
101 | '# your public name: ' + keys.id |
102 | ].join('\n') |
103 | } |
104 | |
105 | function reconstructKeys(keyfile) { |
106 | var private = keyfile |
107 | .replace(/\s*\#[^\n]*/g, '') |
108 | .split('\n').filter(empty).join('') |
109 | |
110 | //if the key is in JSON format, we are good. |
111 | try { |
112 | return JSON.parse(private) |
113 | } catch (_) {} |
114 | |
115 | //else, reconstruct legacy curve... |
116 | |
117 | var curve = getTag(private) |
118 | |
119 | if(curve !== 'k256') |
120 | throw new Error('expected legacy curve (k256) but found:' + curve) |
121 | |
122 | return keysToJSON(ecc.restore(toBuffer(private)), 'k256') |
123 | } |
124 | |
125 | var toNameFile = exports.toNameFile = function (namefile) { |
126 | if(isObject(namefile)) |
127 | return path.join(namefile.path, 'secret') |
128 | return namefile |
129 | } |
130 | |
131 | exports.load = function(namefile, cb) { |
132 | namefile = toNameFile(namefile) |
133 | fs.readFile(namefile, 'ascii', function(err, privateKeyStr) { |
134 | if (err) return cb(err) |
135 | try { cb(null, reconstructKeys(privateKeyStr)) } |
136 | catch (e) { cb(err) } |
137 | }) |
138 | } |
139 | |
140 | exports.loadSync = function(namefile) { |
141 | namefile = toNameFile(namefile) |
142 | return reconstructKeys(fs.readFileSync(namefile, 'ascii')) |
143 | } |
144 | |
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 | |
151 | namefile = toNameFile(namefile) |
152 | var keys = exports.generate(curve) |
153 | var keyfile = constructKeys(keys, legacy) |
154 | mkdirp(path.dirname(namefile), function (err) { |
155 | if(err) return cb(err) |
156 | fs.writeFile(namefile, keyfile, function(err) { |
157 | if (err) return cb(err) |
158 | cb(null, keys) |
159 | }) |
160 | }) |
161 | } |
162 | |
163 | exports.createSync = function(namefile, curve, legacy) { |
164 | namefile = toNameFile(namefile) |
165 | var keys = exports.generate(curve) |
166 | var keyfile = constructKeys(keys, legacy) |
167 | mkdirp.sync(path.dirname(namefile)) |
168 | fs.writeFileSync(namefile, keyfile) |
169 | return keys |
170 | } |
171 | |
172 | exports.loadOrCreate = function (namefile, cb) { |
173 | namefile = toNameFile(namefile) |
174 | exports.load(namefile, function (err, keys) { |
175 | if(!err) return cb(null, keys) |
176 | exports.create(namefile, cb) |
177 | }) |
178 | } |
179 | |
180 | exports.loadOrCreateSync = function (namefile) { |
181 | namefile = toNameFile(namefile) |
182 | try { |
183 | return exports.loadSync(namefile) |
184 | } catch (err) { |
185 | return exports.createSync(namefile) |
186 | } |
187 | } |
188 | |
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 | |
216 | //this should return a key pair: |
217 | // {curve: curve, public: Buffer, private: Buffer} |
218 | |
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) |
226 | } |
227 | |
228 | //takes a public key and a hash and returns a signature. |
229 | //(a signature must be a node buffer) |
230 | |
231 | 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)') |
234 | var hashTag = hash.substring(hash.indexOf('.')) |
235 | var curve = getCurve(keys) |
236 | |
237 | return curves[curve] |
238 | .sign(toBuffer(keys.private || keys), toBuffer(hash)) |
239 | .toString('base64')+'.blake2s.'+curve |
240 | |
241 | } |
242 | |
243 | //takes a public key, signature, and a hash |
244 | //and returns true if the signature was valid. |
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 | ) |
253 | } |
254 | |
255 | // OTHER CRYTPO FUNCTIONS |
256 | |
257 | function createHash() { |
258 | return new Blake2s() |
259 | } |
260 | |
261 | exports.hmac = function (data, key) { |
262 | return createHmac(createHash, 64, key) |
263 | .update(data).digest('base64')+'.blake2s.hmac' |
264 | } |
265 | |
266 | exports.signObj = function (keys, obj) { |
267 | var _obj = clone(obj) |
268 | var str = JSON.stringify(_obj, null, 2) |
269 | var h = hash(str, 'utf8') |
270 | _obj.signature = exports.sign(keys, h) |
271 | return _obj |
272 | } |
273 | |
274 | exports.verifyObj = function (keys, obj) { |
275 | obj = clone(obj) |
276 | var sig = obj.signature |
277 | delete obj.signature |
278 | var str = JSON.stringify(obj, null, 2) |
279 | var h = hash(str, 'utf8') |
280 | return exports.verify(keys, sig, h) |
281 | } |
282 | |
283 | //TODO: remove these (use asymmetric auth for everything) |
284 | |
285 | exports.signObjHmac = function (secret, obj) { |
286 | obj = clone(obj) |
287 | var str = JSON.stringify(obj, null, 2) |
288 | obj.hmac = exports.hmac(str, secret) |
289 | return obj |
290 | } |
291 | |
292 | exports.verifyObjHmac = function (secret, obj) { |
293 | obj = clone(obj) |
294 | var hmac = obj.hmac |
295 | delete obj.hmac |
296 | var str = JSON.stringify(obj, null, 2) |
297 | var _hmac = exports.hmac(str, secret) |
298 | return deepEqual(hmac, _hmac) |
299 | } |
300 | |
301 | exports.createAuth = function (keys, role) { |
302 | return exports.signObj(keys, { |
303 | role: role || 'client', |
304 | ts: Date.now(), |
305 | public: keys.public |
306 | }) |
307 | } |
308 | |
309 |
Built with git-ssb-web