UNPKG

amazon-qldb-kvs-nodejs

Version:

A helper module, simplifying basic interactions with Amazon Quantum Ledger Database for Node.js through a simple key-value store interface.

164 lines (161 loc) 6.38 kB
"use strict"; /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyDocumentMetadata = exports.parseBlock = exports.joinHashesPairwise = exports.flipRandomBit = void 0; const crypto_1 = require("crypto"); const ion_js_1 = require("ion-js"); const Logging_1 = require("./Logging"); const logger = Logging_1.log.getLogger("qldb-helper"); const Util_1 = require("./Util"); const HASH_LENGTH = 32; const UPPER_BOUND = 8; /** * Build the candidate digest representing the entire ledger from the Proof hashes. * @param proof The Proof object. * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. * @returns The calculated root hash. */ function buildCandidateDigest(proof, leafHash) { const parsedProof = parseProof(proof); // Return root hash return calculateRootHashFromInternalHash(parsedProof, leafHash); } /** * Combine the internal hashes and the leaf hash until only one root hash remains. * @param internalHashes An array of hash values. * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. * @returns The root hash constructed by combining internal hashes. */ function calculateRootHashFromInternalHash(internalHashes, leafHash) { // Return root hash return internalHashes.reduce(joinHashesPairwise, leafHash); } /** * Compare two hash values by converting each Uint8Array byte, which is unsigned by default, * into a signed byte, assuming they are little endian. * @param hash1 The hash value to compare. * @param hash2 The hash value to compare. * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes. */ function compareHashValues(hash1, hash2) { if (hash1.length !== HASH_LENGTH || hash2.length !== HASH_LENGTH) { throw new Error("Invalid hash."); } for (let i = hash1.length - 1; i >= 0; i--) { const difference = (hash1[i] << 24 >> 24) - (hash2[i] << 24 >> 24); if (difference !== 0) { return difference; } } return 0; } /** * Helper method that concatenates two Uint8Array. * @param arrays List of array to concatenate, in the order provided. * @returns The concatenated array. */ function concatenate(...arrays) { let totalLength = 0; for (const arr of arrays) { totalLength += arr.length; } const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } /** * Flip a single random bit in the given hash value. * This method is intended to be used for purpose of demonstrating the QLDB verification features only. * @param original The hash value to alter. * @returns The altered hash with a single random bit changed. */ function flipRandomBit(original) { if (original.length === 0) { throw new Error("Array cannot be empty!"); } const bytePos = Math.floor(Math.random() * original.length); const bitShift = Math.floor(Math.random() * UPPER_BOUND); const alteredHash = original; alteredHash[bytePos] = alteredHash[bytePos] ^ (1 << bitShift); return alteredHash; } exports.flipRandomBit = flipRandomBit; /** * Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values. * @param h1 Byte array containing one of the hashes to compare. * @param h2 Byte array containing one of the hashes to compare. * @returns The concatenated array of hashes. */ function joinHashesPairwise(h1, h2) { if (h1.length === 0) { return h2; } if (h2.length === 0) { return h1; } let concat; if (compareHashValues(h1, h2) < 0) { concat = concatenate(h1, h2); } else { concat = concatenate(h2, h1); } const hash = (0, crypto_1.createHash)('sha256'); hash.update(concat); // Return new digest return hash.digest(); } exports.joinHashesPairwise = joinHashesPairwise; /** * Parse the Block object returned by QLDB and retrieve block hash. * @param valueHolder A structure containing an Ion string value. * @returns The block hash. */ function parseBlock(valueHolder) { const block = ion_js_1.dom.load(valueHolder.IonText); // Return block hash return (0, Util_1.getBlobValue)(block, "blockHash"); } exports.parseBlock = parseBlock; /** * Parse the Proof object returned by QLDB into an iterator. * The Proof object returned by QLDB is a dictionary like the following: * {'IonText': '[{{<hash>}},{{<hash>}}]'} * @param valueHolder A structure containing an Ion string value. * @returns A list of hash values. */ function parseProof(valueHolder) { const proofs = ion_js_1.dom.load(valueHolder.IonText); return proofs.elements().map(proof => proof.uInt8ArrayValue()); } /** * Verify document revision metadata against the provided digest. * @param documentHash The SHA-256 value representing the document revision to be verified. * @param digest A Base64 encoded SHA-256 hash value representing the ledger digest. * @param proof The Proof object retrieved from GetRevision.getRevision. * @returns If the document revision verifies against the ledger digest. */ function verifyDocumentMetadata(documentHash, digest, proof) { const fcnName = "[Verifier verifyDocumentMetadata]"; const candidateDigest = buildCandidateDigest(proof, documentHash); logger.debug(`${fcnName} Ledger digest received: ${digest}`); logger.debug(`${fcnName} Ledger digest derived : ${(0, ion_js_1.toBase64)(candidateDigest)}`); return (digest === (0, ion_js_1.toBase64)(candidateDigest)); } exports.verifyDocumentMetadata = verifyDocumentMetadata;