UNPKG

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