From 1c09e78b4f4f881abd68ee6f8245b5894e19bded Mon Sep 17 00:00:00 2001 From: Dimasik Kolezhniuk Date: Fri, 13 Oct 2023 21:27:18 +0200 Subject: [PATCH] Add primitives base58 and sha256 --- package-lock.json | 4 +- package.json | 2 +- src/base58.ts | 62 +++++++++++ src/index.ts | 2 + src/sha256.ts | 252 +++++++++++++++++++++++++++++++++++++++++++ tests/base58.test.ts | 22 ++++ tests/sha256.test.ts | 50 +++++++++ 7 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 src/base58.ts create mode 100644 src/sha256.ts create mode 100644 tests/base58.test.ts create mode 100644 tests/sha256.test.ts diff --git a/package-lock.json b/package-lock.json index 9c2dac8..ebfebed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@iden3/js-crypto", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@iden3/js-crypto", - "version": "1.0.1", + "version": "1.0.2", "license": "AGPL-3.0", "devDependencies": { "@iden3/eslint-config": "https://github.com/iden3/eslint-config", diff --git a/package.json b/package.json index cd0490c..f3a687e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iden3/js-crypto", - "version": "1.0.1", + "version": "1.0.2", "description": "Crypto primitives for iden3", "source": "./src/index.ts", "exports": { diff --git a/src/base58.ts b/src/base58.ts new file mode 100644 index 0000000..42329ee --- /dev/null +++ b/src/base58.ts @@ -0,0 +1,62 @@ +// Original js implementation https://gist.github.com/diafygi/90a3e80ca1c2793220e5/ + +const base58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +export const base58FromBytes = ( + input: Uint8Array //Uint8Array raw byte input +): string => { + const d = []; //the array for storing the stream of base58 digits + let s = ''; //the result string variable that will be returned + let j = 0; //the iterator variable for the base58 digit array (d) + let c = 0; //the carry amount variable that is used to overflow from the current base58 digit to the next base58 digit + let n: number; //a temporary placeholder variable for the current base58 digit + for (let i = 0; i < input.length; i++) { + //loop through each byte in the input stream + j = 0; //reset the base58 digit iterator + c = input[i]; //set the initial carry amount equal to the current byte amount + s += c || s.length ^ i ? '' : '1'; //prepend the result string with a "1" (0 in base58) if the byte stream is zero and non-zero bytes haven't been seen yet (to ensure correct decode length) + while (j in d || c) { + //start looping through the digits until there are no more digits and no carry amount + n = d[j]; //set the placeholder for the current base58 digit + n = n ? n * 256 + c : c; //shift the current base58 one byte and add the carry amount (or just add the carry amount if this is a new digit) + c = (n / 58) | 0; //find the new carry amount (floored integer of current digit divided by 58) + d[j] = n % 58; //reset the current base58 digit to the remainder (the carry amount will pass on the overflow) + j++; //iterate to the next base58 digit + } + } + while (j--) + //since the base58 digits are backwards, loop through them in reverse order + s += base58[d[j]]; //lookup the character associated with each base58 digit + return s; //return the final base58 string +}; + +export const base58ToBytes = ( + str: string //Base58 encoded string input +): Uint8Array => { + const d = []; //the array for storing the stream of decoded bytes + const b = []; //the result byte array that will be returned + let j = 0; //the iterator variable for the byte array (d) + let c = 0; //the carry amount variable that is used to overflow from the current byte to the next byte + let n = 0; //a temporary placeholder variable for the current byte + for (let i = 0; i < str.length; i++) { + //loop through each base58 character in the input string + j = 0; //reset the byte iterator + c = base58.indexOf(str[i]); //set the initial carry amount equal to the current base58 digit + if (c < 0) + //see if the base58 digit lookup is invalid (-1) + throw new Error(`Can't convert base58 string ${str} to bytes`); + c || b.length ^ i ? i : b.push(0); //prepend the result array with a zero if the base58 digit is zero and non-zero characters haven't been seen yet (to ensure correct decode length) + while (j in d || c) { + //start looping through the bytes until there are no more bytes and no carry amount + n = d[j]; //set the placeholder for the current byte + n = n ? n * 58 + c : c; //shift the current byte 58 units and add the carry amount (or just add the carry amount if this is a new byte) + c = n >> 8; //find the new carry amount (1-byte shift of current byte value) + d[j] = n % 256; //reset the current byte to the remainder (the carry amount will pass on the overflow) + j++; //iterate to the next byte + } + } + while (j--) + //since the byte array is backwards, loop through it in reverse order + b.push(d[j]); //append each byte to the result + return new Uint8Array(b); //return the final byte array in Uint8Array format +}; diff --git a/src/index.ts b/src/index.ts index 94fe010..b416ff1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,6 @@ export * from './babyjub'; export * from './poseidon'; export * from './hex'; export * from './blake'; +export * from './base58'; +export * from './sha256'; export { utils as ffUtils } from './ff'; diff --git a/src/sha256.ts b/src/sha256.ts new file mode 100644 index 0000000..f79e5b3 --- /dev/null +++ b/src/sha256.ts @@ -0,0 +1,252 @@ +// SHA-256 for JavaScript. +// Original implementation https://github.com/dchest/fast-sha256-js/blob/master/src/sha256.ts +const digestLength = 32; +const blockSize = 64; + +// SHA-256 constants +const K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +]); + +function hashBlocks(w: Int32Array, v: Int32Array, p: Uint8Array, pos: number, len: number): number { + let a: number, + b: number, + c: number, + d: number, + e: number, + f: number, + g: number, + h: number, + u: number, + i: number, + j: number, + t1: number, + t2: number; + while (len >= 64) { + a = v[0]; + b = v[1]; + c = v[2]; + d = v[3]; + e = v[4]; + f = v[5]; + g = v[6]; + h = v[7]; + + for (i = 0; i < 16; i++) { + j = pos + i * 4; + w[i] = + ((p[j] & 0xff) << 24) | + ((p[j + 1] & 0xff) << 16) | + ((p[j + 2] & 0xff) << 8) | + (p[j + 3] & 0xff); + } + + for (i = 16; i < 64; i++) { + u = w[i - 2]; + t1 = ((u >>> 17) | (u << (32 - 17))) ^ ((u >>> 19) | (u << (32 - 19))) ^ (u >>> 10); + + u = w[i - 15]; + t2 = ((u >>> 7) | (u << (32 - 7))) ^ ((u >>> 18) | (u << (32 - 18))) ^ (u >>> 3); + + w[i] = ((t1 + w[i - 7]) | 0) + ((t2 + w[i - 16]) | 0); + } + + for (i = 0; i < 64; i++) { + t1 = + ((((((e >>> 6) | (e << (32 - 6))) ^ + ((e >>> 11) | (e << (32 - 11))) ^ + ((e >>> 25) | (e << (32 - 25)))) + + ((e & f) ^ (~e & g))) | + 0) + + ((h + ((K[i] + w[i]) | 0)) | 0)) | + 0; + + t2 = + ((((a >>> 2) | (a << (32 - 2))) ^ + ((a >>> 13) | (a << (32 - 13))) ^ + ((a >>> 22) | (a << (32 - 22)))) + + ((a & b) ^ (a & c) ^ (b & c))) | + 0; + + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + + v[0] += a; + v[1] += b; + v[2] += c; + v[3] += d; + v[4] += e; + v[5] += f; + v[6] += g; + v[7] += h; + + pos += 64; + len -= 64; + } + return pos; +} + +// Hash implements SHA256 hash algorithm. +export class Hash { + digestLength: number = digestLength; + blockSize: number = blockSize; + + // Note: Int32Array is used instead of Uint32Array for performance reasons. + private state: Int32Array = new Int32Array(8); // hash state + private temp: Int32Array = new Int32Array(64); // temporary state + private buffer: Uint8Array = new Uint8Array(128); // buffer for data to hash + private bufferLength = 0; // number of bytes in buffer + private bytesHashed = 0; // number of total bytes hashed + + finished = false; // indicates whether the hash was finalized + + constructor() { + this.reset(); + } + + // Resets hash state making it possible + // to re-use this instance to hash other data. + reset(): this { + this.state[0] = 0x6a09e667; + this.state[1] = 0xbb67ae85; + this.state[2] = 0x3c6ef372; + this.state[3] = 0xa54ff53a; + this.state[4] = 0x510e527f; + this.state[5] = 0x9b05688c; + this.state[6] = 0x1f83d9ab; + this.state[7] = 0x5be0cd19; + this.bufferLength = 0; + this.bytesHashed = 0; + this.finished = false; + return this; + } + + // Cleans internal buffers and re-initializes hash state. + clean() { + for (let i = 0; i < this.buffer.length; i++) { + this.buffer[i] = 0; + } + for (let i = 0; i < this.temp.length; i++) { + this.temp[i] = 0; + } + this.reset(); + } + + // Updates hash state with the given data. + // + // Optionally, length of the data can be specified to hash + // fewer bytes than data.length. + // + // Throws error when trying to update already finalized hash: + // instance must be reset to use it again. + update(data: Uint8Array, dataLength: number = data.length): this { + if (this.finished) { + throw new Error("SHA256: can't update because hash was finished."); + } + let dataPos = 0; + this.bytesHashed += dataLength; + if (this.bufferLength > 0) { + while (this.bufferLength < 64 && dataLength > 0) { + this.buffer[this.bufferLength++] = data[dataPos++]; + dataLength--; + } + if (this.bufferLength === 64) { + hashBlocks(this.temp, this.state, this.buffer, 0, 64); + this.bufferLength = 0; + } + } + if (dataLength >= 64) { + dataPos = hashBlocks(this.temp, this.state, data, dataPos, dataLength); + dataLength %= 64; + } + while (dataLength > 0) { + this.buffer[this.bufferLength++] = data[dataPos++]; + dataLength--; + } + return this; + } + + // Finalizes hash state and puts hash into out. + // + // If hash was already finalized, puts the same value. + finish(out: Uint8Array): this { + if (!this.finished) { + const bytesHashed = this.bytesHashed; + const left = this.bufferLength; + const bitLenHi = (bytesHashed / 0x20000000) | 0; + const bitLenLo = bytesHashed << 3; + const padLength = bytesHashed % 64 < 56 ? 64 : 128; + + this.buffer[left] = 0x80; + for (let i = left + 1; i < padLength - 8; i++) { + this.buffer[i] = 0; + } + this.buffer[padLength - 8] = (bitLenHi >>> 24) & 0xff; + this.buffer[padLength - 7] = (bitLenHi >>> 16) & 0xff; + this.buffer[padLength - 6] = (bitLenHi >>> 8) & 0xff; + this.buffer[padLength - 5] = (bitLenHi >>> 0) & 0xff; + this.buffer[padLength - 4] = (bitLenLo >>> 24) & 0xff; + this.buffer[padLength - 3] = (bitLenLo >>> 16) & 0xff; + this.buffer[padLength - 2] = (bitLenLo >>> 8) & 0xff; + this.buffer[padLength - 1] = (bitLenLo >>> 0) & 0xff; + + hashBlocks(this.temp, this.state, this.buffer, 0, padLength); + + this.finished = true; + } + + for (let i = 0; i < 8; i++) { + out[i * 4 + 0] = (this.state[i] >>> 24) & 0xff; + out[i * 4 + 1] = (this.state[i] >>> 16) & 0xff; + out[i * 4 + 2] = (this.state[i] >>> 8) & 0xff; + out[i * 4 + 3] = (this.state[i] >>> 0) & 0xff; + } + + return this; + } + + // Returns the final hash digest. + digest(): Uint8Array { + const out = new Uint8Array(this.digestLength); + this.finish(out); + return out; + } + + // Internal function for use in HMAC for optimization. + _saveState(out: Uint32Array) { + for (let i = 0; i < this.state.length; i++) { + out[i] = this.state[i]; + } + } + + // Internal function for use in HMAC for optimization. + _restoreState(from: Uint32Array, bytesHashed: number) { + for (let i = 0; i < this.state.length; i++) { + this.state[i] = from[i]; + } + this.bytesHashed = bytesHashed; + this.finished = false; + this.bufferLength = 0; + } +} + +export function sha256(data: Uint8Array): Uint8Array { + const h = new Hash().update(data); + const digest = h.digest(); + h.clean(); + return digest; +} diff --git a/tests/base58.test.ts b/tests/base58.test.ts new file mode 100644 index 0000000..9145231 --- /dev/null +++ b/tests/base58.test.ts @@ -0,0 +1,22 @@ +import { Hex } from '../src'; +import { base58ToBytes, base58FromBytes } from '../src/base58'; +describe('base58', () => { + it('base58 to binary', () => { + const inp = '6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV'; + const expected = '02c0ded2bc1f1305fb0faac5e6c03ee3a1924234985427b6167ca569d13df435cfeb05f9d2'; + const inHex = Hex.encodeString(base58ToBytes(inp)); + expect(inHex).toEqual(expected); + expect(() => base58ToBytes('0L')).toThrowError(`Can't convert base58 string 0L to bytes`); + }); + + it('base58 to binary', () => { + expect( + base58FromBytes( + Hex.decodeString( + '02c0ded2bc1f1305fb0faac5e6c03ee3a1924234985427b6167ca569d13df435cfeb05f9d2' + ) + ) + ).toEqual('6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV'); + expect(base58FromBytes(Uint8Array.from([0, 0, 0, 4]))).toBe('1115'); + }); +}); diff --git a/tests/sha256.test.ts b/tests/sha256.test.ts new file mode 100644 index 0000000..a546a3c --- /dev/null +++ b/tests/sha256.test.ts @@ -0,0 +1,50 @@ +import { sha256 } from '../src/sha256'; +import { Hex } from '../src/hex'; + +describe('SHA-256 Hashing', () => { + const encoder = new TextEncoder(); + it('should correctly hash a string', () => { + const suite = [ + { + input: 'Hello, World!', + expectedHash: 'dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f' + }, + { + input: 'London is the capital of Great Britain', + expectedHash: '9d32c323980e796968b64f912ca6fa52e7cf3ea5431f8b28a7671b5ec1fdc53b' + }, + { + input: 'The quick brown fox jumps over the lazy dog', + expectedHash: 'd7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592' + }, + { + input: 'Tets message', + expectedHash: 'dffb29c8f6c14f16bfedd2a81a10986e5828ab6613a7428b1549d5f890e03418' + } + ]; + for (const { input, expectedHash } of suite) { + const result = Hex.encodeString(sha256(encoder.encode(input))); + expect(result).toEqual(expectedHash); + } + }); + + it('should return the same hash for the same input', () => { + const input = 'Hello, World!'; + const expectedHash = 'dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f'; + + const result1 = Hex.encodeString(sha256(encoder.encode(input))); + const result2 = Hex.encodeString(sha256(encoder.encode(input))); + + expect(result1).toEqual(expectedHash); + expect(result2).toEqual(expectedHash); + }); + + it('should hash different inputs to different values', () => { + const input1 = 'Hello, World!'; + const input2 = 'Hello, Goodbay!'; + const hash1 = sha256(encoder.encode(input1)); + const hash2 = sha256(encoder.encode(input2)); + + expect(hash1).not.toEqual(hash2); + }); +});