tidecloak-js
Version:
TideCloak client side JS SDK
703 lines (625 loc) • 20.3 kB
JavaScript
//
// Tide Protocol - Infrastructure for a TRUE Zero-Trust paradigm
// Copyright (C) 2022 Tide Foundation Ltd
//
// This program is free software and is subject to the terms of
// the Tide Community Open Code License as published by the
// Tide Foundation Limited. You may modify it and redistribute
// it in accordance with and subject to the terms of that License.
// This program is distributed WITHOUT WARRANTY of any kind,
// including without any implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
// See the Tide Community Open Code License for more details.
// You should have received a copy of the Tide Community Open
// Code License along with this program.
// If not, see https://tide.org/licenses_tcoc2-0-0-en
//
import { CurrentTime } from "../Tools/Utils.js";
import { Ed25519PublicComponent } from "./Components/Schemes/Ed25519/Ed25519Components.js";
import { etc, Point } from "./Ed25519.js";
import { SHA256_Digest } from "./Hashing/Hash.js";
import { EdDSA } from "./index.js";
import { CreateVRKPackage } from "./TideMemoryObjects.js";
/**
*
* @param {BigInt} value
* @returns
*/
export function writeInt64LittleEndian(value) {
const INT64_MIN = -9223372036854775808n; // -2^63
const INT64_MAX = 9223372036854775807n; // 2^63 - 1
if (value < INT64_MIN || value > INT64_MAX) {
throw new RangeError("Value is out of range for a 64-bit signed integer.");
}
const bytes = new Uint8Array(8);
for (let i = 0; i < 8; i++) {
bytes[i] = Number((value >> BigInt(8 * i)) & 0xFFn);
}
return bytes;
}
/**
*
* @param {Uint8Array} bytes
* @returns
*/
export function readInt64LittleEndian(bytes) {
if (bytes.length !== 8) {
throw new Error("Invalid byte array length. Expected 8 bytes.");
}
let value = 0n;
for (let i = 0; i < 8; i++) {
value |= BigInt(bytes[i]) << BigInt(8 * i);
}
// Interpret the value as a signed 64-bit integer
value = BigInt.asIntN(64, value);
return value;
}
export class AuthorizerPack{
constructor(data){
if(!(data instanceof Uint8Array)) throw Error("Data must be byte array");
this.AuthFlow = StringFromUint8Array(GetValue(data, 0));
this.Authorizer = new GVRK_Pack(GetValue(data, 1));
var c = true;
var i = 2;
this.SignModels = [];
while(c){
try{this.SignModels.push(StringFromUint8Array(GetValue(data, i)));i++;}
catch{c = false;}
}
}
}
export class GVRK_Pack{
constructor(data){
/** @type {Ed25519PublicComponent} */
this.GVRK = Ed25519PublicComponent.DeserializeComponent(GetValue(data, 0));
this.Expiry = readInt64LittleEndian(GetValue(data, 1)); // we do not allow vrks without expiry on enclave for now
}
encode(){
return CreateVRKPackage(this.GVRK, this.Expiry);
}
}
/**
*
* @param {Uint8Array} initialValue
* @param {number} totalLength
* @param {number} version
* @returns
*/
export function CreateTideMemory(initialValue, totalLength, version = 1) {
if (totalLength < initialValue.length + 4) {
throw new Error("Not enough space to allocate requested data. Make sure to request more space in totalLength than length of InitialValue plus 4 bytes for length.");
}
// Total buffer length is 4 (version) + totalLength
const bufferLength = 4 + totalLength;
const buffer = new Uint8Array(bufferLength);
const dataView = new DataView(buffer.buffer);
// Write version at position 0 (4 bytes)
dataView.setInt32(0, version, true); // true for little-endian
let dataLocationIndex = 4;
// Write data length of initialValue at position 4 (4 bytes)
dataView.setInt32(dataLocationIndex, initialValue.length, true);
dataLocationIndex += 4;
// Write initialValue starting from position 8
buffer.set(initialValue, dataLocationIndex);
return buffer;
}
/**
* @param {Uint8Array[]} datas
*/
export function CreateTideMemoryFromArray(datas){
const length = datas.reduce((sum, next) => sum + 4 + next.length, 0);
const mem = CreateTideMemory(datas[0], length);
for(let i = 1; i < datas.length; i++){
WriteValue(mem, i, datas[i]);
}
return mem;
}
/**
*
* @param {Uint8Array} memory
* @param {number} index
* @param {Uint8Array} value
*/
export function WriteValue(memory, index, value) {
if (index < 0) throw new Error("Index cannot be less than 0");
if (index === 0) throw new Error("Use CreateTideMemory to set value at index 0");
if (memory.length < 4 + value.length) throw new Error("Could not write to memory. Memory too small for this value");
const dataView = new DataView(memory.buffer);
let dataLocationIndex = 4; // Start after the version number
// Navigate through existing data segments
for (let i = 0; i < index; i++) {
if (dataLocationIndex + 4 > memory.length) {
throw new RangeError("Index out of range.");
}
// Read data length at current position
const nextDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4;
dataLocationIndex += nextDataLength;
}
// Check if there's enough space to write the value
if (dataLocationIndex + 4 + value.length > memory.length) {
throw new RangeError("Not enough space to write value");
}
// Check if data has already been written to this index
const existingLength = dataView.getInt32(dataLocationIndex, true);
if (existingLength !== 0) {
throw new Error("Data has already been written to this index");
}
// Write data length of value at current position
dataView.setInt32(dataLocationIndex, value.length, true);
dataLocationIndex += 4;
// Write value starting from current position
memory.set(value, dataLocationIndex);
}
/**
*
* @param {Uint8Array} a
* @param {number} index
* @returns
*/
export function GetValue(a, index) {
// 'a' should be an ArrayBuffer or Uint8Array
let buffer;
if (a instanceof Uint8Array) {
buffer = a;
} else {
throw new TypeError("Input must be an Uint8Array.");
}
if (buffer.length < 4) {
throw new Error("Insufficient data to read.");
}
// Create a DataView for reading integers in little-endian format
const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
// Optional: Read the version if needed
// const version = dataView.getInt32(0, true);
let dataLocationIndex = 4;
for (let i = 0; i < index; i++) {
// Check if there's enough data to read the length of the next segment
if (dataLocationIndex + 4 > buffer.length) {
throw new RangeError("Index out of range.");
}
const nextDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4 + nextDataLength;
}
// Check if there's enough data to read the length of the final segment
if (dataLocationIndex + 4 > buffer.length) {
throw new RangeError("Index out of range.");
}
const finalDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4;
// Check if the final data segment is within bounds
if (dataLocationIndex + finalDataLength > buffer.length) {
throw new RangeError("Index out of range.");
}
return buffer.subarray(dataLocationIndex, dataLocationIndex + finalDataLength);
}
export function TryGetValue(a, index, returnObj){
try{
returnObj = GetValue(a, index);
return true;
}catch{
returnObj = null;
return false;
}
}
export function DeserializeNetworkKey(data){
return Point.fromBytes(Hex2Bytes(data.toLowerCase()));
}
/**
*
* @param {Point} p
*/
export async function EdPointToJWK(p){
return JSON.stringify({
"kty": "OKP",
"kid": Bytes2Hex(await SHA256_Digest(p.toRawBytes())),
"alg": "EdDSA",
"crv": "Ed25519",
"x": base64ToBase64Url(p.toBase64())
});
}
/**
*
* @param {string} key
* @param {string} prefix
*/
export function DeserializeTIDE_KEY(key, prefix){
const header = key.substring(0, 8);
const data = base64ToBytes(key.substring(8, key.length));
if(header != "tide" + prefix + "key") throw Error("Unexpected header in deserialization");
if(data.length != 32) throw Error("Unexpected key length in deserialization");
return BigIntFromByteArray(data);
}
export async function GetUID(str){
return Bytes2Hex(await SHA256_Digest(str.toLowerCase()));
}
/**
* @param {BigInt} num
* @returns {Uint8Array}
*/
export function BigIntToByteArray(num) {
return etc.bigIntToBytes(num);
}
/**
* @param {Uint8Array} bytes
* @returns {bigint}
*/
export function BigIntFromByteArray(bytes) {
return etc.bytesToBigInt(bytes);
}
/**
*
* @param {Uint8Array[]} arrays
*/
export function ConcatUint8Arrays(arrays) {
const totalLength = arrays.reduce((sum, next) => next.length + sum, 0);
var newArray = new Uint8Array(totalLength);
var offset = 0;
arrays.forEach(item => {
newArray.set(item, offset);
offset += item.length;
});
return newArray;
}
/**
* @param {Uint8Array} array1
* @param {Uint8Array} array2
*/
export function XOR(array1, array2){
if (array1.length !== array2.length) {
throw new Error('Arrays have different lengths, cannot XOR them.');
}
let result = new Uint8Array(array1.length);
for (let i = 0; i < array1.length; i++) {
result[i] = array1[i] ^ array2[i];
}
return result;
}
/**
*
* @param {Array} array
* @param {number} length
* @param {object} padding
* @returns
*/
export function PadRight(array, length, padding=0) {
while (array.length < length) {
array.push(padding);
}
return array;
}
/**
* @param {string} string
*/
export function StringToUint8Array(string) {
const enc = new TextEncoder();
return enc.encode(string);
}
/**
* @param {Uint8Array} bytes
*/
export function StringFromUint8Array(bytes){
const decoder = new TextDecoder('utf-8');
return decoder.decode(bytes);
}
export class Byte {
constructor() {
/**
* @type {number[]}
*/
this.bits = []; // bits.length should never exceed 8 - hence a byte
}
/**
* Sets a bit at the start of the array (index 0)
* @param {number} bit
*/
setFirstBit(bit){
const b = bit === 0 ? 0 : 1;
this.bits[0] = b;
}
/**
* @returns {Uint8Array}
*/
toUint8Array() {
let number = 0;
for (let i = 0; i < 8; i++) {
number += this.bits[i] * Math.pow(2, 7 - i);
}
const byteArray = new Uint8Array(1); // only 1 byte needed
byteArray[0] = number & 255;
return byteArray;
}
/**
* @param {Uint8Array} uint8Array
* @returns {Byte}
*/
static fromUint8Array(uint8Array) {
let bitArray = new Byte();
for (let i = 7; i >= 0; i--) {
bitArray.bits.push((uint8Array[0] >> i) & 1); // only get first byte of byte array
}
return bitArray;
}
/**
* Maximum number of 255
* @param {number} number
* @returns {Byte}
*/
static fromNumber(number) {
if (number < 0 || number > 255) {
throw Error("Number must be between 0 and 255"); // Adjusted the range check
}
let byte = new Byte();
let binaryString = number.toString(2).padStart(8, '0'); // Pad the string to ensure 8 bits
for (let i = 0; i < 8; i++) {
byte.bits.push(binaryString[i] === '1' ? 1 : 0); // Corrected the condition
}
return byte;
}
}
export function getBytesFromInt16(schemeInt) {
// Create an ArrayBuffer with 2 bytes (16 bits)
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
// Write the 16-bit integer to the buffer, little-endian
view.setInt16(0, schemeInt, true); // 'true' for little-endian, 'false' for big-endian
// Return the bytes as a Uint8Array
return new Uint8Array(buffer);
}
/**
* @param {number} num
* @param {number} len Length of bytes requested
* @returns
*/
export function numberToUint8Array(num, len=-1) {
if (num < 0 || !Number.isInteger(num)) {
throw new Error('Number must be a non-negative integer.');
}
if (num === 0) return new Uint8Array([0]);
let numberOfBytes = Math.ceil(Math.log2(num + 1) / 8);
let byteArray = new Uint8Array(numberOfBytes);
for (let i = 0; i < numberOfBytes; i++) {
byteArray[i] = (num >> (8 * i)) & 0xFF;
}
if(len == -1) return byteArray;
else{
const offset = len - byteArray.length;
if(offset == 0) return byteArray;
const padding = new Uint8Array(offset).fill(0);
return ConcatUint8Arrays([byteArray, padding]);
}
}
/**
* @param {Uint8Array} array
*/
export function Uint8ArrayToNumber(byteArray){
if (!(byteArray instanceof Uint8Array)) {
throw new Error('Input must be a Uint8Array.');
}
let num = 0;
for (let i = byteArray.length - 1; i >= 0; i--) {
num = (num << 8) | byteArray[i];
}
return num;
}
/**
* @param {string} base64
* @returns
*/
export function base64ToBase64Url(base64) {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/**
* @param {string} base64Url
* @returns
*/
export function base64UrlToBase64(base64Url) {
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}
return base64;
}
/**
* @param {number[]} array
* @returns
*/
export function bitArrayToUint8Array(array) {
// Made without ChatGPT (but had some help)
const byteArray = new Uint8Array(Math.ceil(array.length/8));
let bitCount = 0;
for(let i = 0; i < byteArray.length; i++){
const currentByteLength = array.length - bitCount >= 8 ? 8 : array.length - bitCount;
for (let j = 0; j < currentByteLength; j++) {
byteArray[i] |= array[bitCount] << (currentByteLength - 1 - j);
bitCount++;
}
}
return byteArray;
}
/**
* Works for .NET functions.
* @param {number[]} array
* @returns
*/
export function serializeBitArray(bitArray_p) {
// If anyone does a deep dive into this function and notices the bits are reversed, blame .NET's BitArray class.
let bitArray = bitArray_p.slice();
// Ensure the bit array length is a multiple of 8 by padding if necessary
while (bitArray.length % 8 !== 0) {
bitArray.push(0);
}
const byteArray = new Uint8Array(bitArray.length / 8);
for (let byteIndex = 0; byteIndex < byteArray.length; byteIndex++) {
// For each byte, calculate its value from 8 bits, reversing the bit order
let byteValue = 0;
for (let bitPosition = 0; bitPosition < 8; bitPosition++) {
byteValue |= (bitArray[byteIndex * 8 + bitPosition] << bitPosition);
}
byteArray[byteIndex] = byteValue;
}
return byteArray;
}
/**
*
* @param {(0|1)[]} bitarray1
* @param {(0|1)[]} bitarray2
* @returns
*/
export function bitArrayAND(bitarray1, bitarray2){
return bitarray1.map((b, i) => b == 1 && bitarray2[i] == 1 ? 1 : 0)
}
/**
* Works for .NET functions.
* @param {Uint8Array} byteArray
* @returns
*/
export function deserializeBitArray(byteArray) {
const bitArray = [];
byteArray.forEach(byte => {
for (let bitPosition = 0; bitPosition < 8; bitPosition++) {
bitArray.push((byte >> bitPosition) & 1);
}
});
// Remove padding
while (bitArray.length > 0 && bitArray[bitArray.length - 1] === 0) {
bitArray.pop();
}
return bitArray;
}
export function uint8ArrayToBitArray(byteArray) {
// always produces a bitArray of length 20
const bitArray = [];
let count = 0;
for (let i = 0; i < byteArray.length; i++) {
for (let j = 7; j >= 0; j--) {
// Extract the j-th bit of the i-th byte.
const bit = (byteArray[i] >> j) & 1;
if(count < 16 || count > 19) bitArray.push(bit); // exclude intermediate bits, always l=20!
count++;
}
}
return bitArray;
}
/**
* @param {string} string
* @returns {Uint8Array}
*/
export function Hex2Bytes(string) {
const hexRegex = /^0x[0-9A-Fa-f]+$|^[0-9A-Fa-f]+$/;
if (!hexRegex.test(string)) throw Error("Invalid Hex");
const normal = string.length % 2 ? "0" + string : string; // Make even length
const bytes = new Uint8Array(normal.length / 2);
for (let index = 0; index < bytes.length; ++index) {
const c1 = normal.charCodeAt(index * 2);
const c2 = normal.charCodeAt(index * 2 + 1);
const n1 = c1 - (c1 < 58 ? 48 : (c1 < 97 ? 55 : 87));
const n2 = c2 - (c2 < 58 ? 48 : (c2 < 97 ? 55 : 87));
bytes[index] = n1 * 16 + n2;
}
return bytes;
}
/**
* @param {Uint8Array} byteArray
* @returns {string}
*/
export function Bytes2Hex(byteArray) {
const chars = new Uint8Array(byteArray.length * 2);
const alpha = 'a'.charCodeAt(0) - 10;
const digit = '0'.charCodeAt(0);
let p = 0;
for (let i = 0; i < byteArray.length; i++) {
let nibble = byteArray[i] >>> 4;
chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit;
nibble = byteArray[i] & 0xF;
chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit;
}
return String.fromCharCode.apply(null, chars);
}
/**
* Credits to Egor Nepomnyaschih for the below code
* Link: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
*/
const base64abc = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
];
const base64codes = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
];
function getBase64Code(charCode) {
if (charCode >= base64codes.length) {
throw new Error("Unable to parse base64 string.");
}
const code = base64codes[charCode];
if (code === 255) {
throw new Error("Unable to parse base64 string.");
}
return code;
}
/**
* @param {Uint8Array} bytes
* @returns {string}
*/
export function bytesToBase64(bytes) {
let result = '', i, l = bytes.length;
for (i = 2; i < l; i += 3) {
result += base64abc[bytes[i - 2] >> 2];
result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64abc[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
result += base64abc[bytes[i] & 0x3F];
}
if (i === l + 1) { // 1 octet yet to write
result += base64abc[bytes[i - 2] >> 2];
result += base64abc[(bytes[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) { // 2 octets yet to write
result += base64abc[bytes[i - 2] >> 2];
result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
result += base64abc[(bytes[i - 1] & 0x0F) << 2];
result += "=";
}
return result;
}
/**
* @param {string} str
* @returns {Uint8Array}
*/
export function base64ToBytes(str) {
const base64Regex = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/;
if(!base64Regex.test(str)) throw Error("Not valid base64");
if (str.length % 4 !== 0) {
throw new Error("Unable to parse base64 string.");
}
const index = str.indexOf("=");
if (index !== -1 && index < str.length - 2) {
throw new Error("Unable to parse base64 string.");
}
let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0,
n = str.length,
result = new Uint8Array(3 * (n / 4)),
buffer;
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
buffer =
getBase64Code(str.charCodeAt(i)) << 18 |
getBase64Code(str.charCodeAt(i + 1)) << 12 |
getBase64Code(str.charCodeAt(i + 2)) << 6 |
getBase64Code(str.charCodeAt(i + 3));
result[j] = buffer >> 16;
result[j + 1] = (buffer >> 8) & 0xFF;
result[j + 2] = buffer & 0xFF;
}
return result.subarray(0, result.length - missingOctets);
}
// Custom extensions