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