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
JavaScript
;
/*
* 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;