├── .eslintignore ├── crypto.js ├── test ├── .eslintrc.js └── unit │ └── index.js ├── crypto-browser.js ├── .gitignore ├── index.js ├── .eslintrc.js ├── util.js ├── util-browser.js ├── demo └── index.html ├── karma.conf.js ├── webpack.config.js ├── CHANGELOG.md ├── LICENSE ├── .github └── workflows │ └── main.yml ├── package.json ├── main.js ├── bin └── hl ├── codecs.js ├── Hashlink.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /crypto.js: -------------------------------------------------------------------------------- 1 | // WebCrypto polyfill 2 | export {default} from 'isomorphic-webcrypto'; 3 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | mocha: true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /crypto-browser.js: -------------------------------------------------------------------------------- 1 | // WebCrypto 2 | /* eslint-env browser */ 3 | export default (self.crypto || self.msCrypto); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.sw[nop] 3 | *~ 4 | .cache 5 | .project 6 | .settings 7 | TAGS 8 | coverage 9 | dist 10 | node_modules 11 | reports 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 'use strict'; 5 | 6 | // translate `main.js` to CommonJS 7 | require = require('esm')(module); 8 | module.exports = require('./main.js'); 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint-config-digitalbazaar', 5 | 'eslint-config-digitalbazaar/jsdoc' 6 | ], 7 | env: { 8 | node: true 9 | }, 10 | globals: { 11 | CryptoKey: true, 12 | TextDecoder: true, 13 | TextEncoder: true, 14 | Uint8Array: true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | // Node.js TextDecoder/TextEncoder 2 | import {TextDecoder, TextEncoder} from 'util'; 3 | export {TextDecoder, TextEncoder}; 4 | 5 | export function stringToUint8Array(data) { 6 | if(typeof data === 'string') { 7 | // convert data to Uint8Array 8 | return new TextEncoder().encode(data); 9 | } 10 | if(!(data instanceof Uint8Array)) { 11 | throw new TypeError('"data" be a string or Uint8Array.'); 12 | } 13 | return data; 14 | } 15 | -------------------------------------------------------------------------------- /util-browser.js: -------------------------------------------------------------------------------- 1 | // browser TextDecoder/TextEncoder 2 | /* eslint-env browser */ 3 | const TextDecoder = self.TextDecoder; 4 | const TextEncoder = self.TextEncoder; 5 | export {TextDecoder, TextEncoder}; 6 | 7 | export function stringToUint8Array(data) { 8 | if(typeof data === 'string') { 9 | // convert data to Uint8Array 10 | return new TextEncoder().encode(data); 11 | } 12 | if(!(data instanceof Uint8Array)) { 13 | throw new TypeError('"data" be a string or Uint8Array.'); 14 | } 15 | return data; 16 | } 17 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hashlink Test 5 | 6 | 7 | 8 | 9 |

10 | "Hello World!\n" encoded as a Hashlink (base58btc encoded sha2-256 hash) 11 | is: 12 |

13 | 14 |

