UNPKG

15.3 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.hashDataPayload = hashDataPayload;
7exports.hashRawData = hashRawData;
8exports.decodePrivateKey = decodePrivateKey;
9exports.signRawData = signRawData;
10exports.signDataPayload = signDataPayload;
11exports.makeFullyQualifiedDataId = makeFullyQualifiedDataId;
12exports.makeMutableDataInfo = makeMutableDataInfo;
13exports.makeDataTombstone = makeDataTombstone;
14exports.makeMutableDataTombstones = makeMutableDataTombstones;
15exports.makeInodeTombstones = makeInodeTombstones;
16exports.signDataTombstone = signDataTombstone;
17exports.signMutableDataTombstones = signMutableDataTombstones;
18exports.makeInodeHeaderBlob = makeInodeHeaderBlob;
19exports.makeDirInodeBlob = makeDirInodeBlob;
20exports.makeFileInodeBlob = makeFileInodeBlob;
21exports.getChildVersion = getChildVersion;
22exports.inodeDirLink = inodeDirLink;
23exports.inodeDirUnlink = inodeDirUnlink;
24
25var _util = require('./util');
26
27var _schemas = require('./schemas');
28
29var assert = require('assert');
30var crypto = require('crypto');
31var EC = require('elliptic').ec;
32var ec = EC('secp256k1');
33var Ajv = require('ajv');
34
35var BLOCKSTACK_STORAGE_PROTO_VERSION = 1;
36
37/*
38 * Hash an inode payload and its length.
39 * Specifically hash `${payload.length}:${payload},`
40 *
41 * @param payload_buffer (String) the payload to hash
42 *
43 * Return the sha256
44 */
45function hashDataPayload(payload_buffer) {
46 var hash = crypto.createHash('sha256');
47
48 // this forces the hash to be computed over the bytestring
49 // (important when computing length!) which will make it
50 // match with the Python hash verifier
51 var payload_str = Buffer.from(payload_buffer);
52
53 hash.update(payload_str.length + ':');
54 hash.update(payload_str);
55 hash.update(',');
56
57 return hash.digest('hex');
58}
59
60/*
61 * Hash raw data
62 * @param payload_buffer (Buffer) the buffer to hash
63 *
64 * Return the sha256
65 */
66function hashRawData(payload_buffer) {
67 var hash = crypto.createHash('sha256');
68
69 hash.update(payload_buffer);
70
71 return hash.digest('hex');
72}
73
74/*
75 * Decode a hex string into a byte buffer.
76 *
77 * @param hex (String) a string of hex digits.
78 *
79 * Returns a buffer with the raw bytes
80 */
81function decodeHexString(hex) {
82 var bytes = [];
83 for (var i = 0; i < hex.length - 1; i += 2) {
84 bytes.push(parseInt(hex.substr(i, 2), 16));
85 }
86 return Buffer.from(bytes);
87}
88
89/*
90 * Decode an ECDSA private key into a byte buffer
91 * (compatible with Bitcoin's 'compressed' flag quirk)
92 *
93 * @param privkey_hex (String) a hex-encoded ECDSA private key on secp256k1; optionally ending in '01'
94 *
95 * Returns a Buffer with the private key data
96 */
97function decodePrivateKey(privatekey_hex) {
98 if (privatekey_hex.length === 66 && privatekey_hex.slice(64, 66) === '01') {
99 // truncate the '01', which is a hint to Bitcoin to expect a compressed public key
100 privatekey_hex = privatekey_hex.slice(0, 64);
101 }
102 return decodeHexString(privatekey_hex);
103}
104
105/*
106 * Sign a string of data.
107 *
108 * @param payload_buffer (Buffer) the buffer to sign
109 * @param privkey_hex (String) the hex-encoded ECDSA private key
110 * @param hash (String) optional; the hash of the payload. payload_buffer can be null if hash is given.
111 *
112 * Return the base64-encoded signature
113 */
114function signRawData(payload_buffer, privkey_hex, hash) {
115
116 var privkey = decodePrivateKey(privkey_hex);
117
118 if (!hash) {
119 hash = hashRawData(payload_buffer);
120 }
121
122 var sig = ec.sign(hash, privkey, { canonical: true });
123
124 // use signature encoding compatible with Blockstack
125 var r_array = sig.r.toArray();
126 var s_array = sig.s.toArray();
127 var r_buf = Buffer.from(r_array).toString('hex');
128 var s_buf = Buffer.from(s_array).toString('hex');
129
130 if (r_buf.length < 64) {
131 while (r_buf.length < 64) {
132 r_buf = "0" + r_buf;
133 }
134 }
135
136 if (s_buf.length < 64) {
137 while (s_buf.length < 64) {
138 s_buf = "0" + s_buf;
139 }
140 }
141
142 var sig_buf_hex = r_buf + s_buf;
143
144 assert(sig_buf_hex.length == 128);
145
146 var sigb64 = Buffer.from(sig_buf_hex, 'hex').toString('base64');
147 return sigb64;
148}
149
150/*
151 * Sign a data payload and its length.
152 * Specifically sign `${payload.length}:${payload},`
153 *
154 * @payload_string (String) the string to sign
155 * @privkey_hex (String) the hex-encoded private key
156 *
157 * Return the base64-encoded signature
158 */
159function signDataPayload(payload_string, privkey_hex) {
160 return signRawData(Buffer.concat([Buffer.from(payload_string.length + ':'), Buffer.from(payload_string), Buffer.from(',')]), privkey_hex);
161}
162
163/*
164 * Make a fully-qualified data ID (i.e. includes the device ID)
165 * equivalent to this in Python: urllib.quote(str('{}:{}'.format(device_id, data_id).replace('/', '\\x2f')))
166 *
167 * @param device_id (String) the device ID
168 * @param data_id (String) the device-agnostic part of the data ID
169 *
170 * Returns the fully-qualified data ID
171 */
172function makeFullyQualifiedDataId(device_id, data_id) {
173 return escape((device_id + ':' + data_id).replace('/', '\\x2f'));
174}
175
176/*
177 * Make a mutable data payload
178 *
179 * @param data_id (String) the data identifier (not fully qualified)
180 * @param data_payload (String) the data payload to store
181 * @param version (Int) the version number
182 * @param device_id (String) the ID of the device creating this data
183 *
184 * Returns an mutable data payload object.
185 */
186function makeMutableDataInfo(data_id, data_payload, device_id, version) {
187 var fq_data_id = makeFullyQualifiedDataId(device_id, data_id);
188 var timestamp = new Date().getTime();
189
190 var ret = {
191 'fq_data_id': fq_data_id,
192 'data': data_payload,
193 'version': version,
194 'timestamp': timestamp
195 };
196
197 return ret;
198}
199
200/*
201 * Make a single datum tombstone.
202 *
203 * @param tombstone_payload (String) the string that encodes the tombstone
204 *
205 * Returns the tombstone (to be fed into the storage driver)
206 */
207function makeDataTombstone(tombstone_payload) {
208 var now = parseInt(new Date().getTime() / 1000);
209 return 'delete-' + now + ':' + tombstone_payload;
210}
211
212/*
213 * Make a list of mutable data tombstones.
214 *
215 * @param device_ids (Array) the list of device IDs
216 * @param data_id (String) the datum ID
217 *
218 * Returns a list of tombstones.
219 */
220function makeMutableDataTombstones(device_ids, data_id) {
221 var ts = [];
222 var _iteratorNormalCompletion = true;
223 var _didIteratorError = false;
224 var _iteratorError = undefined;
225
226 try {
227 for (var _iterator = device_ids[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
228 var device_id = _step.value;
229
230 ts.push(makeDataTombstone(makeFullyQualifiedDataId(device_id, data_id)));
231 }
232 } catch (err) {
233 _didIteratorError = true;
234 _iteratorError = err;
235 } finally {
236 try {
237 if (!_iteratorNormalCompletion && _iterator.return) {
238 _iterator.return();
239 }
240 } finally {
241 if (_didIteratorError) {
242 throw _iteratorError;
243 }
244 }
245 }
246
247 return ts;
248}
249
250/*
251 * Make a list of inode tombstones.
252 *
253 * @param datastore_id (String) the datastore ID
254 * @param inode_uuid (String) the inode ID
255 * @param device_ids (Array) the list of device IDs
256 *
257 * Returns a list of tombstones.
258 */
259function makeInodeTombstones(datastore_id, inode_uuid, device_ids) {
260 assert(device_ids.length > 0);
261
262 var header_id = datastore_id + '.' + inode_uuid + '.hdr';
263 var header_tombstones = makeMutableDataTombstones(device_ids, header_id);
264
265 var idata_id = datastore_id + '.' + inode_uuid;
266 var idata_tombstones = makeMutableDataTombstones(device_ids, idata_id);
267
268 return header_tombstones.concat(idata_tombstones);
269}
270
271/*
272 * Sign a datum tombstone
273 *
274 * @param tombstone (String) the tombstone string
275 * @param privkey (String) the hex-encoded private key
276 *
277 * Returns the signed tombstone as a String
278 */
279function signDataTombstone(tombstone, privkey) {
280 var sigb64 = signRawData(tombstone, privkey);
281 return tombstone + ':' + sigb64;
282}
283
284/*
285 * Sign a list of mutable data tombstones
286 *
287 * @param tobmstones (Array) the list of per-device tombstones
288 * @param privkey (String) the hex-encoded private key
289 *
290 * Returns the list of signed tombstones as an Array.
291 */
292function signMutableDataTombstones(tombstones, privkey) {
293 var sts = [];
294 var _iteratorNormalCompletion2 = true;
295 var _didIteratorError2 = false;
296 var _iteratorError2 = undefined;
297
298 try {
299 for (var _iterator2 = tombstones[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
300 var ts = _step2.value;
301
302 sts.push(signDataTombstone(ts, privkey));
303 }
304 } catch (err) {
305 _didIteratorError2 = true;
306 _iteratorError2 = err;
307 } finally {
308 try {
309 if (!_iteratorNormalCompletion2 && _iterator2.return) {
310 _iterator2.return();
311 }
312 } finally {
313 if (_didIteratorError2) {
314 throw _iteratorError2;
315 }
316 }
317 }
318
319 ;
320 return sts;
321}
322
323/*
324 * Make an inode header blob.
325 *
326 * @param datastore_id (String) the ID of the datastore for this inode
327 * @param inode_type (Int) 1 for file, 2 for directory
328 * @param owner_id (String) a string that encodes the owner of this inode (i.e. pass datastore_id for now)
329 * @param inode_uuid (String) the inode ID
330 * @param data_hash (String) the hex-encoded sha256 of the data
331 * @param version (Int) the version of this inode.
332 * @param device_id (String) the ID of this device
333 *
334 * Returns an object encoding an inode header.
335 */
336function makeInodeHeaderBlob(datastore_id, inode_type, owner_id, inode_uuid, data_hash, device_id, version) {
337
338 var header = {
339 'type': inode_type,
340 'owner': owner_id,
341 'uuid': inode_uuid,
342 'readers': [], // unused for now
343 'data_hash': data_hash,
344 'version': version,
345 'proto_version': BLOCKSTACK_STORAGE_PROTO_VERSION
346 };
347
348 var valid = null;
349 var ajv = new Ajv();
350 try {
351 valid = ajv.validate(_schemas.MUTABLE_DATUM_INODE_HEADER_SCHEMA, header);
352 assert(valid);
353 } catch (e) {
354 console.log('header: ' + JSON.stringify(header));
355 console.log('schema: ' + JSON.stringify(_schemas.MUTABLE_DATUM_INODE_HEADER_SCHEMA));
356 console.log(e.stack);
357 throw e;
358 }
359
360 var inode_data_id = datastore_id + '.' + inode_uuid + '.hdr';
361 var inode_data_payload = (0, _util.jsonStableSerialize)(header);
362 var inode_header_blob = makeMutableDataInfo(inode_data_id, inode_data_payload, device_id, version);
363 return (0, _util.jsonStableSerialize)(inode_header_blob);
364}
365
366/*
367 * Make a directory inode header for a particular datastore and owner.
368 *
369 * @param datastore_id (String) the ID of the datastore for this inode
370 * @param owner_id (String) a string that encodes the owner of this directory (i.e. pass datastore_id for now)
371 * @param inode_uuid (String) the ID of the inode
372 * @param dir_listing (Object) a MUTABLE_DATUM_DIR_IDATA_SCHEMA-conformant object that describes the directory listing.
373 * @param device_id (String) this device ID
374 *
375 * Returns an object encoding a directory's header and idata
376 */
377function makeDirInodeBlob(datastore_id, owner_id, inode_uuid, dir_listing, device_id, version) {
378
379 var ajv = new Ajv();
380 var valid = null;
381 try {
382 valid = ajv.validate(_schemas.MUTABLE_DATUM_DIR_IDATA_SCHEMA.properties.children, dir_listing);
383 assert(valid);
384 } catch (e) {
385 console.log('dir listing: ' + JSON.stringify(dir_listing));
386 console.log('schema: ' + JSON.stringify(_schemas.MUTABLE_DATUM_DIR_IDATA_SCHEMA));
387 throw e;
388 }
389
390 if (!version) {
391 version = 1;
392 }
393
394 var empty_hash = '0000000000000000000000000000000000000000000000000000000000000000';
395 var internal_header_blob = makeInodeHeaderBlob(datastore_id, _schemas.MUTABLE_DATUM_DIR_TYPE, owner_id, inode_uuid, empty_hash, device_id, version);
396
397 // recover header
398 var internal_header = JSON.parse(JSON.parse(internal_header_blob).data);
399 var idata_payload = {
400 children: dir_listing,
401 header: internal_header
402 };
403
404 var idata_payload_str = (0, _util.jsonStableSerialize)(idata_payload);
405 var idata_hash = hashDataPayload(idata_payload_str);
406
407 var header_blob = makeInodeHeaderBlob(datastore_id, _schemas.MUTABLE_DATUM_DIR_TYPE, owner_id, inode_uuid, idata_hash, device_id, version);
408 return { 'header': header_blob, 'idata': idata_payload_str };
409}
410
411/*
412 * Make a file inode header for a particular datastore and owner.
413 *
414 * @param datastore_id (String) the ID of the datastore for this niode
415 * @param owner_id (String) a string that encodes the owner of this file (i.e. pass datastore_id for now)
416 * @param inode_uuid (String) the ID of the inode
417 * @param data_hash (String) the hash of the file data
418 * @param device_id (String) this device ID
419 *
420 * Returns an object encoding a file's header
421 */
422function makeFileInodeBlob(datastore_id, owner_id, inode_uuid, data_hash, device_id, version) {
423
424 var header_blob = makeInodeHeaderBlob(datastore_id, _schemas.MUTABLE_DATUM_FILE_TYPE, owner_id, inode_uuid, data_hash, device_id, version);
425 return { 'header': header_blob };
426}
427
428/*
429 * Get the child inode version from a directory
430 * @param parent_dir (Object) directory inode
431 * @param child_name (String) name of the directory
432 *
433 * Raises if there is no child
434 */
435function getChildVersion(parent_dir, child_name) {
436 assert(parent_dir['idata']['children'][child_name]);
437 return parent_dir['idata']['children'][child_name].version;
438}
439
440/*
441 * Insert an entry into a directory's listing.
442 *
443 * @param parent_dir (Object) a directory inode structure
444 * @param child_type (Int) 1 for file, 2 for directory
445 * @param child_name (String) the name of the child inode (must be unique in this directory)
446 * @param child_uuid (String) the ID of the child inode.
447 * @param exists (Bool) if given, and if True, then expect the child to exist.
448 *
449 * Returns the new parent directory inode object.
450 */
451function inodeDirLink(parent_dir, child_type, child_name, child_uuid, exists) {
452
453 assert(parent_dir['type'] === _schemas.MUTABLE_DATUM_DIR_TYPE);
454 assert(parent_dir['idata']);
455 assert(parent_dir['idata']['children']);
456
457 if (!exists) {
458 assert(!Object.keys(parent_dir['idata']['children']).includes(child_name));
459 }
460
461 var new_dirent = {
462 uuid: child_uuid,
463 type: child_type,
464 version: 1
465 };
466
467 if (parent_dir['idata']['children']['version']) {
468 new_dirent.version = parent_dir['idata']['children']['version'] + 1;
469 }
470
471 parent_dir['idata']['children'][child_name] = new_dirent;
472 parent_dir['version'] += 1;
473 return parent_dir;
474}
475
476/*
477 * Detach an inode from a directory.
478 *
479 * @param parent_dir (Object) a directory inode structure
480 * @param child_name (String) the name of the child to detach
481 *
482 * Returns the new parent directory inode object.
483 */
484function inodeDirUnlink(parent_dir, child_name) {
485
486 assert(parent_dir['type'] === _schemas.MUTABLE_DATUM_DIR_TYPE);
487 assert(parent_dir['idata']);
488 assert(parent_dir['idata']['children']);
489
490 assert(Object.keys(parent_dir['idata']['children']).includes(child_name));
491
492 delete parent_dir['idata']['children'][child_name];
493 parent_dir['version'] += 1;
494 return parent_dir;
495}
\No newline at end of file