From 5e6cdba3e7d9a4b94e21ddcd5f2b5138fb440ee8 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 14 Jan 2023 02:26:26 -0500 Subject: [PATCH] *Add dist --- dist/blake2b.d.ts | 3 + dist/blake2b.js | 185 +++++++++++++++ dist/checkObjProps.d.ts | 3 + dist/checkObjProps.js | 36 +++ dist/encoding.d.ts | 10 + dist/encoding.js | 122 ++++++++++ dist/err.d.ts | 2 + dist/err.js | 14 ++ dist/errTracker.d.ts | 13 ++ dist/errTracker.js | 73 ++++++ dist/index.d.ts | 13 ++ dist/index.js | 12 + dist/objAsString.d.ts | 2 + dist/objAsString.js | 60 +++++ dist/parse.d.ts | 3 + dist/parse.js | 342 +++++++++++++++++++++++++++ dist/sha512.d.ts | 4 + dist/sha512.js | 416 +++++++++++++++++++++++++++++++++ dist/skylinkBitfield.d.ts | 4 + dist/skylinkBitfield.js | 164 +++++++++++++ dist/skylinkValidate.d.ts | 5 + dist/skylinkValidate.js | 115 +++++++++ dist/stringifyJSON.d.ts | 3 + dist/stringifyJSON.js | 22 ++ dist/types.d.ts | 19 ++ dist/types.js | 1 + dist/validateObjPropTypes.d.ts | 3 + dist/validateObjPropTypes.js | 98 ++++++++ 28 files changed, 1747 insertions(+) create mode 100644 dist/blake2b.d.ts create mode 100644 dist/blake2b.js create mode 100644 dist/checkObjProps.d.ts create mode 100644 dist/checkObjProps.js create mode 100644 dist/encoding.d.ts create mode 100644 dist/encoding.js create mode 100644 dist/err.d.ts create mode 100644 dist/err.js create mode 100644 dist/errTracker.d.ts create mode 100644 dist/errTracker.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/objAsString.d.ts create mode 100644 dist/objAsString.js create mode 100644 dist/parse.d.ts create mode 100644 dist/parse.js create mode 100644 dist/sha512.d.ts create mode 100644 dist/sha512.js create mode 100644 dist/skylinkBitfield.d.ts create mode 100644 dist/skylinkBitfield.js create mode 100644 dist/skylinkValidate.d.ts create mode 100644 dist/skylinkValidate.js create mode 100644 dist/stringifyJSON.d.ts create mode 100644 dist/stringifyJSON.js create mode 100644 dist/types.d.ts create mode 100644 dist/types.js create mode 100644 dist/validateObjPropTypes.d.ts create mode 100644 dist/validateObjPropTypes.js diff --git a/dist/blake2b.d.ts b/dist/blake2b.d.ts new file mode 100644 index 0000000..674d0f0 --- /dev/null +++ b/dist/blake2b.d.ts @@ -0,0 +1,3 @@ +declare const BLAKE2B_HASH_SIZE = 32; +declare function blake2b(input: Uint8Array): Uint8Array; +export { BLAKE2B_HASH_SIZE, blake2b }; diff --git a/dist/blake2b.js b/dist/blake2b.js new file mode 100644 index 0000000..b6d1bd0 --- /dev/null +++ b/dist/blake2b.js @@ -0,0 +1,185 @@ +// Blake2B, adapted from the reference implementation in RFC7693 +// Ported to Javascript by DC - https://github.com/dcposch +// Then ported to typescript by https://github.com/DavidVorick +// 64-bit unsigned addition +// Sets v[a,a+1] += v[b,b+1] +// v should be a Uint32Array +function ADD64AA(v, a, b) { + const o0 = v[a] + v[b]; + let o1 = v[a + 1] + v[b + 1]; + if (o0 >= 0x100000000) { + o1++; + } + v[a] = o0; + v[a + 1] = o1; +} +// 64-bit unsigned addition +// Sets v[a,a+1] += b +// b0 is the low 32 bits of b, b1 represents the high 32 bits +function ADD64AC(v, a, b0, b1) { + let o0 = v[a] + b0; + if (b0 < 0) { + o0 += 0x100000000; + } + let o1 = v[a + 1] + b1; + if (o0 >= 0x100000000) { + o1++; + } + v[a] = o0; + v[a + 1] = o1; +} +// Little-endian byte access +function B2B_GET32(arr, i) { + return arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24); +} +// G Mixing function +// The ROTRs are inlined for speed +function B2B_G(a, b, c, d, ix, iy, m, v) { + const x0 = m[ix]; + const x1 = m[ix + 1]; + const y0 = m[iy]; + const y1 = m[iy + 1]; + ADD64AA(v, a, b); // v[a,a+1] += v[b,b+1] ... in JS we must store a uint64 as two uint32s + ADD64AC(v, a, x0, x1); // v[a, a+1] += x ... x0 is the low 32 bits of x, x1 is the high 32 bits + // v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated to the right by 32 bits + let xor0 = v[d] ^ v[a]; + let xor1 = v[d + 1] ^ v[a + 1]; + v[d] = xor1; + v[d + 1] = xor0; + ADD64AA(v, c, d); + // v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 24 bits + xor0 = v[b] ^ v[c]; + xor1 = v[b + 1] ^ v[c + 1]; + v[b] = (xor0 >>> 24) ^ (xor1 << 8); + v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8); + ADD64AA(v, a, b); + ADD64AC(v, a, y0, y1); + // v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated right by 16 bits + xor0 = v[d] ^ v[a]; + xor1 = v[d + 1] ^ v[a + 1]; + v[d] = (xor0 >>> 16) ^ (xor1 << 16); + v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16); + ADD64AA(v, c, d); + // v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 63 bits + xor0 = v[b] ^ v[c]; + xor1 = v[b + 1] ^ v[c + 1]; + v[b] = (xor1 >>> 31) ^ (xor0 << 1); + v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1); +} +// Initialization Vector +const BLAKE2B_IV32 = new Uint32Array([ + 0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372, + 0x5f1d36f1, 0xa54ff53a, 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c, + 0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19, +]); +const SIGMA8 = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, + 6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, + 9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4, + 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, + 15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7, + 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, + 13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, + 1, 12, 0, 2, 11, 7, 5, 3, +]; +// These are offsets into a uint64 buffer. +// Multiply them all by 2 to make them offsets into a uint32 buffer, +// because this is Javascript and we don't have uint64s +const SIGMA82 = new Uint8Array(SIGMA8.map(function (x) { + return x * 2; +})); +// Compression function. 'last' flag indicates last block. +// Note we're representing 16 uint64s as 32 uint32s +function blake2bCompress(ctx, last) { + const v = new Uint32Array(32); + const m = new Uint32Array(32); + let i = 0; + // init work variables + for (i = 0; i < 16; i++) { + v[i] = ctx.h[i]; + v[i + 16] = BLAKE2B_IV32[i]; + } + // low 64 bits of offset + v[24] = v[24] ^ ctx.t; + v[25] = v[25] ^ (ctx.t / 0x100000000); + // high 64 bits not supported, offset may not be higher than 2**53-1 + // last block flag set ? + if (last) { + v[28] = ~v[28]; + v[29] = ~v[29]; + } + // get little-endian words + for (i = 0; i < 32; i++) { + m[i] = B2B_GET32(ctx.b, 4 * i); + } + // twelve rounds of mixing + for (i = 0; i < 12; i++) { + B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1], m, v); + B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3], m, v); + B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5], m, v); + B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7], m, v); + B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9], m, v); + B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11], m, v); + B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13], m, v); + B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15], m, v); + } + for (i = 0; i < 16; i++) { + ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16]; + } +} +// Creates a BLAKE2b hashing context +// Requires an output length between 1 and 64 bytes +function blake2bInit() { + // state, 'param block' + const ctx = { + b: new Uint8Array(128), + h: new Uint32Array(16), + t: 0, + c: 0, + outlen: 32, // output length in bytes + }; + // initialize hash state + for (let i = 0; i < 16; i++) { + ctx.h[i] = BLAKE2B_IV32[i]; + } + ctx.h[0] ^= 0x01010000 ^ 32; + return ctx; +} +// Updates a BLAKE2b streaming hash +// Requires hash context and Uint8Array (byte array) +function blake2bUpdate(ctx, input) { + for (let i = 0; i < input.length; i++) { + if (ctx.c === 128) { + // buffer full ? + ctx.t += ctx.c; // add counters + blake2bCompress(ctx, false); // compress (not last) + ctx.c = 0; // counter to zero + } + ctx.b[ctx.c++] = input[i]; + } +} +// Completes a BLAKE2b streaming hash +// Returns a Uint8Array containing the message digest +function blake2bFinal(ctx) { + ctx.t += ctx.c; // mark last block offset + while (ctx.c < 128) { + // fill up with zeros + ctx.b[ctx.c++] = 0; + } + blake2bCompress(ctx, true); // final block flag = 1 + // little endian convert and store + const out = new Uint8Array(ctx.outlen); + for (let i = 0; i < ctx.outlen; i++) { + out[i] = ctx.h[i >> 2] >> (8 * (i & 3)); + } + return out; +} +const BLAKE2B_HASH_SIZE = 32; +// Computes the blake2b hash of the input. Returns 32 bytes. +function blake2b(input) { + const ctx = blake2bInit(); + blake2bUpdate(ctx, input); + return blake2bFinal(ctx); +} +export { BLAKE2B_HASH_SIZE, blake2b }; diff --git a/dist/checkObjProps.d.ts b/dist/checkObjProps.d.ts new file mode 100644 index 0000000..de4ec11 --- /dev/null +++ b/dist/checkObjProps.d.ts @@ -0,0 +1,3 @@ +import { Err } from "./types.js"; +declare function checkObjProps(obj: any, checks: [string, string][]): Err; +export { checkObjProps }; diff --git a/dist/checkObjProps.js b/dist/checkObjProps.js new file mode 100644 index 0000000..d33ed5e --- /dev/null +++ b/dist/checkObjProps.js @@ -0,0 +1,36 @@ +// checkObjProps take an untrusted object and a list of typechecks to perform +// and will check that the object adheres to the typechecks. If a type is +// missing or has the wrong type, an error will be returned. This is intended +// to be used to check untrusted objects after they get decoded from JSON. This +// is particularly useful when receiving objects from untrusted entities over +// the network or over postMessage. +// +// Below is an example object, followed by the call that you would make to +// checkObj to verify the object. +// +// const expectedObj = { +// aNum: 35, +// aStr: "hi", +// aBig: 10n, +// }; +// +// const err = checkObjProps(expectedObj, [ +// ["aNum", "number"], +// ["aStr", "string"], +// ["aBig", "bigint"], +// ]); +// +// Over time, we intend to extend this function to support more types than just +// the default types supported by javascript. For example, we intend to add +// special cases for arrays, and for cryptographic objects. +function checkObjProps(obj, checks) { + for (let i = 0; i < checks.length; i++) { + const check = checks[i]; + const type = typeof obj[check[0]]; + if (type !== check[1]) { + return "check failed, expecting " + check[1] + " got " + type; + } + } + return null; +} +export { checkObjProps }; diff --git a/dist/encoding.d.ts b/dist/encoding.d.ts new file mode 100644 index 0000000..cf9f564 --- /dev/null +++ b/dist/encoding.d.ts @@ -0,0 +1,10 @@ +import { Err } from "./types.js"; +declare function b64ToBuf(b64: string): [Uint8Array, Err]; +declare function bufToHex(buf: Uint8Array): string; +declare function bufToB64(buf: Uint8Array): string; +declare function bufToStr(buf: ArrayBuffer): [string, Err]; +declare function decodeU64(u8: Uint8Array): [bigint, Err]; +declare function encodePrefixedBytes(bytes: Uint8Array): [Uint8Array, Err]; +declare function encodeU64(num: bigint): [Uint8Array, Err]; +declare function hexToBuf(hex: string): [Uint8Array, Err]; +export { b64ToBuf, bufToHex, bufToB64, bufToStr, decodeU64, encodePrefixedBytes, encodeU64, hexToBuf, }; diff --git a/dist/encoding.js b/dist/encoding.js new file mode 100644 index 0000000..5d6dc57 --- /dev/null +++ b/dist/encoding.js @@ -0,0 +1,122 @@ +import { addContextToErr } from "./err.js"; +const MAX_UINT_64 = 18446744073709551615n; +// b64ToBuf will take an untrusted base64 string and convert it into a +// Uin8Array, returning an error if the input is not valid base64. +const b64regex = /^[0-9a-zA-Z-_/+=]*$/; +function b64ToBuf(b64) { + // Check that the final string is valid base64. + if (!b64regex.test(b64)) { + return [new Uint8Array(0), "provided string is not valid base64"]; + } + // Swap any '-' characters for '+', and swap any '_' characters for '/' + // for use in the atob function. + b64 = b64.replaceAll("-", "+").replaceAll("_", "/"); + // Perform the conversion. + const binStr = atob(b64); + const len = binStr.length; + const buf = new Uint8Array(len); + for (let i = 0; i < len; i++) { + buf[i] = binStr.charCodeAt(i); + } + return [buf, null]; +} +// bufToHex takes a Uint8Array as input and returns the hex encoding of those +// bytes as a string. +function bufToHex(buf) { + return [...buf].map((x) => x.toString(16).padStart(2, "0")).join(""); +} +// bufToB64 will convert a Uint8Array to a base64 string with URL encoding and +// no padding characters. +function bufToB64(buf) { + const b64Str = btoa(String.fromCharCode(...buf)); + return b64Str.replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); +} +// bufToStr takes an ArrayBuffer as input and returns a text string. bufToStr +// will check for invalid characters. +function bufToStr(buf) { + try { + const text = new TextDecoder("utf-8", { fatal: true }).decode(buf); + return [text, null]; + } + catch (err) { + return [ + "", + addContextToErr(err.toString(), "unable to decode ArrayBuffer to string"), + ]; + } +} +// decodeU64 is the opposite of encodeU64, it takes a uint64 encoded as 8 bytes +// and decodes them into a BigInt. +function decodeU64(u8) { + // Check the input. + if (u8.length !== 8) { + return [0n, "input should be 8 bytes"]; + } + // Process the input. + let num = 0n; + for (let i = u8.length - 1; i >= 0; i--) { + num *= 256n; + num += BigInt(u8[i]); + } + return [num, null]; +} +// encodePrefixedBytes takes a Uint8Array as input and returns a Uint8Array +// that has the length prefixed as an 8 byte prefix. +function encodePrefixedBytes(bytes) { + const [encodedLen, err] = encodeU64(BigInt(bytes.length)); + if (err !== null) { + return [ + new Uint8Array(0), + addContextToErr(err, "unable to encode array length"), + ]; + } + const prefixedArray = new Uint8Array(8 + bytes.length); + prefixedArray.set(encodedLen, 0); + prefixedArray.set(bytes, 8); + return [prefixedArray, null]; +} +// encodeU64 will encode a bigint in the range of a uint64 to an 8 byte +// Uint8Array. +function encodeU64(num) { + // Check the bounds on the bigint. + if (num < 0) { + return [new Uint8Array(0), "expected a positive integer"]; + } + if (num > MAX_UINT_64) { + return [new Uint8Array(0), "expected a number no larger than a uint64"]; + } + // Encode the bigint into a Uint8Array. + const encoded = new Uint8Array(8); + for (let i = 0; i < encoded.length; i++) { + const byte = Number(num & 0xffn); + encoded[i] = byte; + num = num >> 8n; + } + return [encoded, null]; +} +// hexToBuf takes an untrusted string as input, verifies that the string is +// valid hex, and then converts the string to a Uint8Array. +const allHex = /^[0-9a-f]+$/i; +function hexToBuf(hex) { + // The rest of the code doesn't handle zero length input well, so we handle + // that separately. It's not an error, we just return an empty array. + if (hex.length === 0) { + return [new Uint8Array(0), null]; + } + // Check that the length makes sense. + if (hex.length % 2 !== 0) { + return [new Uint8Array(0), "input has incorrect length"]; + } + // Check that all of the characters are legal. + if (!allHex.test(hex)) { + return [new Uint8Array(0), "input has invalid character"]; + } + // Create the buffer and fill it. + const matches = hex.match(/.{2}/g); + if (matches === null) { + return [new Uint8Array(0), "input is incomplete"]; + } + const u8 = new Uint8Array(matches.map((byte) => parseInt(byte, 16))); + return [u8, null]; +} +export { b64ToBuf, bufToHex, bufToB64, bufToStr, decodeU64, encodePrefixedBytes, encodeU64, hexToBuf, }; diff --git a/dist/err.d.ts b/dist/err.d.ts new file mode 100644 index 0000000..c663342 --- /dev/null +++ b/dist/err.d.ts @@ -0,0 +1,2 @@ +declare function addContextToErr(err: any, context: string): string; +export { addContextToErr }; diff --git a/dist/err.js b/dist/err.js new file mode 100644 index 0000000..fc945fb --- /dev/null +++ b/dist/err.js @@ -0,0 +1,14 @@ +import { objAsString } from "./objAsString.js"; +// addContextToErr is a helper function that standardizes the formatting of +// adding context to an error. +// +// NOTE: To protect against accidental situations where an Error type or some +// other type is provided instead of a string, we wrap both of the inputs with +// objAsString before returning them. This prevents runtime failures. +function addContextToErr(err, context) { + if (err === null || err === undefined) { + err = "[no error provided]"; + } + return objAsString(context) + ": " + objAsString(err); +} +export { addContextToErr }; diff --git a/dist/errTracker.d.ts b/dist/errTracker.d.ts new file mode 100644 index 0000000..8dfa3a8 --- /dev/null +++ b/dist/errTracker.d.ts @@ -0,0 +1,13 @@ +import { Err } from "./types.js"; +interface HistoricErr { + err: Err; + date: Date; +} +interface ErrTracker { + recentErrs: HistoricErr[]; + oldErrs: HistoricErr[]; + addErr: (err: Err) => void; + viewErrs: () => HistoricErr[]; +} +declare function newErrTracker(): ErrTracker; +export { ErrTracker, HistoricErr, newErrTracker }; diff --git a/dist/errTracker.js b/dist/errTracker.js new file mode 100644 index 0000000..5448e3f --- /dev/null +++ b/dist/errTracker.js @@ -0,0 +1,73 @@ +// errTracker.ts defines an 'ErrTracker' type which keeps track of historical +// errors. When the number of errors gets too large, it randomly starts pruning +// errors. It always keeps 250 of the most recent errors, and then keeps up to +// 500 historic errors, where the first few errors after runtime are always +// kept, and the ones in the middle are increasingly likely to be omitted from +// the history. +// MAX_ERRORS defines the maximum number of errors that will be held in the +// HistoricErr object. +const MAX_ERRORS = 1000; +// newErrTracker returns an ErrTracker object that is ready to have errors +// added to it. +function newErrTracker() { + const et = { + recentErrs: [], + oldErrs: [], + addErr: function (err) { + addHistoricErr(et, err); + }, + viewErrs: function () { + return viewErrs(et); + }, + }; + return et; +} +// addHistoricErr is a function that will add an error to a set of historic +// errors. It uses randomness to prune errors once the error object is too +// large. +function addHistoricErr(et, err) { + // Add this error to the set of most recent errors. + et.recentErrs.push({ + err, + date: new Date(), + }); + // Determine whether some of the most recent errors need to be moved into + // logTermErrs. If the length of the mostRecentErrs is not at least half of + // the MAX_ERRORS, we don't need to do anything. + if (et.recentErrs.length < MAX_ERRORS / 2) { + return; + } + // Iterate through the recentErrs. For the first half of the recentErrs, we + // will use randomness to either toss them or move them to oldErrs. The + // second half of the recentErrs will be kept as the new recentErrs array. + const newRecentErrs = []; + for (let i = 0; i < et.recentErrs.length; i++) { + // If we are in the second half of the array, add the element to + // newRecentErrs. + if (i > et.recentErrs.length / 2) { + newRecentErrs.push(et.recentErrs[i]); + continue; + } + // We are in the first half of the array, use a random number to add the + // error oldErrs probabilistically. + const rand = Math.random(); + const target = et.oldErrs.length / (MAX_ERRORS / 2); + if (rand > target || et.oldErrs.length < 25) { + et.oldErrs.push(et.recentErrs[i]); + } + } + et.recentErrs = newRecentErrs; +} +// viewErrs returns the list of errors that have been retained by the +// HistoricErr object. +function viewErrs(et) { + const finalErrs = []; + for (let i = 0; i < et.oldErrs.length; i++) { + finalErrs.push(et.oldErrs[i]); + } + for (let i = 0; i < et.recentErrs.length; i++) { + finalErrs.push(et.recentErrs[i]); + } + return finalErrs; +} +export { newErrTracker }; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..6ace363 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,13 @@ +export { BLAKE2B_HASH_SIZE, blake2b } from "./blake2b.js"; +export { checkObjProps } from "./checkObjProps.js"; +export { b64ToBuf, bufToB64, bufToHex, bufToStr, decodeU64, encodePrefixedBytes, encodeU64, hexToBuf, } from "./encoding.js"; +export { addContextToErr } from "./err.js"; +export { ErrTracker, HistoricErr, newErrTracker } from "./errTracker.js"; +export { objAsString } from "./objAsString.js"; +export { parseJSON } from "./parse.js"; +export { SHA512_HASH_SIZE, sha512 } from "./sha512.js"; +export { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, skylinkV1Bitfield, } from "./skylinkBitfield.js"; +export { validateSkyfileMetadata, validateSkyfilePath, validateSkylink, } from "./skylinkValidate.js"; +export { jsonStringify } from "./stringifyJSON.js"; +export { DataFn, Err, ErrFn, ErrTuple, KernelAuthStatus, RequestOverrideResponse, SkynetPortal, } from "./types.js"; +export { validateObjPropTypes } from "./validateObjPropTypes.js"; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..b5f5d06 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,12 @@ +export { BLAKE2B_HASH_SIZE, blake2b } from "./blake2b.js"; +export { checkObjProps } from "./checkObjProps.js"; +export { b64ToBuf, bufToB64, bufToHex, bufToStr, decodeU64, encodePrefixedBytes, encodeU64, hexToBuf, } from "./encoding.js"; +export { addContextToErr } from "./err.js"; +export { newErrTracker } from "./errTracker.js"; +export { objAsString } from "./objAsString.js"; +export { parseJSON } from "./parse.js"; +export { SHA512_HASH_SIZE, sha512 } from "./sha512.js"; +export { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, skylinkV1Bitfield, } from "./skylinkBitfield.js"; +export { validateSkyfileMetadata, validateSkyfilePath, validateSkylink, } from "./skylinkValidate.js"; +export { jsonStringify } from "./stringifyJSON.js"; +export { validateObjPropTypes } from "./validateObjPropTypes.js"; diff --git a/dist/objAsString.d.ts b/dist/objAsString.d.ts new file mode 100644 index 0000000..50bf241 --- /dev/null +++ b/dist/objAsString.d.ts @@ -0,0 +1,2 @@ +declare function objAsString(obj: any): string; +export { objAsString }; diff --git a/dist/objAsString.js b/dist/objAsString.js new file mode 100644 index 0000000..3519e01 --- /dev/null +++ b/dist/objAsString.js @@ -0,0 +1,60 @@ +// objAsString will try to return the provided object as a string. If the +// object is already a string, it will be returned without modification. If the +// object is an 'Error', the message of the error will be returned. If the object +// has a toString method, the toString method will be called and the result +// will be returned. If the object is null or undefined, a special string will +// be returned indicating that the undefined/null object cannot be converted to +// a string. In all other cases, JSON.stringify is used. If JSON.stringify +// throws an exception, a message "[could not provide object as string]" will +// be returned. +// +// NOTE: objAsString is intended to produce human readable output. It is lossy, +// and it is not intended to be used for serialization. +function objAsString(obj) { + // Check for undefined input. + if (obj === undefined) { + return "[cannot convert undefined to string]"; + } + if (obj === null) { + return "[cannot convert null to string]"; + } + // Parse the error into a string. + if (typeof obj === "string") { + return obj; + } + // Check if the object is an error, and return the message of the error if + // so. + if (obj instanceof Error) { + return obj.message; + } + // Check if the object has a 'toString' method defined on it. To ensure + // that we don't crash or throw, check that the toString is a function, and + // also that the return value of toString is a string. + if (Object.prototype.hasOwnProperty.call(obj, "toString")) { + if (typeof obj.toString === "function") { + const str = obj.toString(); + if (typeof str === "string") { + return str; + } + } + } + // If the object does not have a custom toString, attempt to perform a + // JSON.stringify. We use a lot of bigints in libskynet, and calling + // JSON.stringify on an object with a bigint will cause a throw, so we add + // some custom handling to allow bigint objects to still be encoded. + try { + return JSON.stringify(obj, (_, v) => { + if (typeof v === "bigint") { + return v.toString(); + } + return v; + }); + } + catch (err) { + if (err !== undefined && typeof err.message === "string") { + return `[stringify failed]: ${err.message}`; + } + return "[stringify failed]"; + } +} +export { objAsString }; diff --git a/dist/parse.d.ts b/dist/parse.d.ts new file mode 100644 index 0000000..9fc4876 --- /dev/null +++ b/dist/parse.d.ts @@ -0,0 +1,3 @@ +import { Err } from "./types.js"; +declare function parseJSON(json: string): [any, Err]; +export { parseJSON }; diff --git a/dist/parse.js b/dist/parse.js new file mode 100644 index 0000000..8a66f31 --- /dev/null +++ b/dist/parse.js @@ -0,0 +1,342 @@ +// @ts-nocheck +import { objAsString } from "./objAsString.js"; +// json_parse extracted from the json-bigint npm library +// regexpxs extracted from +// (c) BSD-3-Clause +// https://github.com/fastify/secure-json-parse/graphs/contributors and https://github.com/hapijs/bourne/graphs/contributors +const suspectProtoRx = /(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/; +const suspectConstructorRx = /(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/; +let json_parse = function (options) { + "use strict"; + // This is a function that can parse a JSON text, producing a JavaScript + // data structure. It is a simple, recursive descent parser. It does not use + // eval or regular expressions, so it can be used as a model for implementing + // a JSON parser in other languages. + // We are defining the function inside of another function to avoid creating + // global variables. + // Default options one can override by passing options to the parse() + let _options = { + strict: false, + storeAsString: false, + alwaysParseAsBig: false, + protoAction: "error", + constructorAction: "error", + }; + // If there are options, then use them to override the default _options + if (options !== undefined && options !== null) { + if (options.strict === true) { + _options.strict = true; + } + if (options.storeAsString === true) { + _options.storeAsString = true; + } + _options.alwaysParseAsBig = + options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false; + if (typeof options.constructorAction !== "undefined") { + if (options.constructorAction === "error" || + options.constructorAction === "ignore" || + options.constructorAction === "preserve") { + _options.constructorAction = options.constructorAction; + } + else { + throw new Error(`Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed ${options.constructorAction}`); + } + } + if (typeof options.protoAction !== "undefined") { + if (options.protoAction === "error" || + options.protoAction === "ignore" || + options.protoAction === "preserve") { + _options.protoAction = options.protoAction; + } + else { + throw new Error(`Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed ${options.protoAction}`); + } + } + } + let at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + "\\": "\\", + "/": "/", + b: "\b", + f: "\f", + n: "\n", + r: "\r", + t: "\t", + }, text, error = function (m) { + // Call error when something is wrong. + throw { + name: "SyntaxError", + message: m, + at: at, + text: text, + }; + }, next = function (c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error("Expected '" + c + "' instead of '" + ch + "'"); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, number = function () { + // Parse a number value. + let number, string = ""; + if (ch === "-") { + string = "-"; + next("-"); + } + while (ch >= "0" && ch <= "9") { + string += ch; + next(); + } + if (ch === ".") { + string += "."; + while (next() && ch >= "0" && ch <= "9") { + string += ch; + } + } + if (ch === "e" || ch === "E") { + string += ch; + next(); + if (ch === "-" || ch === "+") { + string += ch; + next(); + } + while (ch >= "0" && ch <= "9") { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error("Bad number"); + } + else { + if (Number.isSafeInteger(number)) + return !_options.alwaysParseAsBig ? number : BigInt(number); + // Number with fractional part should be treated as number(double) including big integers in scientific notation, i.e 1.79e+308 + else + return _options.storeAsString + ? string + : /[.eE]/.test(string) + ? number + : BigInt(string); + } + }, string = function () { + // Parse a string value. + let hex, i, string = "", uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + let startAt = at; + while (next()) { + if (ch === '"') { + if (at - 1 > startAt) + string += text.substring(startAt, at - 1); + next(); + return string; + } + if (ch === "\\") { + if (at - 1 > startAt) + string += text.substring(startAt, at - 1); + next(); + if (ch === "u") { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } + else if (typeof escapee[ch] === "string") { + string += escapee[ch]; + } + else { + break; + } + startAt = at; + } + } + } + error("Bad string"); + }, white = function () { + // Skip whitespace. + while (ch && ch <= " ") { + next(); + } + }, word = function () { + // true, false, or null. + switch (ch) { + case "t": + next("t"); + next("r"); + next("u"); + next("e"); + return true; + case "f": + next("f"); + next("a"); + next("l"); + next("s"); + next("e"); + return false; + case "n": + next("n"); + next("u"); + next("l"); + next("l"); + return null; + } + error("Unexpected '" + ch + "'"); + }, value, // Place holder for the value function. + array = function () { + // Parse an array value. + let array = []; + if (ch === "[") { + next("["); + white(); + if (ch === "]") { + next("]"); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === "]") { + next("]"); + return array; + } + next(","); + white(); + } + } + error("Bad array"); + }, object = function () { + // Parse an object value. + let key, object = Object.create(null); + if (ch === "{") { + next("{"); + white(); + if (ch === "}") { + next("}"); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(":"); + if (_options.strict === true && + Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + if (suspectProtoRx.test(key) === true) { + if (_options.protoAction === "error") { + error("Object contains forbidden prototype property"); + } + else if (_options.protoAction === "ignore") { + value(); + } + else { + object[key] = value(); + } + } + else if (suspectConstructorRx.test(key) === true) { + if (_options.constructorAction === "error") { + error("Object contains forbidden constructor property"); + } + else if (_options.constructorAction === "ignore") { + value(); + } + else { + object[key] = value(); + } + } + else { + object[key] = value(); + } + white(); + if (ch === "}") { + next("}"); + return object; + } + next(","); + white(); + } + } + error("Bad object"); + }; + value = function () { + // Parse a JSON value. It could be an object, an array, a string, a number, + // or a word. + white(); + switch (ch) { + case "{": + return object(); + case "[": + return array(); + case '"': + return string(); + case "-": + return number(); + default: + return ch >= "0" && ch <= "9" ? number() : word(); + } + }; + // Return the json_parse function. It will have access to all of the above + // functions and variables. + return function (source, reviver) { + let result; + text = source + ""; + at = 0; + ch = " "; + result = value(); + white(); + if (ch) { + error("Syntax error"); + } + // If there is a reviver function, we recursively walk the new structure, + // passing each name/value pair to the reviver function for possible + // transformation, starting with a temporary root object that holds the result + // in an empty key. If there is not a reviver function, we simply return the + // result. + return typeof reviver === "function" + ? (function walk(holder, key) { + let v, value = holder[key]; + if (value && typeof value === "object") { + Object.keys(value).forEach(function (k) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } + else { + delete value[k]; + } + }); + } + return reviver.call(holder, key, value); + })({ "": result }, "") + : result; + }; +}; +// parseJSON is a wrapper for JSONbig.parse that returns an error rather than +// throwing an error. JSONbig is an alternative to JSON.parse that decodes +// every number as a bigint. This is required when working with the skyd API +// because the skyd API uses 64 bit precision for all of its numbers, and +// therefore cannot be parsed losslessly by javascript. The skyd API is +// cryptographic, therefore full precision is required. +function parseJSON(json) { + try { + let obj = json_parse({ alwaysParseAsBig: true })(json); + return [obj, null]; + } + catch (err) { + return [{}, objAsString(err)]; + } +} +export { parseJSON }; diff --git a/dist/sha512.d.ts b/dist/sha512.d.ts new file mode 100644 index 0000000..a02eaa6 --- /dev/null +++ b/dist/sha512.d.ts @@ -0,0 +1,4 @@ +declare const SHA512_HASH_SIZE = 64; +declare const sha512internal: (out: Uint8Array, m: Uint8Array, n: number) => number; +declare function sha512(m: Uint8Array): Uint8Array; +export { SHA512_HASH_SIZE, sha512, sha512internal }; diff --git a/dist/sha512.js b/dist/sha512.js new file mode 100644 index 0000000..06fac5f --- /dev/null +++ b/dist/sha512.js @@ -0,0 +1,416 @@ +const SHA512_HASH_SIZE = 64; +const K = [ + 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, 0xb5c0fbcf, 0xec4d3b2f, + 0xe9b5dba5, 0x8189dbbc, 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, + 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, 0xd807aa98, 0xa3030242, + 0x12835b01, 0x45706fbe, 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, 0x9bdc06a7, 0x25c71235, + 0xc19bf174, 0xcf692694, 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, + 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, 0x2de92c6f, 0x592b0275, + 0x4a7484aa, 0x6ea6e483, 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, 0xb00327c8, 0x98fb213f, + 0xbf597fc7, 0xbeef0ee4, 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, + 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, 0x27b70a85, 0x46d22ffc, + 0x2e1b2138, 0x5c26c926, 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, 0x81c2c92e, 0x47edaee6, + 0x92722c85, 0x1482353b, 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, + 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, 0xd192e819, 0xd6ef5218, + 0xd6990624, 0x5565a910, 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, 0x2748774c, 0xdf8eeb99, + 0x34b0bcb5, 0xe19b48a8, 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, + 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, 0x748f82ee, 0x5defb2fc, + 0x78a5636f, 0x43172f60, 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, 0xbef9a3f7, 0xb2c67915, + 0xc67178f2, 0xe372532b, 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, + 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, 0x06f067aa, 0x72176fba, + 0x0a637dc5, 0xa2c898a6, 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, 0x3c9ebe0a, 0x15c9bebc, + 0x431d67c4, 0x9c100d4c, 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, + 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817, +]; +function ts64(x, i, h, l) { + x[i] = (h >> 24) & 0xff; + x[i + 1] = (h >> 16) & 0xff; + x[i + 2] = (h >> 8) & 0xff; + x[i + 3] = h & 0xff; + x[i + 4] = (l >> 24) & 0xff; + x[i + 5] = (l >> 16) & 0xff; + x[i + 6] = (l >> 8) & 0xff; + x[i + 7] = l & 0xff; +} +function crypto_hashblocks_hl(hh, hl, m, n) { + const wh = new Int32Array(16), wl = new Int32Array(16); + let bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, th, tl, i, j, h, l, a, b, c, d; + let ah0 = hh[0], ah1 = hh[1], ah2 = hh[2], ah3 = hh[3], ah4 = hh[4], ah5 = hh[5], ah6 = hh[6], ah7 = hh[7], al0 = hl[0], al1 = hl[1], al2 = hl[2], al3 = hl[3], al4 = hl[4], al5 = hl[5], al6 = hl[6], al7 = hl[7]; + let pos = 0; + while (n >= 128) { + for (i = 0; i < 16; i++) { + j = 8 * i + pos; + wh[i] = (m[j + 0] << 24) | (m[j + 1] << 16) | (m[j + 2] << 8) | m[j + 3]; + wl[i] = (m[j + 4] << 24) | (m[j + 5] << 16) | (m[j + 6] << 8) | m[j + 7]; + } + for (i = 0; i < 80; i++) { + bh0 = ah0; + bh1 = ah1; + bh2 = ah2; + bh3 = ah3; + bh4 = ah4; + bh5 = ah5; + bh6 = ah6; + bh7 = ah7; + bl0 = al0; + bl1 = al1; + bl2 = al2; + bl3 = al3; + bl4 = al4; + bl5 = al5; + bl6 = al6; + bl7 = al7; + // add + h = ah7; + l = al7; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + // Sigma1 + h = + ((ah4 >>> 14) | (al4 << (32 - 14))) ^ + ((ah4 >>> 18) | (al4 << (32 - 18))) ^ + ((al4 >>> (41 - 32)) | (ah4 << (32 - (41 - 32)))); + l = + ((al4 >>> 14) | (ah4 << (32 - 14))) ^ + ((al4 >>> 18) | (ah4 << (32 - 18))) ^ + ((ah4 >>> (41 - 32)) | (al4 << (32 - (41 - 32)))); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // Ch + h = (ah4 & ah5) ^ (~ah4 & ah6); + l = (al4 & al5) ^ (~al4 & al6); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // K + h = K[i * 2]; + l = K[i * 2 + 1]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // w + h = wh[i % 16]; + l = wl[i % 16]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + th = (c & 0xffff) | (d << 16); + tl = (a & 0xffff) | (b << 16); + // add + h = th; + l = tl; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + // Sigma0 + h = + ((ah0 >>> 28) | (al0 << (32 - 28))) ^ + ((al0 >>> (34 - 32)) | (ah0 << (32 - (34 - 32)))) ^ + ((al0 >>> (39 - 32)) | (ah0 << (32 - (39 - 32)))); + l = + ((al0 >>> 28) | (ah0 << (32 - 28))) ^ + ((ah0 >>> (34 - 32)) | (al0 << (32 - (34 - 32)))) ^ + ((ah0 >>> (39 - 32)) | (al0 << (32 - (39 - 32)))); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // Maj + h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2); + l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + bh7 = (c & 0xffff) | (d << 16); + bl7 = (a & 0xffff) | (b << 16); + // add + h = bh3; + l = bl3; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = th; + l = tl; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + bh3 = (c & 0xffff) | (d << 16); + bl3 = (a & 0xffff) | (b << 16); + ah1 = bh0; + ah2 = bh1; + ah3 = bh2; + ah4 = bh3; + ah5 = bh4; + ah6 = bh5; + ah7 = bh6; + ah0 = bh7; + al1 = bl0; + al2 = bl1; + al3 = bl2; + al4 = bl3; + al5 = bl4; + al6 = bl5; + al7 = bl6; + al0 = bl7; + if (i % 16 === 15) { + for (j = 0; j < 16; j++) { + // add + h = wh[j]; + l = wl[j]; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = wh[(j + 9) % 16]; + l = wl[(j + 9) % 16]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // sigma0 + th = wh[(j + 1) % 16]; + tl = wl[(j + 1) % 16]; + h = + ((th >>> 1) | (tl << (32 - 1))) ^ + ((th >>> 8) | (tl << (32 - 8))) ^ + (th >>> 7); + l = + ((tl >>> 1) | (th << (32 - 1))) ^ + ((tl >>> 8) | (th << (32 - 8))) ^ + ((tl >>> 7) | (th << (32 - 7))); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + // sigma1 + th = wh[(j + 14) % 16]; + tl = wl[(j + 14) % 16]; + h = + ((th >>> 19) | (tl << (32 - 19))) ^ + ((tl >>> (61 - 32)) | (th << (32 - (61 - 32)))) ^ + (th >>> 6); + l = + ((tl >>> 19) | (th << (32 - 19))) ^ + ((th >>> (61 - 32)) | (tl << (32 - (61 - 32)))) ^ + ((tl >>> 6) | (th << (32 - 6))); + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + wh[j] = (c & 0xffff) | (d << 16); + wl[j] = (a & 0xffff) | (b << 16); + } + } + } + // add + h = ah0; + l = al0; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[0]; + l = hl[0]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[0] = ah0 = (c & 0xffff) | (d << 16); + hl[0] = al0 = (a & 0xffff) | (b << 16); + h = ah1; + l = al1; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[1]; + l = hl[1]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[1] = ah1 = (c & 0xffff) | (d << 16); + hl[1] = al1 = (a & 0xffff) | (b << 16); + h = ah2; + l = al2; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[2]; + l = hl[2]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[2] = ah2 = (c & 0xffff) | (d << 16); + hl[2] = al2 = (a & 0xffff) | (b << 16); + h = ah3; + l = al3; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[3]; + l = hl[3]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[3] = ah3 = (c & 0xffff) | (d << 16); + hl[3] = al3 = (a & 0xffff) | (b << 16); + h = ah4; + l = al4; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[4]; + l = hl[4]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[4] = ah4 = (c & 0xffff) | (d << 16); + hl[4] = al4 = (a & 0xffff) | (b << 16); + h = ah5; + l = al5; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[5]; + l = hl[5]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[5] = ah5 = (c & 0xffff) | (d << 16); + hl[5] = al5 = (a & 0xffff) | (b << 16); + h = ah6; + l = al6; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[6]; + l = hl[6]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[6] = ah6 = (c & 0xffff) | (d << 16); + hl[6] = al6 = (a & 0xffff) | (b << 16); + h = ah7; + l = al7; + a = l & 0xffff; + b = l >>> 16; + c = h & 0xffff; + d = h >>> 16; + h = hh[7]; + l = hl[7]; + a += l & 0xffff; + b += l >>> 16; + c += h & 0xffff; + d += h >>> 16; + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + hh[7] = ah7 = (c & 0xffff) | (d << 16); + hl[7] = al7 = (a & 0xffff) | (b << 16); + pos += 128; + n -= 128; + } + return n; +} +const sha512internal = function (out, m, n) { + const hh = new Int32Array(8), hl = new Int32Array(8), x = new Uint8Array(256), b = n; + let i; + hh[0] = 0x6a09e667; + hh[1] = 0xbb67ae85; + hh[2] = 0x3c6ef372; + hh[3] = 0xa54ff53a; + hh[4] = 0x510e527f; + hh[5] = 0x9b05688c; + hh[6] = 0x1f83d9ab; + hh[7] = 0x5be0cd19; + hl[0] = 0xf3bcc908; + hl[1] = 0x84caa73b; + hl[2] = 0xfe94f82b; + hl[3] = 0x5f1d36f1; + hl[4] = 0xade682d1; + hl[5] = 0x2b3e6c1f; + hl[6] = 0xfb41bd6b; + hl[7] = 0x137e2179; + crypto_hashblocks_hl(hh, hl, m, n); + n %= 128; + for (i = 0; i < n; i++) + x[i] = m[b - n + i]; + x[n] = 128; + n = 256 - 128 * (n < 112 ? 1 : 0); + x[n - 9] = 0; + ts64(x, n - 8, (b / 0x20000000) | 0, b << 3); + crypto_hashblocks_hl(hh, hl, x, n); + for (i = 0; i < 8; i++) + ts64(out, 8 * i, hh[i], hl[i]); + return 0; +}; +// sha512 is the standard sha512 cryptographic hash function. This is the +// default choice for Skynet operations, though many of the Sia protocol +// standards use blake2b instead, so you will see both. +function sha512(m) { + const out = new Uint8Array(SHA512_HASH_SIZE); + sha512internal(out, m, m.length); + return out; +} +export { SHA512_HASH_SIZE, sha512, sha512internal }; diff --git a/dist/skylinkBitfield.d.ts b/dist/skylinkBitfield.d.ts new file mode 100644 index 0000000..0a36cd6 --- /dev/null +++ b/dist/skylinkBitfield.d.ts @@ -0,0 +1,4 @@ +declare const SKYLINK_U8_V1_V2_LENGTH = 34; +declare function parseSkylinkBitfield(skylinkU8: Uint8Array): [bigint, bigint, bigint, string | null]; +declare function skylinkV1Bitfield(dataSizeBI: bigint): [Uint8Array, string | null]; +export { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, skylinkV1Bitfield }; diff --git a/dist/skylinkBitfield.js b/dist/skylinkBitfield.js new file mode 100644 index 0000000..12997f0 --- /dev/null +++ b/dist/skylinkBitfield.js @@ -0,0 +1,164 @@ +// skylinkBitfield.ts defines a bunch of operations for working with skylink +// bitfields. This code is ported from the Sia codebase. You can see that code +// and all of the comments at: +// https://gitlab.com/SkynetLabs/skyd/-/blob/master/skymodules/skylink.go +// SKYLINK_U8_V1_V2_LENGTH defines the length of a skylink that is V1 or V2 +// when it is encoded as a Uint8Array. +const SKYLINK_U8_V1_V2_LENGTH = 34; +// SECTOR_SIZE is the size of a sector +const SECTOR_SIZE = 1 << 22; +// parseSkylinkBitfield parses a skylink bitfield and returns the corresponding +// version, offset, and fetchSize. +function parseSkylinkBitfield(skylinkU8) { + // Validate the input. + if (skylinkU8.length !== SKYLINK_U8_V1_V2_LENGTH) { + return [0n, 0n, 0n, "provided skylink has incorrect length"]; + } + // Extract the bitfield. + let bitfield = new DataView(skylinkU8.buffer).getUint16(0, true); + // Extract the version. We add '1' so that an empty version field maps to + // version 1, and a version field set to '1' maps to version 2. + const version = (bitfield & 3) + 1; + // Only versions 1 and 2 are recognized. + if (version !== 1 && version !== 2) { + return [0n, 0n, 0n, "provided skylink has unrecognized version"]; + } + // If the skylink is set to version 2, we only recognize the link if the rest + // of the bits in the bitfield are empty. This is the definition of a v2 + // skylink, other skylink versions (such as version 1) use more of the + // bitfield. + if (version === 2) { + if ((bitfield & 3) !== bitfield) { + return [0n, 0n, 0n, "provided skylink has unrecognized version"]; + } + return [BigInt(version), 0n, 0n, null]; + } + // Verify that the mode is valid, then fetch the mode. + bitfield = bitfield >> 2; + if ((bitfield & 255) === 255) { + return [0n, 0n, 0n, "provided skylink has an unrecognized version"]; + } + let mode = 0; + for (let i = 0; i < 8; i++) { + if ((bitfield & 1) === 0) { + bitfield = bitfield >> 1; + break; + } + bitfield = bitfield >> 1; + mode++; + } + // If the mode is greater than 7, this is not a valid v1 skylink. + if (mode > 7) { + return [0n, 0n, 0n, "provided skylink has an invalid v1 bitfield"]; + } + // Determine the offset and fetchSize increment. + const offsetIncrement = 4096 << mode; + let fetchSizeIncrement = 4096; + let fetchSizeStart = 0; + if (mode > 0) { + fetchSizeIncrement = fetchSizeIncrement << (mode - 1); + fetchSizeStart = (1 << 15) << (mode - 1); + } + // The next three bits decide the fetchSize. + const fetchSizeBits = (bitfield & 7) + 1; // +1 because semantic range is [1,8]. + const fetchSize = fetchSizeBits * fetchSizeIncrement + fetchSizeStart; + bitfield = bitfield >> 3; + // The remaining bits determine the offset. + const offset = bitfield * offsetIncrement; + if (offset + fetchSize > SECTOR_SIZE) { + return [0n, 0n, 0n, "provided skylink has an invalid v1 bitfield"]; + } + // Return what we learned. + return [BigInt(version), BigInt(offset), BigInt(fetchSize), null]; +} +// skylinkV1Bitfield sets the bitfield of a V1 skylink. It assumes the version +// is 1 and the offset is 0. It will determine the appropriate fetchSize from +// the provided dataSize. +function skylinkV1Bitfield(dataSizeBI) { + // Check that the dataSize is not too large. + if (dataSizeBI > SECTOR_SIZE) { + return [new Uint8Array(0), "dataSize must be less than the sector size"]; + } + const dataSize = Number(dataSizeBI); + // Determine the mode for the file. The mode is determined by the + // dataSize. + let mode = 0; + for (let i = 1 << 15; i < dataSize; i *= 2) { + mode += 1; + } + // Determine the download number. + let downloadNumber = 0; + if (mode === 0) { + if (dataSize !== 0) { + downloadNumber = Math.floor((dataSize - 1) / (1 << 12)); + } + } + else { + const step = 1 << (11 + mode); + const target = dataSize - (1 << (14 + mode)); + if (target !== 0) { + downloadNumber = Math.floor((target - 1) / step); + } + } + // Create the Uint8Array and fill it out. The main reason I switch over + // the 7 modes like this is because I wasn't sure how to make a uint16 + // in javascript. If we could treat the uint8array as a uint16 and then + // later convert it over, we could use basic bitshifiting and really + // simplify the code here. + const bitfield = new Uint8Array(2); + if (mode === 7) { + // 0 0 0 X X X 0 1|1 1 1 1 1 1 0 0 + bitfield[0] = downloadNumber; + bitfield[0] *= 4; + bitfield[0] += 1; + bitfield[1] = 4 + 8 + 16 + 32 + 64 + 128; + } + if (mode === 6) { + // 0 0 0 0 X X X 0|1 1 1 1 1 1 0 0 + bitfield[0] = downloadNumber; + bitfield[0] *= 2; + bitfield[1] = 4 + 8 + 16 + 32 + 64 + 128; + } + if (mode === 5) { + // 0 0 0 0 0 X X X|0 1 1 1 1 1 0 0 + bitfield[0] = downloadNumber; + bitfield[1] = 4 + 8 + 16 + 32 + 64; + } + if (mode === 4) { + // 0 0 0 0 0 0 X X|X 0 1 1 1 1 0 0 + bitfield[0] = downloadNumber; + bitfield[0] /= 2; + bitfield[1] = (downloadNumber & 1) * 128; + bitfield[1] += 4 + 8 + 16 + 32; + } + if (mode === 3) { + // 0 0 0 0 0 0 0 X|X X 0 1 1 1 0 0 + bitfield[0] = downloadNumber; + bitfield[0] /= 4; + bitfield[1] = (downloadNumber & 3) * 64; + bitfield[1] += 4 + 8 + 16; + } + if (mode === 2) { + // 0 0 0 0 0 0 0 0|X X X 0 1 1 0 0 + bitfield[0] = 0; + bitfield[1] = downloadNumber * 32; + bitfield[1] += 4 + 8; + } + if (mode === 1) { + // 0 0 0 0 0 0 0 0|0 X X X 0 1 0 0 + bitfield[0] = 0; + bitfield[1] = downloadNumber * 16; + bitfield[1] += 4; + } + if (mode === 0) { + // 0 0 0 0 0 0 0 0|0 0 X X X 0 0 0 + bitfield[0] = 0; + bitfield[1] = downloadNumber * 8; + } + // Swap the byte order. + const zero = bitfield[0]; + bitfield[0] = bitfield[1]; + bitfield[1] = zero; + return [bitfield, null]; +} +export { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, skylinkV1Bitfield }; diff --git a/dist/skylinkValidate.d.ts b/dist/skylinkValidate.d.ts new file mode 100644 index 0000000..ceff77a --- /dev/null +++ b/dist/skylinkValidate.d.ts @@ -0,0 +1,5 @@ +import { Err } from "./types.js"; +declare function validateSkyfilePath(path: string): string | null; +declare function validateSkyfileMetadata(metadata: any): string | null; +declare function validateSkylink(skylink: string | Uint8Array): Err; +export { validateSkyfileMetadata, validateSkyfilePath, validateSkylink }; diff --git a/dist/skylinkValidate.js b/dist/skylinkValidate.js new file mode 100644 index 0000000..ba9399a --- /dev/null +++ b/dist/skylinkValidate.js @@ -0,0 +1,115 @@ +import { addContextToErr } from "./err.js"; +import { b64ToBuf } from "./encoding.js"; +import { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, } from "./skylinkBitfield.js"; +// validateSkyfilePath checks whether the provided path is a valid path for a +// file in a skylink. +function validateSkyfilePath(path) { + if (path === "") { + return "path cannot be blank"; + } + if (path === "..") { + return "path cannot be .."; + } + if (path === ".") { + return "path cannot be ."; + } + if (path.startsWith("/")) { + return "metdata.Filename cannot start with /"; + } + if (path.startsWith("../")) { + return "metdata.Filename cannot start with ../"; + } + if (path.startsWith("./")) { + return "metdata.Filename cannot start with ./"; + } + const pathElems = path.split("/"); + for (let i = 0; i < pathElems.length; i++) { + if (pathElems[i] === ".") { + return "path cannot have a . element"; + } + if (pathElems[i] === "..") { + return "path cannot have a .. element"; + } + if (pathElems[i] === "") { + return "path cannot have an empty element, cannot contain //"; + } + } + return null; +} +// validateSkyfileMetadata checks whether the provided metadata is valid +// metadata for a skyfile. +function validateSkyfileMetadata(metadata) { + // Check that the filename is valid. + if (!("Filename" in metadata)) { + return "metadata.Filename does not exist"; + } + if (typeof metadata.Filename !== "string") { + return "metadata.Filename is not a string"; + } + const errVSP = validateSkyfilePath(metadata.Filename); + if (errVSP !== null) { + return addContextToErr(errVSP, "metadata.Filename does not have a valid path"); + } + // Check that there are no subfiles. + if ("Subfiles" in metadata) { + // TODO: Fill this out using code from + // skymodules.ValidateSkyfileMetadata to support subfiles. + return "cannot upload files that have subfiles"; + } + // Check that the default path rules are being respected. + if ("DisableDefaultPath" in metadata && "DefaultPath" in metadata) { + return "cannot set both a DefaultPath and also DisableDefaultPath"; + } + if ("DefaultPath" in metadata) { + // TODO: Fill this out with code from + // skymodules.validateDefaultPath to support subfiles and + // default paths. + return "cannot set a default path if there are no subfiles"; + } + if ("TryFiles" in metadata) { + if (!metadata.TryFiles.IsArray()) { + return "metadata.TryFiles must be an array"; + } + if (metadata.TryFiles.length === 0) { + return "metadata.TryFiles should not be empty"; + } + if ("DefaultPath" in metadata) { + return "metadata.TryFiles cannot be used alongside DefaultPath"; + } + if ("DisableDefaultPath" in metadata) { + return "metadata.TryFiles cannot be used alongside DisableDefaultPath"; + } + // TODO: finish the TryFiles checking using skymodules.ValidateTryFiles + return "TryFiles is not supported at this time"; + } + if ("ErrorPages" in metadata) { + // TODO: finish using skymodules.ValidateErrorPages + return "ErrorPages is not supported at this time"; + } + return null; +} +// validateSkylink returns null if the provided Uint8Array is a valid skylink. +function validateSkylink(skylink) { + // If the input is a string, convert it to a Uint8Array. + let skylinkU8; + if (typeof skylink === "string") { + const [buf, err] = b64ToBuf(skylink); + if (err !== null) { + return addContextToErr(err, "unable to convert skylink from string"); + } + skylinkU8 = buf; + } + else { + skylinkU8 = skylink; + } + // skylink is now a Uint8 + if (skylinkU8.length !== SKYLINK_U8_V1_V2_LENGTH) { + return `skylinkU8 has an invalid length: ${skylinkU8.length}`; + } + const [, , , errPSB] = parseSkylinkBitfield(skylinkU8); + if (errPSB !== null) { + return addContextToErr(errPSB, "skylink did not decode"); + } + return null; +} +export { validateSkyfileMetadata, validateSkyfilePath, validateSkylink }; diff --git a/dist/stringifyJSON.d.ts b/dist/stringifyJSON.d.ts new file mode 100644 index 0000000..aea158b --- /dev/null +++ b/dist/stringifyJSON.d.ts @@ -0,0 +1,3 @@ +import { Err } from "./types.js"; +declare function jsonStringify(obj: any): [string, Err]; +export { jsonStringify }; diff --git a/dist/stringifyJSON.js b/dist/stringifyJSON.js new file mode 100644 index 0000000..a053f89 --- /dev/null +++ b/dist/stringifyJSON.js @@ -0,0 +1,22 @@ +import { addContextToErr } from "./err.js"; +import { objAsString } from "./objAsString.js"; +// jsonStringify is a replacement for JSON.stringify that returns an error +// rather than throwing. +function jsonStringify(obj) { + try { + const str = JSON.stringify(obj, (_, v) => { + if (typeof v === "bigint") { + return Number(v); + } + return v; + }); + return [str, null]; + } + catch (err) { + return [ + "", + addContextToErr(objAsString(err), "unable to stringify object"), + ]; + } +} +export { jsonStringify }; diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..3d81fc0 --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,19 @@ +type DataFn = (data?: any) => void; +type Err = string | null; +type ErrFn = (errMsg: string) => void; +type ErrTuple = [data: any, err: Err]; +interface KernelAuthStatus { + loginComplete: boolean; + kernelLoaded: "not yet" | "success" | string; + logoutComplete: boolean; +} +interface SkynetPortal { + url: string; + name: string; +} +interface RequestOverrideResponse { + override: boolean; + headers?: any; + body?: Uint8Array; +} +export { DataFn, ErrFn, Err, ErrTuple, KernelAuthStatus, RequestOverrideResponse, SkynetPortal, }; diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/types.js @@ -0,0 +1 @@ +export {}; diff --git a/dist/validateObjPropTypes.d.ts b/dist/validateObjPropTypes.d.ts new file mode 100644 index 0000000..5df3c27 --- /dev/null +++ b/dist/validateObjPropTypes.d.ts @@ -0,0 +1,3 @@ +import { Err } from "./types.js"; +declare function validateObjPropTypes(obj: any, checks: [string, string][]): Err; +export { validateObjPropTypes }; diff --git a/dist/validateObjPropTypes.js b/dist/validateObjPropTypes.js new file mode 100644 index 0000000..77f000c --- /dev/null +++ b/dist/validateObjPropTypes.js @@ -0,0 +1,98 @@ +import { addContextToErr } from "./err.js"; +// validateObjPropTypes takes an object as input, along with a list of checks +// that should performed on the properties of the object. If all of the +// properties are present in the object and adhere to the suggested types, +// `null` is returned. Otherwise a string is returned indicating the first +// property that failed a check. +// +// This function is intended to be used on objects that were decoded from JSON +// after being received by an untrusted source. +// +// validateObjProperties supports all of the basic types, as well as arrays for +// types boolean, number, bigint, and string. In the future, support for more +// types may be added as well. +// +// Below is an example object, followed by the call that you would make to +// checkObj to verify the object. +// +// const expectedObj = { +// aNum: 35, +// aStr: "hi", +// aBig: 10n, +// aArr: [1, 2, 3], +// }; +// +// const err = validateObjPropTypes(expectedObj, [ +// ["aNum", "number"], +// ["aStr", "string"], +// ["aBig", "bigint"], +// ["aArr", "numberArray"], +// ["aUint8Array", "Uint8Array"], +// ]); +function validateObjPropTypes(obj, checks) { + for (let i = 0; i < checks.length; i++) { + const [property, expectedType] = checks[i]; + // Loop through the array cases. + const arrayCases = [ + ["booleanArray", "boolean"], + ["numberArray", "number"], + ["bigintArray", "bigint"], + ["stringArray", "string"], + ]; + let checkPassed = false; + for (let j = 0; j < arrayCases.length; j++) { + // If this is not an array case, ignore it. + const [arrCaseType, arrType] = arrayCases[j]; + if (expectedType !== arrCaseType) { + continue; + } + // Check every element in the array. + const err = validateArrayTypes(obj[property], arrType); + if (err !== null) { + return addContextToErr(err, `check failed for array property '${property}'`); + } + // We found the expected type for this check, we can stop checking the + // rest. + checkPassed = true; + break; + } + // If the type was an array type, we don't need to perform the next check. + if (checkPassed === true) { + continue; + } + // Uint8Array check. + if (expectedType === "Uint8Array") { + if (obj[property] instanceof Uint8Array) { + continue; + } + else { + return `check failed for property '${property};, expecting Uint8Array`; + } + } + // Generic typeof check. + const type = typeof obj[property]; + if (type !== expectedType) { + return `check failed for property '${property}', expecting ${expectedType} got ${type}`; + } + } + return null; +} +// validateArrayTypes takes an array as input and validates that every element +// in the array matches the provided type. +// +// This is a helper function for validateObjPropTypes, the property is provided +// as an input to produce a more coherent error message. +function validateArrayTypes(arr, expectedType) { + // Check that the provided input is actually an array. + if (!Array.isArray(arr)) { + return `not an array`; + } + for (let i = 0; i < arr.length; i++) { + const type = typeof arr[i]; + if (type !== expectedType) { + return `element ${i} is expected to be ${expectedType}, got ${type}`; + } + } + return null; +} +export { validateObjPropTypes };