├── .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 | [](https://npm.im/hashlink)
4 | [](https://github.com/digitalbazaar/hashlink/actions?query=workflow%3A%22Node.js+CI%22)
5 | [](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 |
--------------------------------------------------------------------------------