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