15 | 16 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | return config.set({ 3 | frameworks: ['mocha'], 4 | files: ['test/unit/index.js'], 5 | reporters: ['mocha'], 6 | basePath: '', 7 | port: 9876, 8 | colors: true, 9 | browsers: ['ChromeHeadless'], 10 | client: { 11 | mocha: { 12 | timeout: 2000 13 | } 14 | }, 15 | singleRun: true, 16 | // preprocess matching files before serving them to the browser 17 | // available preprocessors: 18 | // https://npmjs.org/browse/keyword/karma-preprocessor 19 | preprocessors: { 20 | 'test/unit/*.js': ['webpack', 'sourcemap'], 21 | }, 22 | webpack: { 23 | devtool: 'inline-source-map', 24 | mode: 'development', 25 | node: { 26 | // FIXME: prefer a non Buffer based CBOR library 27 | //Buffer: false, 28 | crypto: false, 29 | setImmediate: false 30 | } 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', // 'production' | 'development' | 'none' 5 | // Chosen mode tells webpack to use its built-in optimizations accordingly. 6 | entry: './main.js', // string | object | array 7 | // defaults to ./src 8 | // Here the application starts executing 9 | // and webpack starts bundling 10 | output: { 11 | // options related to how webpack emits results 12 | path: path.resolve(__dirname, 'dist'), // string 13 | // the target directory for all output files 14 | // must be an absolute path (use the Node.js path module) 15 | filename: 'hashlink.min.js', // string 16 | // the url to the output directory resolved relative to the HTML page 17 | library: 'hashlink', // string, 18 | // the name of the exported library 19 | libraryTarget: 'umd', // universal module definition 20 | // the type of the exported library 21 | /* Advanced output configuration (click to show) */ 22 | /* Expert output configuration (on own risk) */ 23 | }, 24 | devtool: 'cheap-module-source-map', // enum 25 | // enhance debugging by adding meta info for the browser devtools 26 | }; 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # hashlink ChangeLog 2 | 3 | ## 0.12.1 - 2020-04-06 4 | 5 | ### Fixed 6 | - Distribute sourcemap file. 7 | 8 | ## 0.12.0 - 2020-01-28 9 | 10 | ### Changed 11 | - Update dependencies. 12 | - Use base58-universal. 13 | 14 | ## 0.11.0 - 2019-09-11 15 | 16 | ### Fixed 17 | - Fix a bug when passing a sha2-256 buffer during the encoding process. 18 | 19 | ### Changed 20 | - **BREAKING**: Rename hl.create to hl.encode to bring it inline w/ hl.decode(). 21 | 22 | ## 0.10.4 - 2019-09-06 23 | 24 | ### Fixed 25 | - Distribute all js files. 26 | 27 | ## 0.10.3 - 2019-09-06 28 | 29 | ### Fixed 30 | - Distribute hl binary. 31 | 32 | ## 0.10.2 - 2019-09-06 33 | 34 | ### Fixed 35 | - Path in package.json for hl binary. 36 | 37 | ## 0.10.1 - 2019-08-21 38 | 39 | ### Added 40 | - Browser demo for Github pages. 41 | - Instructions for use in browser environments. 42 | 43 | ## 0.10.0 - 2019-08-21 44 | 45 | ### Added 46 | - Browser packaging. 47 | - Webpack support. 48 | 49 | ### Fixed 50 | - Style and doc fixes. 51 | - Simplify and enable Karma testing. 52 | - Currently using a CBOR library that uses a Buffer polyfill. 53 | - Test codec buffer handling. 54 | 55 | ## 0.9.0 - 2019-08-20 56 | 57 | ### Added 58 | - Add Acknowledgements to README.md 59 | 60 | ## 0.8.0 - 2019-08-20 61 | 62 | ### Added 63 | - Add initial implementation for first release. 64 | 65 | - See git history for changes previous to this release. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Digital Bazaar, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-node: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x, 14.x] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - run: npm install 19 | - name: Run test with Node.js ${{ matrix.node-version }} 20 | run: npm run test-node 21 | test-karma: 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | strategy: 25 | matrix: 26 | node-version: [14.x] 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - run: npm install 34 | - name: Run karma tests 35 | run: npm run test-karma 36 | lint: 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | strategy: 40 | matrix: 41 | node-version: [14.x] 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v1 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | - run: npm install 49 | - name: Run eslint 50 | run: npm run lint 51 | coverage: 52 | needs: [test-node, test-karma] 53 | runs-on: ubuntu-latest 54 | timeout-minutes: 10 55 | strategy: 56 | matrix: 57 | node-version: [14.x] 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Use Node.js ${{ matrix.node-version }} 61 | uses: actions/setup-node@v1 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | - run: npm install 65 | - name: Generate coverage report 66 | run: npm run coverage-ci 67 | - name: Upload coverage to Codecov 68 | uses: codecov/codecov-action@v1 69 | with: 70 | file: ./coverage/lcov.info 71 | fail_ci_if_error: true 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashlink", 3 | "version": "0.12.2-0", 4 | "description": "Javascript Cryptographic Hyperlinks Library (hashlink)", 5 | "license": "BSD-3-Clause", 6 | "main": "index.js", 7 | "bin": { 8 | "hl": "./bin/hl" 9 | }, 10 | "scripts": { 11 | "test": "npm run test-node", 12 | "test-node": "cross-env NODE_ENV=test mocha -t 30000 -R ${REPORTER:-spec} test/unit/index.js", 13 | "test-karma": "karma start", 14 | "lint": "eslint .", 15 | "prepublish": "npm run build", 16 | "build": "webpack", 17 | "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm run test-node", 18 | "coverage-ci": "cross-env NODE_ENV=test nyc --reporter=lcovonly npm run test-node", 19 | "coverage-report": "nyc report" 20 | }, 21 | "files": [ 22 | "Hashlink.js", 23 | "bin/hl", 24 | "codecs.js", 25 | "crypto-browser.js", 26 | "crypto.js", 27 | "dist/*.js", 28 | "dist/*.map", 29 | "index.js", 30 | "main.js", 31 | "util-browser.js", 32 | "util.js" 33 | ], 34 | "dependencies": { 35 | "base58-universal": "^1.0.0", 36 | "blakejs": "^1.1.0", 37 | "borc": "^2.1.1", 38 | "esm": "^3.2.22", 39 | "isomorphic-webcrypto": "^1.6.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.8.3", 43 | "@babel/plugin-transform-modules-commonjs": "^7.8.3", 44 | "@babel/plugin-transform-runtime": "^7.8.3", 45 | "@babel/preset-env": "^7.8.3", 46 | "@babel/runtime": "^7.8.3", 47 | "babel-loader": "^8.0.5", 48 | "chai": "^4.2.0", 49 | "chai-bytes": "^0.1.2", 50 | "cross-env": "^6.0.3", 51 | "eslint": "^7.20.0", 52 | "eslint-config-digitalbazaar": "^2.6.1", 53 | "eslint-plugin-jsdoc": "^32.2.0", 54 | "jsonld": "^3.0.1", 55 | "karma": "^4.4.1", 56 | "karma-babel-preprocessor": "^8.0.0", 57 | "karma-chai": "^0.1.0", 58 | "karma-chrome-launcher": "^3.1.0", 59 | "karma-firefox-launcher": "^1.3.0", 60 | "karma-ie-launcher": "^1.0.0", 61 | "karma-mocha": "^1.3.0", 62 | "karma-mocha-reporter": "^2.2.5", 63 | "karma-sauce-launcher": "^2.0.2", 64 | "karma-sourcemap-loader": "^0.3.7", 65 | "karma-webpack": "^4.0.2", 66 | "mocha": "^7.0.0", 67 | "mocha-lcov-reporter": "^1.3.0", 68 | "nyc": "^15.1.0", 69 | "webpack": "^4.41.5", 70 | "webpack-cli": "^3.3.10" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com/digitalbazaar/hashlink" 75 | }, 76 | "keywords": [ 77 | "hashlink", 78 | "multihash", 79 | "cryptography" 80 | ], 81 | "author": { 82 | "name": "Digital Bazaar, Inc.", 83 | "email": "support@digitalbazaar.com", 84 | "url": "https://digitalbazaar.com/" 85 | }, 86 | "bugs": { 87 | "url": "https://github.com/digitalbazaar/hashlink/issues" 88 | }, 89 | "homepage": "https://github.com/digitalbazaar/hashlink", 90 | "module": "main.js", 91 | "browser": { 92 | "./crypto.js": "./crypto-browser.js", 93 | "./util.js": "./util-browser.js" 94 | }, 95 | "engines": { 96 | "node": ">=8.6.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 'use strict'; 5 | 6 | //import * as base58 from 'base58-universal'; 7 | //import crypto from './crypto.js'; 8 | import * as defaultCodecs from './codecs.js'; 9 | import {Hashlink} from './Hashlink.js'; 10 | //import {stringToUint8Array} from './util.js'; 11 | 12 | // setup exports for this module 13 | export {Hashlink} from './Hashlink.js'; 14 | export { 15 | encode, 16 | decode, 17 | verify, 18 | }; 19 | 20 | // setup the default encoder/decoder 21 | const hlDefault = new Hashlink(); 22 | hlDefault.use(new defaultCodecs.MultihashSha2256()); 23 | hlDefault.use(new defaultCodecs.MultihashBlake2b64()); 24 | hlDefault.use(new defaultCodecs.MultibaseBase58btc()); 25 | 26 | /** 27 | * Encodes a hashlink. If only a `url` parameter is provided, the URL is 28 | * fetched, transformed, and encoded into a hashlink. If a data parameter 29 | * is provided, the hashlink is encoded from the data. 30 | * 31 | * @param {object} options - The options for the encode operation. 32 | * @param {Uint8Array} options.data - The data associated with the given URL. If 33 | * provided, this data is used to encode the cryptographic hash. 34 | * @param {Array} options.urls - One or more URLs that contain the data 35 | * referred to by the hashlink. 36 | * @param {Array} options.codecs - One or more URLs that contain the data 37 | * referred to by the hashlink. 38 | * @param {object} options.meta - A set of key-value metadata that will be 39 | * encoded into the hashlink. 40 | * 41 | * @returns {Promise} Resolves to a string that is a hashlink. 42 | */ 43 | async function encode({data, urls, url, 44 | codecs = ['mh-sha2-256', 'mb-base58-btc'], meta = {}}) { 45 | 46 | if(url && !urls) { 47 | urls = [url]; 48 | } 49 | 50 | return hlDefault.encode({data, urls, codecs, meta}); 51 | } 52 | 53 | /** 54 | * Decodes a hashlink resulting in an object with key-value pairs 55 | * representing the values encoded in the hashlink. 56 | * 57 | * @param {object} options - The options for the encode operation. 58 | * @param {string} options.hashlink - The encoded hashlink value to decode. 59 | * 60 | * @returns {object} Returns an object with the decoded hashlink values. 61 | */ 62 | function decode({ 63 | /* eslint-disable-next-line no-unused-vars */ 64 | hashlink 65 | }) { 66 | throw new Error('Not implemented.'); 67 | } 68 | 69 | /** 70 | * Verifies a hashlink resulting in a simple true or false value. 71 | * 72 | * @param {object} options - The options for the encode operation. 73 | * @param {string} options.hashlink - The encoded hashlink value to verify. 74 | * @param {Uint8Array} options.data - Optional data to use when verifying 75 | * hashlink. 76 | * @param {Array} options.resolvers - An array of Objects with key-value 77 | * pairs. Each object must contain a `scheme` key associated with a 78 | * Function({url, options}) that resolves any URL with the given scheme 79 | * and options to data. 80 | * 81 | * @returns {Promise} Return true if the hashlink is valid, false 82 | * otherwise. 83 | */ 84 | async function verify({hashlink, data, resolvers}) { 85 | return hlDefault.verify({hashlink, data, resolvers}); 86 | } 87 | -------------------------------------------------------------------------------- /bin/hl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs-extra'); 4 | const program = require('commander'); 5 | const hl = require('../'); 6 | const {URL} = require('url'); 7 | 8 | function collect(value, previous) { 9 | return previous.concat([value]); 10 | } 11 | 12 | program 13 | .command('encode') 14 | .alias('e') 15 | .description('Encode a hashlink given a URL and/or data.') 16 | .option('-u, --url ', 'target URL for the hashlink', collect, []) 17 | .option('-l, --legacy', 'encode a hashlink for a legacy URL') 18 | .option('-h, --hash ', 'cryptographic hash algorithm', 'sha2-256') 19 | .option('-b, --base ', 'base encoding', 'base58-btc') 20 | .action(async (dataFile, options) => { 21 | try { 22 | let data, urls, codecs, meta = undefined; 23 | // read data from disk if provided 24 | if(dataFile) { 25 | data = fs.readFileSync(dataFile); 26 | } 27 | if(options.url.length > 0) { 28 | urls = options.url; 29 | } 30 | // generate codecs from command line options 31 | codecs = ['mh-' + options.hash, 'mb-' + options.base]; 32 | const hashlink = await hl.encode({data, urls, codecs, meta}); 33 | 34 | if(options.legacy) { 35 | // print out a legacy hashlink 36 | const rawHashlink = hashlink.split(':')[1]; 37 | options.url.forEach(inputUrl => { 38 | const outputUrl = new URL(inputUrl); 39 | outputUrl.searchParams.set('hl', rawHashlink); 40 | console.log(outputUrl.toString()); 41 | }); 42 | } else { 43 | // print out the hashlink 44 | console.log(hashlink); 45 | } 46 | } catch(e) { 47 | console.error(`${e}`); 48 | process.exit(1); 49 | } 50 | }) 51 | .on('--help', () => { 52 | console.log(); 53 | console.log(' Examples: '); 54 | console.log(); 55 | console.log(' hl encode hw.txt'); 56 | console.log(' hl encode --url "https://example.com/hw.txt"'); 57 | console.log(' hl encode --url "https://example.com/hw.txt" hw.txt'); 58 | console.log(); 59 | }); 60 | 61 | program 62 | .command('verify') 63 | .alias('v') 64 | .description('Verify a hashlink.') 65 | .option('-u, --url ', 'target URL for the hashlink') 66 | .option('-f, --file ', 'load data to hash from specified file') 67 | .action(async (hashlink, options) => { 68 | try { 69 | let data, urls, codecs, meta = undefined; 70 | // read data from disk if provided 71 | if(options.file) { 72 | data = fs.readFileSync(options.file); 73 | } 74 | if(options.url) { 75 | urls = options.url; 76 | } 77 | // check to see if hashlink is valid 78 | const valid = await hl.verify({data, hashlink}); 79 | if(valid) { 80 | console.log('hashlink is valid'); 81 | } else { 82 | throw new Error('hashlink is invalid'); 83 | } 84 | } catch(e) { 85 | console.error(`${e}`); 86 | process.exit(1); 87 | } 88 | }) 89 | .on('--help', () => { 90 | console.log(); 91 | console.log(' Examples: '); 92 | console.log(); 93 | console.log(' hl verify --file hw.txt'); 94 | console.log(' hl verify --url "https://example.com/hw.txt"'); 95 | console.log(); 96 | }); 97 | 98 | program.parse(process.argv); 99 | -------------------------------------------------------------------------------- /codecs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 'use strict'; 5 | 6 | import * as base58 from 'base58-universal'; 7 | import {blake2b} from 'blakejs'; 8 | import crypto from './crypto.js'; 9 | import {TextDecoder, stringToUint8Array} from './util.js'; 10 | 11 | class MultihashSha2256 { 12 | /** 13 | * Creates a new MultihashSha2256 data codec. 14 | * 15 | * @returns {MultihashSha2256} A MultihashSha2256 used to encode and decode 16 | * Multihash SHA-2 256-bit values. 17 | */ 18 | constructor() { 19 | this.identifier = new Uint8Array([0x12, 0x20]); 20 | this.algorithm = 'mh-sha2-256'; 21 | } 22 | 23 | /** 24 | * Encoder that takes a Uint8Array as input and performs a SHA-2 25 | * cryptographic hash on the data and outputs a multihash-encoded value. 26 | * 27 | * @param {Uint8Array} input - The input for the encode function. 28 | * 29 | * @returns {Uint8Array} The output of the encode function. 30 | */ 31 | async encode(input) { 32 | const sha2256 = new Uint8Array( 33 | await crypto.subtle.digest({name: 'SHA-256'}, input)); 34 | const mhsha2256 = new Uint8Array( 35 | sha2256.byteLength + this.identifier.byteLength); 36 | 37 | mhsha2256.set(this.identifier, 0); 38 | mhsha2256.set(sha2256, this.identifier.byteLength); 39 | 40 | return mhsha2256; 41 | } 42 | } 43 | 44 | class MultihashBlake2b64 { 45 | /** 46 | * Creates a new MultihashBlake2b64 data codec. 47 | * 48 | * @returns {MultihashBlake2b64} A MultihashBlake2b64 used to encode and 49 | * decode Multihash Blake2b 64-bit values. 50 | */ 51 | constructor() { 52 | this.identifier = new Uint8Array([0xb2, 0x08, 0x08]); 53 | this.algorithm = 'mh-blake2b-64'; 54 | } 55 | 56 | /** 57 | * Encoder function that takes a Uint8Array as input and performs a blake2b 58 | * cryptographic hash on the data and outputs a multihash-encoded value. 59 | * 60 | * @param {Uint8Array} input - The input for the encode function. 61 | * 62 | * @returns {Uint8Array} The output of the encode function. 63 | */ 64 | async encode(input) { 65 | const blake2b64 = blake2b(input, null, 8); 66 | const mhblake2b64 = new Uint8Array( 67 | blake2b64.byteLength + this.identifier.byteLength); 68 | 69 | mhblake2b64.set(this.identifier, 0); 70 | mhblake2b64.set(blake2b64, this.identifier.byteLength); 71 | 72 | return mhblake2b64; 73 | } 74 | } 75 | 76 | class MultibaseBase58btc { 77 | /** 78 | * Creates a new MultibaseBase58btc data codec. 79 | * 80 | * @returns {MultibaseBase58btc} A MultibaseBase58btc used to encode and 81 | * decode Multibase base58btc values. 82 | */ 83 | constructor() { 84 | this.identifier = new Uint8Array([0x7a]); 85 | this.algorithm = 'mb-base58-btc'; 86 | } 87 | 88 | /** 89 | * Encoder function that takes a Uint8Array as input and performs a multibase 90 | * base58btc encoding on the data. 91 | * 92 | * @param {Uint8Array} input - The input for the encode function. 93 | * 94 | * @returns {Uint8Array} The output of the encode function. 95 | */ 96 | encode(input) { 97 | return new Uint8Array(stringToUint8Array('z' + base58.encode(input))); 98 | } 99 | 100 | /** 101 | * Decoder function that takes a Uint8Array as input and performs a multibase 102 | * base58btc decode on the data. 103 | * 104 | * @param {Uint8Array} input - The input for the decode function. 105 | * 106 | * @returns {Uint8Array} The output of the decode function. 107 | */ 108 | decode(input) { 109 | return base58.decode(new TextDecoder('utf-8').decode(input.slice(1))); 110 | } 111 | } 112 | 113 | export { 114 | MultihashSha2256, 115 | MultihashBlake2b64, 116 | MultibaseBase58btc 117 | }; 118 | -------------------------------------------------------------------------------- /Hashlink.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 'use strict'; 5 | 6 | import * as cbor from 'borc'; 7 | import {TextDecoder, stringToUint8Array} from './util.js'; 8 | 9 | export class Hashlink { 10 | /** 11 | * Encodes a new Hashlink instance that can be used to encode or decode 12 | * data at URLs. 13 | * 14 | * @returns {Hashlink} A Hashlink used to encode and decode cryptographic 15 | * hyperlinks. 16 | */ 17 | constructor() { 18 | this.registeredCodecs = {}; 19 | } 20 | 21 | /** 22 | * Encodes a hashlink. If only a `url` parameter is provided, the URL is 23 | * fetched, transformed, and encoded into a hashlink. If a data parameter 24 | * is provided, the hashlink is encoded from the data. 25 | * 26 | * @param {object} options - The options for the encode operation. 27 | * @param {Uint8Array} [options.data] - The data associated with the given 28 | * URL. If provided, this data is used to encode the cryptographic hash. 29 | * @param {Array} options.codecs - One or more codecs that should be used 30 | * to encode the data. 31 | * @param {Array} [options.urls] - One or more URLs that contain the data 32 | * referred to by the hashlink. 33 | * @param {object} [options.meta] - A set of key-value metadata that will be 34 | * encoded into the hashlink. 35 | * 36 | * @returns {Promise} Resolves to a string that is a hashlink. 37 | */ 38 | async encode({data, urls, codecs, meta = {}}) { 39 | // ensure data or urls are provided 40 | if(data === undefined && urls === undefined) { 41 | throw new Error('Either `data` or `urls` must be provided.'); 42 | } 43 | 44 | // ensure codecs are provided 45 | if(codecs === undefined) { 46 | throw new Error('The hashlink creation `codecs` must be provided.'); 47 | } 48 | 49 | if(urls !== undefined) { 50 | // ensure urls are an array 51 | if(!Array.isArray(urls)) { 52 | urls = [urls]; 53 | } 54 | 55 | // ensure all URLs are strings 56 | urls.forEach(url => { 57 | if(typeof url !== 'string') { 58 | throw new Error(`URL "${url}" must be a string.`); 59 | } 60 | }); 61 | 62 | // merge meta options with urls 63 | meta = {...meta, url: urls}; 64 | } 65 | 66 | // generate the encoded cryptographic hash 67 | const outputData = await codecs.reduce(async (output, codec) => { 68 | const encoder = this.registeredCodecs[codec]; 69 | if(encoder === undefined) { 70 | throw new Error(`Unknown cryptographic hash encoder "${encoder}".`); 71 | } 72 | 73 | return encoder.encode(await output); 74 | }, data); 75 | 76 | // generate the encoded metadata 77 | const metadata = new Map(); 78 | if(meta.url) { 79 | metadata.set(0x0f, meta.url); 80 | } 81 | if(meta['content-type']) { 82 | metadata.set(0x0e, meta['content-type']); 83 | } 84 | if(meta.experimental) { 85 | metadata.set(0x0d, meta.experimental); 86 | } 87 | if(meta.transform) { 88 | metadata.set(0x0c, meta.transform); 89 | } 90 | 91 | // build the hashlink 92 | const textDecoder = new TextDecoder(); 93 | let hashlink = 'hl:' + textDecoder.decode(outputData); 94 | 95 | // append meta data if present 96 | if(metadata.size > 0) { 97 | const baseEncodingCodec = codecs[codecs.length - 1]; 98 | const cborData = new Uint8Array(cbor.encode(metadata)); 99 | const mbCborData = textDecoder.decode( 100 | this.registeredCodecs[baseEncodingCodec].encode(cborData)); 101 | hashlink += ':' + mbCborData; 102 | } 103 | 104 | return hashlink; 105 | } 106 | 107 | /** 108 | * Decodes a hashlink resulting in an object with key-value pairs 109 | * representing the values encoded in the hashlink. 110 | * 111 | * @param {object} options - The options for the encode operation. 112 | * @param {string} options.hashlink - The encoded hashlink value to decode. 113 | * 114 | * @returns {object} Returns an object with the decoded hashlink values. 115 | */ 116 | decode({ 117 | /* eslint-disable-next-line no-unused-vars */ 118 | hashlink 119 | }) { 120 | throw new Error('Not implemented.'); 121 | } 122 | 123 | /** 124 | * Verifies a hashlink resulting in a simple true or false value. 125 | * 126 | * @param {object} options - The options for the encode operation. 127 | * @param {string} options.hashlink - The encoded hashlink value to verify. 128 | * @param {string} options.data - The data to use for the hashlink. 129 | * @param {Array} options.resolvers - An array of Objects with key-value 130 | * pairs. Each object must contain a `scheme` key associated with a 131 | * Function({url, options}) that resolves any URL with the given scheme 132 | * and options to data. 133 | * 134 | * @returns {Promise} Return true if the hashlink is valid, false 135 | * otherwise. 136 | */ 137 | async verify({ 138 | data, hashlink, 139 | /* eslint-disable-next-line no-unused-vars */ 140 | resolvers 141 | }) { 142 | const components = hashlink.split(':'); 143 | 144 | if(components.length > 3) { 145 | throw new Error(`Hashlink "${hashlink}" is invalid; ` + 146 | 'it contains more than two colons.'); 147 | } 148 | 149 | // determine the base encoding decoder and decode the multihash value 150 | const multibaseEncodedMultihash = stringToUint8Array(components[1]); 151 | const multibaseDecoder = this._findDecoder(multibaseEncodedMultihash); 152 | const encodedMultihash = multibaseDecoder.decode(multibaseEncodedMultihash); 153 | 154 | // determine the multihash decoder 155 | const multihashDecoder = this._findDecoder(encodedMultihash); 156 | 157 | // extract the metadata to discover extra codecs 158 | const codecs = []; 159 | if(components.length === 3) { 160 | const encodedMeta = stringToUint8Array(components[2]); 161 | const cborMeta = multibaseDecoder.decode(encodedMeta); 162 | const meta = cbor.decode(cborMeta); 163 | // extract transforms if they exist 164 | if(meta.has(0x0c)) { 165 | codecs.push(...meta.get(0x0c)); 166 | } 167 | } 168 | 169 | // generate the complete list of codecs 170 | codecs.push(multihashDecoder.algorithm, multibaseDecoder.algorithm); 171 | 172 | // generate the hashlink 173 | const generatedHashlink = await this.encode({data, codecs}); 174 | const generatedComponents = generatedHashlink.split(':'); 175 | 176 | // check to see if the encoded hashes match 177 | return components[1] === generatedComponents[1]; 178 | } 179 | 180 | /** 181 | * Extends the Hashlink instance such that it can support new codecs 182 | * such as new cryptographic hashing, base-encoding, and resolution 183 | * mechanisms. 184 | * 185 | * @param {Codec} codec - A Codec instance that has a .encode() 186 | * and a .decode() method. It must also have an `identifier` and 187 | * `algorithm` property. 188 | */ 189 | use(codec) { 190 | this.registeredCodecs[codec.algorithm] = codec; 191 | } 192 | 193 | /** 194 | * Finds a registered decoder for a given set of bytes or throws an Error. 195 | * 196 | * @param {Uint8Array} bytes - A stream of bytes to use when matching against 197 | * the registered decoders. 198 | * @returns A registered decoder that can be used to encode/decode the byte 199 | * stream. 200 | */ 201 | _findDecoder(bytes) { 202 | const decoders = Object.values(this.registeredCodecs); 203 | const decoder = decoders.find( 204 | decoder => decoder.identifier.every((id, i) => id === bytes[i])); 205 | if(!decoder) { 206 | throw new Error('Could not determine decoder for: ' + bytes); 207 | } 208 | return decoder; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | const chai = require('chai'); 5 | const {Hashlink} = require('../../'); 6 | const hl = require('../../'); 7 | const jsonld = require('jsonld'); 8 | const {stringToUint8Array, TextDecoder} = require('../../util.js'); 9 | const defaultCodecs = require('../../codecs.js'); 10 | 11 | chai.should(); 12 | 13 | describe('hashlink library', function() { 14 | // setup test data 15 | const testData = stringToUint8Array('Hello World!\n'); 16 | const exampleUrl = 'https://example.com/hw.txt'; 17 | 18 | // setup JSON-LD tests 19 | /* eslint-disable quotes */ 20 | const jsonldData = { 21 | "@type": ["http://schema.org/Person"], 22 | "http://schema.org/jobTitle": [{"@value": "Professor"}], 23 | "http://schema.org/name": [{"@value": "Jane Doe"}], 24 | "http://schema.org/telephone": [{"@value": "(425) 123-4567"}], 25 | "http://schema.org/url": [{"@id": "http://www.janedoe.com"}] 26 | }; 27 | /* eslint-enable quotes */ 28 | 29 | // setup URDNA2015 codec 30 | class Urdna2015 { 31 | constructor() { 32 | this.identifier = stringToUint8Array('urdna2015'); 33 | this.algorithm = 'urdna2015'; 34 | } 35 | 36 | async encode(input) { 37 | const inputJsonld = JSON.parse(new TextDecoder().decode(input)); 38 | return stringToUint8Array(await jsonld.canonize( 39 | inputJsonld, {format: 'application/n-quads'})); 40 | } 41 | } 42 | 43 | describe(`Hashlink class`, function() { 44 | describe(`encode() [sha2-256]`, function() { 45 | // setup the encoder/decoder 46 | const hlInstance = new Hashlink(); 47 | hlInstance.use(new defaultCodecs.MultihashSha2256()); 48 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 49 | 50 | it('encode({data, codecs}) should encode a hashlink' + 51 | '', async function() { 52 | const result = await hlInstance.encode({ 53 | data: testData, 54 | codecs: ['mh-sha2-256', 'mb-base58-btc'] 55 | }); 56 | 57 | result.should.equal( 58 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH'); 59 | }); 60 | 61 | it('encode({data, urls, codecs}) should encode a hashlink' + 62 | '', async function() { 63 | const result = await hlInstance.encode({ 64 | data: testData, 65 | urls: [exampleUrl], 66 | codecs: ['mh-sha2-256', 'mb-base58-btc'] 67 | }); 68 | 69 | result.should.equal( 70 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH:' + 71 | 'z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'); 72 | }); 73 | 74 | it('encode({data, urls, meta, codecs}) should encode a hashlink' + 75 | '', async function() { 76 | const result = await hlInstance.encode({ 77 | data: testData, 78 | urls: [exampleUrl], 79 | meta: { 80 | 'content-type': 'text/plain' 81 | }, 82 | codecs: ['mh-sha2-256', 'mb-base58-btc'] 83 | }); 84 | 85 | result.should.equal( 86 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH:' + 87 | 'zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qdfW3bb6W8dVdRu'); 88 | }); 89 | }); 90 | 91 | describe(`encode() [blake2b-64]`, function() { 92 | // setup the encoder/decoder 93 | const hlInstance = new Hashlink(); 94 | hlInstance.use(new defaultCodecs.MultihashBlake2b64()); 95 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 96 | 97 | it('encode({data, codecs}) should encode a hashlink' + 98 | '', async function() { 99 | const result = await hlInstance.encode({ 100 | data: testData, 101 | codecs: ['mh-blake2b-64', 'mb-base58-btc'] 102 | }); 103 | 104 | result.should.equal('hl:zm9YZpCjPLPJ4Epc'); 105 | }); 106 | 107 | it('encode({data, urls, codecs}) should encode a hashlink' + 108 | '', async function() { 109 | const result = await hlInstance.encode({ 110 | data: testData, 111 | urls: [exampleUrl], 112 | codecs: ['mh-blake2b-64', 'mb-base58-btc'] 113 | }); 114 | 115 | result.should.equal( 116 | 'hl:zm9YZpCjPLPJ4Epc:' + 117 | 'z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'); 118 | }); 119 | 120 | it('encode({data, urls, meta, codecs}) should encode a hashlink' + 121 | '', async function() { 122 | const result = await hlInstance.encode({ 123 | data: testData, 124 | urls: [exampleUrl], 125 | meta: { 126 | 'content-type': 'text/plain' 127 | }, 128 | codecs: ['mh-blake2b-64', 'mb-base58-btc'] 129 | }); 130 | 131 | result.should.equal( 132 | 'hl:zm9YZpCjPLPJ4Epc:' + 133 | 'zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qdfW3bb6W8dVdRu'); 134 | }); 135 | }); 136 | 137 | describe(`encode() [urdna2015]`, function() { 138 | // setup the encoder/decoder 139 | const hlInstance = new Hashlink(); 140 | hlInstance.use(new Urdna2015()); 141 | hlInstance.use(new defaultCodecs.MultihashBlake2b64()); 142 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 143 | 144 | it('encode({data, codecs}) should encode a hashlink' + 145 | '', async function() { 146 | const result = await hlInstance.encode({ 147 | data: stringToUint8Array(JSON.stringify(jsonldData)), 148 | codecs: ['urdna2015', 'mh-blake2b-64', 'mb-base58-btc'], 149 | transform: ['urdna2015'] 150 | }); 151 | 152 | result.should.equal('hl:zm9YaHWNePhdaQ2J'); 153 | }); 154 | 155 | it('encode({data, urls, codecs}) should encode a hashlink' + 156 | '', async function() { 157 | const result = await hlInstance.encode({ 158 | data: stringToUint8Array(JSON.stringify(jsonldData)), 159 | urls: [exampleUrl], 160 | codecs: ['urdna2015', 'mh-blake2b-64', 'mb-base58-btc'], 161 | transform: ['urdna2015'] 162 | }); 163 | 164 | result.should.equal( 165 | 'hl:zm9YaHWNePhdaQ2J:' + 166 | 'z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'); 167 | }); 168 | 169 | it('encode({data, urls, meta, codecs}) should encode a hashlink' + 170 | '', async function() { 171 | const result = await hlInstance.encode({ 172 | data: stringToUint8Array(JSON.stringify(jsonldData)), 173 | urls: [exampleUrl], 174 | meta: { 175 | 'content-type': 'text/plain' 176 | }, 177 | codecs: ['urdna2015', 'mh-blake2b-64', 'mb-base58-btc'], 178 | transform: ['urdna2015'] 179 | }); 180 | 181 | result.should.equal( 182 | 'hl:zm9YaHWNePhdaQ2J:' + 183 | 'zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qdfW3bb6W8dVdRu'); 184 | }); 185 | }); 186 | 187 | describe(`verify() [sha2-256]`, function() { 188 | // setup the encoder/decoder 189 | const hlInstance = new Hashlink(); 190 | hlInstance.use(new defaultCodecs.MultihashSha2256()); 191 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 192 | 193 | it('verify({data, hashlink}) should verify a hashlink', async function() { 194 | const result = await hlInstance.verify({ 195 | data: testData, 196 | hashlink: 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH' 197 | }); 198 | 199 | chai.expect(result).to.equal(true); 200 | }); 201 | }); 202 | 203 | describe(`verify() [blake2b-64]`, function() { 204 | // setup the encoder/decoder 205 | const hlInstance = new Hashlink(); 206 | hlInstance.use(new defaultCodecs.MultihashBlake2b64()); 207 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 208 | 209 | it('verify({data, hashlink}) should verify a hashlink', async function() { 210 | const result = await hlInstance.verify({ 211 | data: testData, 212 | hashlink: 'hl:zm9YZpCjPLPJ4Epc' 213 | }); 214 | 215 | chai.expect(result).to.equal(true); 216 | }); 217 | }); 218 | 219 | describe(`verify() [urdna2015]`, function() { 220 | // setup the encoder/decoder 221 | const hlInstance = new Hashlink(); 222 | hlInstance.use(new Urdna2015()); 223 | hlInstance.use(new defaultCodecs.MultihashSha2256()); 224 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 225 | 226 | it('verify({data, hashlink}) should verify a hashlink', async function() { 227 | const result = await hlInstance.verify({ 228 | data: stringToUint8Array(JSON.stringify(jsonldData)), 229 | hashlink: 230 | 'hl:zQmVcHtE3hUCF3s6fgjohUL3ANsKGnmRC9UsEaAjZuvgzdc:' + 231 | 'zER21ZLCmb3bkKNtm8g' 232 | }); 233 | 234 | chai.expect(result).to.equal(true); 235 | }); 236 | }); 237 | 238 | describe(`use()`, function() { 239 | // setup the encoder/decoder 240 | const hlInstance = new Hashlink(); 241 | hlInstance.use(new Urdna2015()); 242 | hlInstance.use(new defaultCodecs.MultihashSha2256()); 243 | hlInstance.use(new defaultCodecs.MultibaseBase58btc()); 244 | 245 | it('use() with custom JSON-LD transform', async function() { 246 | const result = await hlInstance.encode({ 247 | data: stringToUint8Array(JSON.stringify(jsonldData)), 248 | codecs: ['urdna2015', 'mh-sha2-256', 'mb-base58-btc'] 249 | }); 250 | 251 | result.should.equal( 252 | 'hl:zQmVcHtE3hUCF3s6fgjohUL3ANsKGnmRC9UsEaAjZuvgzdc'); 253 | }); 254 | }); 255 | }); 256 | 257 | describe(`convenience functionality`, function() { 258 | describe(`encode()`, function() { 259 | 260 | it('encode({data}) should encode a hashlink', async function() { 261 | const result = await hl.encode({ 262 | data: testData 263 | }); 264 | 265 | result.should.equal( 266 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH'); 267 | }); 268 | 269 | it('encode({data, urls}) should encode a hashlink', async function() { 270 | const result = await hl.encode({ 271 | data: testData, 272 | urls: [exampleUrl] 273 | }); 274 | 275 | result.should.equal( 276 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH:' + 277 | 'z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'); 278 | }); 279 | 280 | it('encode({data, urls, meta}) should encode a hashlink' + 281 | '', async function() { 282 | const result = await hl.encode({ 283 | data: testData, 284 | urls: [exampleUrl], 285 | meta: { 286 | 'content-type': 'text/plain' 287 | } 288 | }); 289 | 290 | result.should.equal( 291 | 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH:' + 292 | 'zCwPSdabLuj3jue1qYujzunnKwpL4myKdyeqySyFhnzZ8qdfW3bb6W8dVdRu'); 293 | }); 294 | }); 295 | 296 | describe(`verify() [sha2-256]`, function() { 297 | 298 | it('verify({data, hashlink}) should verify a hashlink', async function() { 299 | const result = await hl.verify({ 300 | data: testData, 301 | hashlink: 'hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH' 302 | }); 303 | 304 | chai.expect(result).to.equal(true); 305 | }); 306 | }); 307 | 308 | describe(`verify() [blake2b-64]`, function() { 309 | it('verify({data, hashlink}) should verify a hashlink', async function() { 310 | const result = await hl.verify({ 311 | data: testData, 312 | hashlink: 'hl:zm9YZpCjPLPJ4Epc' 313 | }); 314 | 315 | chai.expect(result).to.equal(true); 316 | }); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Javascript Cryptographic Hyperlinks Library _(hashlink)_ 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/hashlink.svg)](https://npm.im/hashlink) 4 | [![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/hashlink/Node.js%20CI)](https://github.com/digitalbazaar/hashlink/actions?query=workflow%3A%22Node.js+CI%22) 5 | [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/hashlink)](https://codecov.io/gh/digitalbazaar/hashlink) 6 | 7 | A Javascript library for encoding, decoding, and verifying hashlinks as 8 | defined in the 9 | [IETF Hashlink draft spec](https://tools.ietf.org/html/draft-sporny-hashlink). 10 | 11 | Example Hashlinks: 12 | 13 | - Regular Hashlink (without URL encoded) 14 | - `hl:zm9YZpCjPLPJ4Epc` 15 | - Regular Hashlink (with URL encoded): 16 | - `hl:zm9YZpCjPLPJ4Epc:z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy` 17 | - Hashlink as a query parameter: 18 | - `https://example.com/hw.txt?hl=zm9YZpCjPLPJ4Epc` 19 | 20 | ## Table of Contents 21 | 22 | - [Security](#security) 23 | - [Background](#background) 24 | - [Install](#install) 25 | - [Usage](#usage) 26 | - [API](#api) 27 | - [Testing](#testing) 28 | - [Contribute](#contribute) 29 | - [Commercial Support](#commercial-support) 30 | - [License](#license) 31 | 32 | ## Security 33 | 34 | Security is hard. Cryptography is harder. When in doubt, leave it to the 35 | professionals. 36 | 37 | While the authors of this library are professionals, and they have used 38 | cryptographic primitives and libraries written by people more capable than them, 39 | bugs happen. Implementers that use this library are urged to study the code and 40 | perform a review of their own before using this library in a production system. 41 | 42 | It is also possible to misuse this library in a variety of ways if you don't 43 | know what you are doing. If you are ever in doubt, remember that cryptography 44 | is hard. Leave it to the professionals. 45 | 46 | ## Background 47 | 48 | When using a hyperlink to fetch a resource from the Internet, it is often 49 | useful to know if the resource has changed since the data was published. 50 | Cryptographic hashes, such as SHA-256, are often used to determine if 51 | published data has changed in unexpected ways. Due to the nature of most 52 | hyperlinks, the cryptographic hash is often published separately from the 53 | link itself. The Hashlink specification describes a data model and serialization 54 | formats for expressing cryptographically protected hyperlinks. The mechanisms 55 | described in the Hashlink specification enables a system to publish a hyperlink 56 | in a way that empowers a consuming application to determine if the resource 57 | associated with the hyperlink has changed in unexpected ways. 58 | 59 | See also (related specs): 60 | 61 | * [IETF Hashlink draft spec](https://tools.ietf.org/html/draft-sporny-hashlink) 62 | * [IETF Multihash draft spec](https://tools.ietf.org/html/draft-multiformats-multihash) 63 | * [IETF Multibase draft spec](https://tools.ietf.org/html/draft-multiformats-multibase) 64 | 65 | ## Install 66 | 67 | To use this library in the browser, you can include the latest version 68 | via a simple script tag: 69 | 70 | ``` 71 | 72 | ``` 73 | 74 | To use the library in Node.js: 75 | 76 | - Node.js 8.3+ required. 77 | 78 | To install locally (for development): 79 | 80 | ``` 81 | git clone https://github.com/digitalbazaar/hashlink.git 82 | cd hashlink 83 | npm install 84 | ``` 85 | 86 | ## Usage 87 | 88 | Use on the command line, or see the API section below. 89 | 90 | ### Encoding a Hashlink 91 | 92 | There are a number of ways you can encode a hashlink. The simplest way is to 93 | provide the data directly. 94 | 95 | ```bash 96 | ./bin/hl encode hw.txt 97 | ``` 98 | 99 | You can encode a hashlink from any data published on the Web: 100 | 101 | ```bash 102 | ./bin/hl encode --url "https://example.com/hw.txt" 103 | ``` 104 | 105 | You can also encode a hashlink from data on disk and specify the location on 106 | the web that the data will be published to: 107 | 108 | ```bash 109 | ./bin/hl encode --url "https://example.com/hw.txt" hw.txt 110 | ``` 111 | 112 | Hashlinks are also backwards compatible with legacy URL schemes, which enables 113 | you to use query parameters to encode the hashlink information: 114 | 115 | ```bash 116 | ./bin/hl encode --legacy --url "https://example.com/hw.txt" hw.txt 117 | ``` 118 | 119 | ### Decoding a Hashlink 120 | 121 | To decode a hashlink, you can run the following command: 122 | 123 | ```base 124 | ./bin/hl decode hl:zm9YZpCjPLPJ4Epc:z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy 125 | ``` 126 | 127 | The command above will result in the following output: 128 | 129 | ``` 130 | URLs: https://example.com/hw.txt 131 | sha256sum: 12207f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069 132 | ``` 133 | 134 | ### Verifying a Hashlink 135 | 136 | To verify a hashlink, you can run the following command: 137 | 138 | ``` 139 | ./bin/hl verify --file hw.txt hl:zQmNbCYUrvaVfy6w9b5W3SVTP2newPK5FoeY37QurUEUydH 140 | ``` 141 | 142 | The command above will result in the following output: 143 | 144 | ``` 145 | hashlink is valid 146 | ``` 147 | 148 | ## API 149 | 150 | The API is useful when integrating this library with a larger software system. 151 | 152 | You can use the API in the browser by including the latest version 153 | via a simple script tag: 154 | 155 | ``` 156 | 157 | ``` 158 | 159 | The rest of the examples in this section assume a node.js environment, but 160 | all API calls listed below are also available in the browser version. 161 | 162 | ### Encoding a Hashlink 163 | 164 | You can encode a hashlink from an existing URL (**coming soon**): 165 | 166 | ```js 167 | const hl = require('hashlink'); 168 | 169 | const url = 'https://example.com/hw.txt'; 170 | 171 | // encode a hashlink by fetching the URL content and hashing it 172 | const hlUrl = await hl.encode({url}); 173 | 174 | // print out the hashlink 175 | console.log(hlUrl); 176 | ``` 177 | 178 | You can encode a hashlink from data: 179 | 180 | ```js 181 | const hl = require('hashlink'); 182 | 183 | // encode a hashlink using data to be published at a URL 184 | const data = fs.readFileSync('hw.txt'); 185 | const url = 'https://example.com/hw.txt'; 186 | const hlUrl = await hl.encode({data, url}); 187 | 188 | // print out the hashlink 189 | console.log(hlUrl); 190 | ``` 191 | 192 | You can change the default options used by the hashlink function: 193 | 194 | ```js 195 | const {Hashlink} = require('hashlink'); 196 | const {Urdna2015} = require('hashlink-jsonld'); 197 | 198 | // setup hl library to use RDF Dataset Canonicalization codec 199 | const hl = new Hashlink(); 200 | hl.use(new Urdna2015()); 201 | 202 | // encode a hashlink using canonicalized data published at a URL 203 | const url = 'https://example.com/credential.jsonld'; 204 | // encode the input data using urdna2015 canonicalization algorithm and 205 | // then hash using blake2b with a 64-bit output 206 | const codecs = ['urdna2015', 'blake2b-64']; 207 | const hlUrl = await hl.encode({ 208 | url, 209 | codecs, 210 | 'content-type': 'application/ld+json' 211 | }); 212 | 213 | // print out the hashlink 214 | console.log(hlUrl); 215 | ``` 216 | 217 | ### Decoding a Hashlink 218 | 219 | You can decode a hashlink by simply calling decode (**coming soon**): 220 | 221 | ```js 222 | const hl = require('hashlink'); 223 | 224 | const url = 'hl:zm9YZpCjPLPJ4Epc:z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'; 225 | const hlData = hl.decode(url); 226 | 227 | // print out the decoded hashlink information (an object) 228 | console.log(hlData); 229 | ``` 230 | 231 | ### Verifying a Hashlink 232 | 233 | You can verify the integrity of a hashlink: 234 | 235 | ```js 236 | const hl = require('hashlink'); 237 | 238 | const hashlink = 'hl:zm9YZpCjPLPJ4Epc:z3TSgXTuaHxY2tsArhUreJ4ixgw9NW7DYuQ9QTPQyLHy'; 239 | const data = '...'; 240 | const valid = await hl.verify({hashlink, data}); 241 | 242 | // print out whether or not the hashlink is valid 243 | console.log(valid); 244 | ``` 245 | 246 | ### Advanced Verification 247 | 248 | In some cases, you need to be able to canonize the contents of the hashlink 249 | in order to verify it: 250 | 251 | ```js 252 | const {Hashlink} = require('hashlink'); 253 | const {Urdna2016} = require('hashlink-jsonld'); 254 | 255 | // setup hl library to use RDF Dataset Canonicalization codec 256 | const hl = new Hashlink(); 257 | hl.use(new Urdna2015()); 258 | 259 | // encode a hashlink using canonicalized data published at a URL 260 | const hlUrl = 'hl:zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e:' + 261 | 'zuh8iaLobXC8g9tfma1CSTtYBakXeSTkHrYA5hmD4F7dCLw8XYwZ1GWyJ3zwF'; 262 | const valid = await hl.verify({hashlink: hlUrl}); 263 | 264 | // print out whether or not the hashlink is valid 265 | console.log(valid); 266 | ``` 267 | ### Extending the Hashlink Library 268 | 269 | The Hashlink library is built to support arbitrary transformations of 270 | input data using codecs (encoder/decoders). 271 | 272 | The Hashlink library has an internal default instance of a Hashlink 273 | class that is provided as a convenience so that for most use cases, the 274 | defaults work just fine. 275 | 276 | ```js 277 | const hl = require('hashlink'); 278 | const hlUrl = await hl.encode({url: 'https://example.com/hw.txt'}); 279 | ``` 280 | 281 | In some cases, however, a developer will need to extend the default 282 | transformations, like when input needs to be canonicalized before it is 283 | hashed. 284 | 285 | ```js 286 | const {Hashlink} = require('hashlink'); 287 | const jsonld = require('jsonld'); 288 | 289 | // setup URDNA2015 codec that encodes 290 | class Urdna2015 { 291 | constructor() { 292 | this.algorithm = 'urdna2015'; 293 | } 294 | 295 | async encode(input) { 296 | const inputJsonld = JSON.parse(new TextDecoder().decode(input)); 297 | return await jsonld.canonize( 298 | inputJsonld, {format: 'application/n-quads'}); 299 | } 300 | } 301 | 302 | // setup hl library to use RDF Dataset Canonicalization 303 | const hl = new Hashlink(); 304 | hl.use(new Urdna2015()); 305 | 306 | // encode a hashlink using canonicalized data published at a URL 307 | const url = 'https://example.com/credential.jsonld'; 308 | 309 | // encode the input data using urdna2015 canonicalization algorithm and 310 | // then hash using blake2b with a 64-bit output 311 | const codecs = ['urdna2015', 'blake2b-64']; 312 | const hlUrl = await hl.encode({ 313 | url, 314 | codecs, 315 | 'content-type': 'application/ld+json' 316 | }); 317 | 318 | // print out the hashlink 319 | console.log(hlUrl); 320 | ``` 321 | 322 | Note the use of the `Hashlink` class above as well as the `use()` API. Using 323 | this API, any arbitrary number of transforms may be applied to the input 324 | data before the final hashlink value is produced. 325 | 326 | ## Testing 327 | 328 | To run Mocha tests: 329 | 330 | ``` 331 | npm run mocha 332 | ``` 333 | 334 | To run the VC Test Suite: 335 | 336 | ``` 337 | npm run fetch-hl-test-suite 338 | npm test 339 | ``` 340 | 341 | ## Contribute 342 | 343 | See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)! 344 | 345 | PRs accepted. 346 | 347 | Small note: If editing the Readme, please conform to the 348 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 349 | 350 | ## Commercial Support 351 | 352 | Commercial support for this library is available upon request from 353 | Digital Bazaar: support@digitalbazaar.com 354 | 355 | ## License 356 | 357 | [New BSD License (3-clause)](LICENSE) © Digital Bazaar 358 | 359 | ## Acknowledgements 360 | 361 | The authors of this package thank Daniel Levett 362 | ([dlevs](https://github.com/dlevs/)) for contributing the `hashlink` npm package 363 | name for use with this project. 364 | --------------------------------------------------------------------------------