diff --git a/.vscode/settings.json b/.vscode/settings.json index b6556d8..4900444 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,7 @@ "authly", "bytewise", "cloudly", + "crockford", "cryptly", "Encrypter", "Encrypters", diff --git a/ArrayBuffer.ts b/ArrayBuffer.ts deleted file mode 100644 index eecbf4c..0000000 --- a/ArrayBuffer.ts +++ /dev/null @@ -1,26 +0,0 @@ -function reduce(reducer: (left: number, right: number) => number, data: ArrayBuffer[]): ArrayBuffer { - let result = new Uint8Array(data.reduce((l, d) => (d.byteLength > l ? d.byteLength : l), 0)) - for (const d of data.map(d => new Uint8Array(d))) { - const offset = result.length - d.length - result = result.reduceRight( - (r, value, index) => ((r[index] = index < offset ? value : reducer(value, d[index - offset])), r), - result - ) - } - return result.buffer -} - -export function xor(...data: ArrayBuffer[]): ArrayBuffer { - return reduce((left, right) => left ^ right, data) -} -export function bytewiseAdd(...data: ArrayBuffer[]): ArrayBuffer { - return reduce((left, right) => left + right, data) -} -export function add(...data: ArrayBuffer[]): ArrayBuffer { - let previous = 0 - return reduce((left, right) => (previous = (previous >> 8) + left + right), data) -} -export function combine(...data: ArrayBuffer[]): ArrayBuffer { - let previous = 0 - return reduce((left, right) => (previous = (left << 1) + (previous >> 8) + left + right), data) -} diff --git a/ArrayBuffer.spec.ts b/ArrayBuffer/index.spec.ts similarity index 97% rename from ArrayBuffer.spec.ts rename to ArrayBuffer/index.spec.ts index 5762b1c..dac8fee 100644 --- a/ArrayBuffer.spec.ts +++ b/ArrayBuffer/index.spec.ts @@ -1,4 +1,4 @@ -import { cryptly } from "./index" +import { cryptly } from "../index" describe("Base64", () => { it("xor same length", () => { diff --git a/ArrayBuffer/index.ts b/ArrayBuffer/index.ts new file mode 100644 index 0000000..3d538a6 --- /dev/null +++ b/ArrayBuffer/index.ts @@ -0,0 +1,46 @@ +export namespace ArrayBuffer { + function reduce(reducer: (left: number, right: number) => number, data: ArrayBuffer[]): ArrayBuffer { + let result = new Uint8Array(data.reduce((l, d) => (d.byteLength > l ? d.byteLength : l), 0)) + for (const d of data.map(d => new Uint8Array(d))) { + const offset = result.length - d.length + result = result.reduceRight( + (r, value, index) => ((r[index] = index < offset ? value : reducer(value, d[index - offset])), r), + result + ) + } + return result.buffer + } + + export function xor(...data: ArrayBuffer[]): ArrayBuffer { + return reduce((left, right) => left ^ right, data) + } + export function bytewiseAdd(...data: ArrayBuffer[]): ArrayBuffer { + return reduce((left, right) => left + right, data) + } + export function add(...data: ArrayBuffer[]): ArrayBuffer { + let previous = 0 + return reduce((left, right) => (previous = (previous >> 8) + left + right), data) + } + export function combine(...data: ArrayBuffer[]): ArrayBuffer { + let previous = 0 + return reduce((left, right) => (previous = (left << 1) + (previous >> 8) + left + right), data) + } + export function random< + T extends + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint16Array + | Uint32Array + | Uint8ClampedArray + | Float32Array + | Float64Array + | DataView + | null + >(array: T): T + export function random(length: number): Uint8Array + export function random(array: ArrayBufferView | number): ArrayBufferView { + return crypto.getRandomValues(typeof array == "number" ? new Uint8Array(length) : array) + } +} diff --git a/Base16.ts b/Base16.ts deleted file mode 100644 index a7b52dc..0000000 --- a/Base16.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as ArrayBuffer from "./ArrayBuffer" -import { TextEncoder } from "./TextEncoder" - -export function encode(value: ArrayBuffer | Uint8Array | string, length?: number): string { - const data = - typeof value == "string" - ? new TextEncoder().encode(value) - : value instanceof Uint8Array - ? value - : new Uint8Array(value) - let result: string[] = [] - for (const d of data) - result.push(Math.floor(d / 16).toString(16), (d % 16).toString(16)) - if (length) - result = result.slice(0, length) - return result.join("") -} -export function decode(value: string): Uint8Array { - if (value.length % 2 == 1) - value += "0" - const result = new Uint8Array(value.length / 2) - for (let index = 0; index < result.length; index++) - result[index] = Number.parseInt(value[index * 2], 16) * 16 + Number.parseInt(value[index * 2 + 1], 16) - return result -} -export function xor(data: string[]): string { - return encode(ArrayBuffer.xor(...data.map(decode))) -} -export function bytewiseAdd(data: string[]): string { - return encode(ArrayBuffer.bytewiseAdd(...data.map(decode))) -} -export function add(data: string[]): string { - return encode(ArrayBuffer.add(...data.map(decode))) -} -export function combine(data: string[]): string { - return encode(ArrayBuffer.combine(...data.map(decode))) -} diff --git a/Base16.spec.ts b/Base16/index.spec.ts similarity index 70% rename from Base16.spec.ts rename to Base16/index.spec.ts index 8c2d406..499fdf9 100644 --- a/Base16.spec.ts +++ b/Base16/index.spec.ts @@ -1,6 +1,8 @@ -import { cryptly } from "./index" +import { cryptly } from "../index" describe("Base16", () => { + it.each(["1337", "1ee7", "1EE7", "", "0", "ffff"])("is %s", value => expect(cryptly.Base16.is(value)).toEqual(true)) + it.each(["13.37", "1eet", "1EET", "O"])("is not %s", value => expect(cryptly.Base16.is(value)).toEqual(false)) it("encode standard 1", () => expect(cryptly.Base16.encode("This is the data (*)")).toEqual("5468697320697320746865206461746120282a29")) it("encode standard 2", () => @@ -10,19 +12,19 @@ describe("Base16", () => { it("encode standard 4", () => expect(cryptly.Base16.encode("any carnal pleasur")).toEqual("616e79206361726e616c20706c6561737572")) it("decode standard 1", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base16.decode("5468697320697320746865206461746120282a29"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base16.decode("5468697320697320746865206461746120282a29"))).toEqual( "This is the data (*)" )) it("decode standard 2", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c6561737572652e"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c6561737572652e"))).toEqual( "any carnal pleasure." )) it("decode standard 3", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c656173757265"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c656173757265"))).toEqual( "any carnal pleasure" )) it("decode standard 4", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c6561737572"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base16.decode("616e79206361726e616c20706c6561737572"))).toEqual( "any carnal pleasur" )) it("xor", () => diff --git a/Base16/index.ts b/Base16/index.ts new file mode 100644 index 0000000..7796765 --- /dev/null +++ b/Base16/index.ts @@ -0,0 +1,47 @@ +import { isly } from "isly" +import { ArrayBuffer } from "../ArrayBuffer" + +export type Base16 = string + +export namespace Base16 { + export const type = isly.named("cryptly.Base16", isly.string(/^[A-Fa-f0-9]*$/)) + export const is = type.is + export const flaw = type.flaw + export function encode(value: ArrayBuffer | Uint8Array | string, length?: number): Base16 { + const data = + typeof value == "string" + ? new TextEncoder().encode(value) + : value instanceof Uint8Array + ? value + : new Uint8Array(value) + let result: string[] = [] + for (const d of data) + result.push(Math.floor(d / 16).toString(16), (d % 16).toString(16)) + if (length) + result = result.slice(0, length) + return result.join("") + } + export function decode(value: Base16): Uint8Array { + if (value.length % 2 == 1) + value += "0" + const result = new Uint8Array(value.length / 2) + for (let index = 0; index < result.length; index++) + result[index] = Number.parseInt(value[index * 2], 16) * 16 + Number.parseInt(value[index * 2 + 1], 16) + return result + } + export function xor(data: Base16[]): Base16 { + return encode(ArrayBuffer.xor(...data.map(decode))) + } + export function bytewiseAdd(data: Base16[]): Base16 { + return encode(ArrayBuffer.bytewiseAdd(...data.map(decode))) + } + export function add(data: Base16[]): Base16 { + return encode(ArrayBuffer.add(...data.map(decode))) + } + export function combine(data: Base16[]): Base16 { + return encode(ArrayBuffer.combine(...data.map(decode))) + } + export function random(length: number): Base16 { + return Base16.encode(ArrayBuffer.random(Math.ceil(length / 2))).substring(0, length) + } +} diff --git a/Base32.ts b/Base32.ts deleted file mode 100644 index 80452f6..0000000 --- a/Base32.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as ArrayBuffer from "./ArrayBuffer" -import { TextEncoder } from "./TextEncoder" - -export type Standard = "standard" | "hex" | "crockford" -const tables: { [standard in Standard]: string } = { - standard: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", - hex: "0123456789ABCDEFGHIJKLMNOPQRSTUV", - crockford: "0123456789ABCDEFGHJKMNPQRSTVWXYZ", -} -export function encode( - input: ArrayBuffer | Uint8Array | string | number | bigint, - standard: Standard = "standard", - padding: "" | "=" | "-" = "" -): string { - let data: Uint8Array - switch (typeof input) { - case "string": - data = new TextEncoder().encode(input) - break - case "number": - data = new Uint8Array(new BigUint64Array([BigInt(input)]).buffer) - break - case "bigint": - data = new Uint8Array(new BigUint64Array([input]).buffer) - break - default: - data = input instanceof Uint8Array ? input : new Uint8Array(input) - break - } - const table = tables[standard] - let bits = 0 - let value = 0 - let result = "" - for (let i = 0; i < data.length; i++) { - value = (value << 8) | data[i] - bits += 8 - while (bits >= 5) { - result += table[(value >>> (bits - 5)) & 31] - bits -= 5 - } - } - if (bits > 0) - result += table[(value << (5 - bits)) & 31] - while (padding && result.length % 8 != 0) - result += "=" - return result -} -export function decode(input: string, standard: Standard = "standard"): Uint8Array { - while (input.endsWith("=") && input.length > 0) - input = input.substring(0, input.length - 1) - const table = tables[standard] - const result = new Uint8Array(((input.length * 5) / 8) | 0) - let bits = 0 - let value = 0 - let index = 0 - for (let i = 0; i < input.length; i++) { - value = (value << 5) | table.indexOf(input[i]) - bits += 5 - if (bits >= 8) { - result[index++] = (value >>> (bits - 8)) & 255 - bits -= 8 - } - } - return result -} -export function xor(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.xor(...data.map(d => decode(d, standard))), standard, padding) -} -export function bytewiseAdd(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.bytewiseAdd(...data.map(d => decode(d, standard))), standard, padding) -} -export function add(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.add(...data.map(d => decode(d, standard))), standard, padding) -} -export function combine(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.combine(...data.map(d => decode(d, standard))), standard, padding) -} diff --git a/Base32/Standard.ts b/Base32/Standard.ts new file mode 100644 index 0000000..7219c2a --- /dev/null +++ b/Base32/Standard.ts @@ -0,0 +1,9 @@ +import { isly } from "isly" + +export type Standard = typeof Standard.values[number] +export namespace Standard { + export const values = ["standard", "hex", "crockford"] as const + export const type = isly.named("cryptly.Base32.Standard", isly.string(values)) + export const is = type.is + export const flaw = type.flaw +} diff --git a/Base32.spec.ts b/Base32/index.spec.ts similarity index 71% rename from Base32.spec.ts rename to Base32/index.spec.ts index 0f373b3..cddb799 100644 --- a/Base32.spec.ts +++ b/Base32/index.spec.ts @@ -1,6 +1,8 @@ -import { cryptly } from "./index" +import { cryptly } from "../index" describe("Base32", () => { + it.each(["1337", "LEET1337", "LEET", "", "0"])("is %s", value => expect(cryptly.Base32.is(value)).toEqual(true)) + it.each(["13.37", "1eet1337"])("is not %s", value => expect(cryptly.Base32.is(value)).toEqual(false)) it("encode standard 1", () => expect(cryptly.Base32.encode("This is the data (*)")).toEqual("KRUGS4ZANFZSA5DIMUQGIYLUMEQCQKRJ")) it("encode standard 2", () => @@ -10,19 +12,19 @@ describe("Base32", () => { it("encode standard 4", () => expect(cryptly.Base32.encode("any carnal pleasur")).toEqual("MFXHSIDDMFZG4YLMEBYGYZLBON2XE")) it("decode standard 1", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base32.decode("KRUGS4ZANFZSA5DIMUQGIYLUMEQCQKRJ"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base32.decode("KRUGS4ZANFZSA5DIMUQGIYLUMEQCQKRJ"))).toEqual( "This is the data (*)" )) it("decode standard 2", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XEZJO"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XEZJO"))).toEqual( "any carnal pleasure." )) it("decode standard 3", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XEZI"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XEZI"))).toEqual( "any carnal pleasure" )) it("decode standard 4", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XE"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base32.decode("MFXHSIDDMFZG4YLMEBYGYZLBON2XE"))).toEqual( "any carnal pleasur" )) it("xor", () => diff --git a/Base32/index.ts b/Base32/index.ts new file mode 100644 index 0000000..89f66c4 --- /dev/null +++ b/Base32/index.ts @@ -0,0 +1,88 @@ +import { isly } from "isly" +import { ArrayBuffer } from "../ArrayBuffer" +import { Standard as Base32Standard } from "./Standard" + +export type Base32 = string + +export namespace Base32 { + export import Standard = Base32Standard + export const type = isly.named("cryptly.Base32", isly.string(/^[A-Z0-9]*$/)) + export const is = type.is + export const flaw = type.flaw + const tables: { [standard in Standard]: string } = { + standard: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", + hex: "0123456789ABCDEFGHIJKLMNOPQRSTUV", + crockford: "0123456789ABCDEFGHJKMNPQRSTVWXYZ", + } + export function encode( + input: ArrayBuffer | Uint8Array | string | number | bigint, + standard: Standard = "standard", + padding: "" | "=" | "-" = "" + ): Base32 { + let data: Uint8Array + switch (typeof input) { + case "string": + data = new TextEncoder().encode(input) + break + case "number": + data = new Uint8Array(new BigUint64Array([BigInt(input)]).buffer) + break + case "bigint": + data = new Uint8Array(new BigUint64Array([input]).buffer) + break + default: + data = input instanceof Uint8Array ? input : new Uint8Array(input) + break + } + const table = tables[standard] + let bits = 0 + let value = 0 + let result = "" + for (let i = 0; i < data.length; i++) { + value = (value << 8) | data[i] + bits += 8 + while (bits >= 5) { + result += table[(value >>> (bits - 5)) & 31] + bits -= 5 + } + } + if (bits > 0) + result += table[(value << (5 - bits)) & 31] + while (padding && result.length % 8 != 0) + result += "=" + return result + } + export function decode(input: Base32, standard: Standard = "standard"): Uint8Array { + while (input.endsWith("=") && input.length > 0) + input = input.substring(0, input.length - 1) + const table = tables[standard] + const result = new Uint8Array(((input.length * 5) / 8) | 0) + let bits = 0 + let value = 0 + let index = 0 + for (let i = 0; i < input.length; i++) { + value = (value << 5) | table.indexOf(input[i]) + bits += 5 + if (bits >= 8) { + result[index++] = (value >>> (bits - 8)) & 255 + bits -= 8 + } + } + return result + } + export function xor(data: Base32[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base32 { + return encode(ArrayBuffer.xor(...data.map(d => decode(d, standard))), standard, padding) + } + export function bytewiseAdd(data: Base32[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base32 { + return encode(ArrayBuffer.bytewiseAdd(...data.map(d => decode(d, standard))), standard, padding) + } + export function add(data: Base32[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base32 { + return encode(ArrayBuffer.add(...data.map(d => decode(d, standard))), standard, padding) + } + export function combine(data: Base32[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base32 { + return encode(ArrayBuffer.combine(...data.map(d => decode(d, standard))), standard, padding) + } + export function random(length: number): Base32 { + return Base32.encode(ArrayBuffer.random(Math.ceil(length * (5 / 8)))).substring(0, length) + } +} diff --git a/Base64.ts b/Base64.ts deleted file mode 100644 index e7631d1..0000000 --- a/Base64.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as ArrayBuffer from "./ArrayBuffer" -import { TextEncoder } from "./TextEncoder" - -const tables: { [standard in Standard]: string } = { - standard: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", - url: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", - ordered: "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz", - reversed: "zyxwvutsrqponmlkjihgfedcba_ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210-", -} -export type Standard = "standard" | "url" | "ordered" | "reversed" -export function encode( - value: ArrayBuffer | Uint8Array | string | number | bigint, - standard: Standard = "standard", - padding: "" | "=" | "-" = "" -): string { - let data: Uint8Array - const reverse = (array: Uint8Array) => (standard == "ordered" || standard == "reversed" ? array.reverse() : array) - switch (typeof value) { - case "string": - data = new TextEncoder().encode(value) - break - case "number": - data = reverse(new Uint8Array(new BigUint64Array([BigInt(value)]).buffer)) - break - case "bigint": - data = reverse(new Uint8Array(new BigUint64Array([value]).buffer)) - break - default: - data = value instanceof Uint8Array ? value : new Uint8Array(value) - break - } - const table = tables[standard] - const result: string[] = [] - for (let c = 0; c < data.length; c += 3) { - const c0 = data[c] - const c1 = c + 1 < data.length ? data[c + 1] : 0 - const c2 = c + 2 < data.length ? data[c + 2] : 0 - result.push(table[c0 >>> 2]) - result.push(table[((c0 & 3) << 4) | (c1 >>> 4)]) - result.push(table[((c1 & 15) << 2) | (c2 >>> 6)]) - result.push(table[c2 & 63]) - } - const length = Math.ceil((data.length / 3) * 4) - return result.join("").substring(0, length) + padding.repeat(result.length - length) -} -export function decode(value: string, standard: Standard = "standard"): Uint8Array { - while (value.endsWith("=") && value.length > 0) - value = value.substring(0, value.length - 1) - const table = tables[standard] - const data = [...value].map(c => table.indexOf(c)) - const result = new Uint8Array(Math.floor((data.length / 4) * 3)) - for (let c = 0; c < result.length; c += 3) { - const d0 = data.shift() || 0 - const d1 = data.shift() || 0 - const d2 = data.shift() || 0 - const d3 = data.shift() || 0 - result[c] = (d0 << 2) | (d1 >>> 4) - result[c + 1] = ((d1 & 15) << 4) | (d2 >>> 2) - result[c + 2] = ((d2 & 3) << 6) | d3 - } - return standard == "ordered" || standard == "reversed" ? result.reverse() : result -} -export function next(value: string, increment = 1, standard: Standard = "standard"): string { - const table = tables[standard] - const rest = value.length > 1 ? value.substring(0, value.length - 1) : increment > 0 ? "" : table[63] - const number = (value.length == 0 ? 0 : table.indexOf(value[value.length - 1])) + increment - return ( - (number > 63 || number < 0 ? next(rest, Math.floor(number / 63), standard) : rest) + table[remainder(number, 64)] - ) -} -export function convert(value: string, input: Standard, output: Standard): string { - const inputTable = tables[input] - const outputTable = tables[output] - return [...value].map(c => outputTable[inputTable.indexOf(c)]).join() -} -function remainder(left: number, right: number): number { - return left >= 0 ? left % right : remainder(left + right, right) -} -export function xor(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.xor(...data.map(d => decode(d, standard))), standard, padding) -} -export function bytewiseAdd(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.bytewiseAdd(...data.map(d => decode(d, standard))), standard, padding) -} -export function add(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.add(...data.map(d => decode(d, standard))), standard, padding) -} -export function combine(data: string[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): string { - return encode(ArrayBuffer.combine(...data.map(d => decode(d, standard))), standard, padding) -} diff --git a/Base64/Standard.ts b/Base64/Standard.ts new file mode 100644 index 0000000..e439750 --- /dev/null +++ b/Base64/Standard.ts @@ -0,0 +1,10 @@ +import { isly } from "isly" + +export type Standard = typeof Standard.values[number] + +export namespace Standard { + export const values = ["standard", "url", "ordered", "reversed"] as const + export const type = isly.named("cryptly.Base64.Standard", isly.string(values)) + export const is = type.is + export const flaw = type.flaw +} diff --git a/Base64.spec.ts b/Base64/index.spec.ts similarity index 87% rename from Base64.spec.ts rename to Base64/index.spec.ts index f9bf1c3..6a786c2 100644 --- a/Base64.spec.ts +++ b/Base64/index.spec.ts @@ -1,6 +1,10 @@ -import { cryptly } from "./index" +import { cryptly } from "../index" describe("Base64", () => { + it.each(["1337", "leet1337", "LEET", "", "0", "----", "-_", "+/="])("is %s", value => + expect(cryptly.Base64.is(value)).toEqual(true) + ) + it.each(["13.37", "1e!et"])("is not %s", value => expect(cryptly.Base64.is(value)).toEqual(false)) it("encode standard =", () => expect(cryptly.Base64.encode("This is the data (*)", "standard", "=")).toEqual("VGhpcyBpcyB0aGUgZGF0YSAoKik=")) it("encode standard = 1", () => @@ -39,11 +43,11 @@ describe("Base64", () => { it("encode url", () => expect(cryptly.Base64.encode("This is the data (*)", "url", "")).toEqual("VGhpcyBpcyB0aGUgZGF0YSAoKik")) it("decode url", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base64.decode("VGhpcyBpcyB0aGUgZGF0YSAoKik", "url"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base64.decode("VGhpcyBpcyB0aGUgZGF0YSAoKik", "url"))).toEqual( "This is the data (*)" )) it("decode standard =", () => - expect(new cryptly.TextDecoder().decode(cryptly.Base64.decode("VGhpcyBpcyB0aGUgZGF0YSAoKik==", "url"))).toEqual( + expect(new TextDecoder().decode(cryptly.Base64.decode("VGhpcyBpcyB0aGUgZGF0YSAoKik==", "url"))).toEqual( "This is the data (*)" )) it("decode DvT", () => expect(cryptly.Base64.decode("DvQ", "url")).toEqual(Uint8Array.from([14, 244]))) diff --git a/Base64/index.ts b/Base64/index.ts new file mode 100644 index 0000000..a625a43 --- /dev/null +++ b/Base64/index.ts @@ -0,0 +1,106 @@ +import { isly } from "isly" +import { ArrayBuffer } from "../ArrayBuffer" +import { Standard as Base64Standard } from "./Standard" + +export type Base64 = string + +export namespace Base64 { + export import Standard = Base64Standard + export const type = isly.named("cryptly.Base64", isly.string(/^[A-Za-z0-9+/\-_=]*$/)) + export const is = type.is + export const flaw = type.flaw + const tables: { [standard in Standard]: string } = { + standard: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + url: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", + ordered: "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz", + reversed: "zyxwvutsrqponmlkjihgfedcba_ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210-", + } + export function encode( + value: ArrayBuffer | Uint8Array | string | number | bigint, + standard: Standard = "standard", + padding: "" | "=" | "-" = "" + ): Base64 { + let data: Uint8Array + const reverse = (array: Uint8Array) => (standard == "ordered" || standard == "reversed" ? array.reverse() : array) + switch (typeof value) { + case "string": + data = new TextEncoder().encode(value) + break + case "number": + data = reverse(new Uint8Array(new BigUint64Array([BigInt(value)]).buffer)) + break + case "bigint": + data = reverse(new Uint8Array(new BigUint64Array([value]).buffer)) + break + default: + data = value instanceof Uint8Array ? value : new Uint8Array(value) + break + } + const table = tables[standard] + const result: string[] = [] + for (let c = 0; c < data.length; c += 3) { + const c0 = data[c] + const c1 = c + 1 < data.length ? data[c + 1] : 0 + const c2 = c + 2 < data.length ? data[c + 2] : 0 + result.push(table[c0 >>> 2]) + result.push(table[((c0 & 3) << 4) | (c1 >>> 4)]) + result.push(table[((c1 & 15) << 2) | (c2 >>> 6)]) + result.push(table[c2 & 63]) + } + const length = Math.ceil((data.length / 3) * 4) + return result.join("").substring(0, length) + padding.repeat(result.length - length) + } + export function decode(value: Base64, standard: Standard = "standard"): Uint8Array { + while (value.endsWith("=") && value.length > 0) + value = value.substring(0, value.length - 1) + const table = tables[standard] + const data = [...value].map(c => table.indexOf(c)) + const result = new Uint8Array(Math.floor((data.length / 4) * 3)) + for (let c = 0; c < result.length; c += 3) { + const d0 = data.shift() || 0 + const d1 = data.shift() || 0 + const d2 = data.shift() || 0 + const d3 = data.shift() || 0 + result[c] = (d0 << 2) | (d1 >>> 4) + result[c + 1] = ((d1 & 15) << 4) | (d2 >>> 2) + result[c + 2] = ((d2 & 3) << 6) | d3 + } + return standard == "ordered" || standard == "reversed" ? result.reverse() : result + } + export function next(value: Base64, increment = 1, standard: Standard = "standard"): Base64 { + const table = tables[standard] + const rest = value.length > 1 ? value.substring(0, value.length - 1) : increment > 0 ? "" : table[63] + const number = (value.length == 0 ? 0 : table.indexOf(value[value.length - 1])) + increment + return ( + (number > 63 || number < 0 ? next(rest, Math.floor(number / 63), standard) : rest) + table[remainder(number, 64)] + ) + } + export function convert(value: Base64, input: Standard, output: Standard): Base64 { + const inputTable = tables[input] + const outputTable = tables[output] + return [...value].map(c => outputTable[inputTable.indexOf(c)]).join("") + } + function remainder(left: number, right: number): number { + return left >= 0 ? left % right : remainder(left + right, right) + } + export function xor(data: Base64[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base64 { + return encode(ArrayBuffer.xor(...data.map(d => decode(d, standard))), standard, padding) + } + export function bytewiseAdd(data: Base64[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base64 { + return encode(ArrayBuffer.bytewiseAdd(...data.map(d => decode(d, standard))), standard, padding) + } + export function add(data: Base64[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base64 { + return encode(ArrayBuffer.add(...data.map(d => decode(d, standard))), standard, padding) + } + export function combine(data: Base64[], standard: Standard = "standard", padding: "" | "=" | "-" = ""): Base64 { + return encode(ArrayBuffer.combine(...data.map(d => decode(d, standard))), standard, padding) + } + export function random(length: number): Base64 { + return Base64.encode(ArrayBuffer.random(Math.ceil(length * (3 / 4)))).substring(0, length) + } + export function* slice(data: Base64, length: number): Generator { + let start = 0 + while (start < data.length) + yield data.slice(start, (start = start + length)) + } +} diff --git a/Digester/Algorithm.ts b/Digester/Algorithm.ts index d0fd937..a52b64a 100644 --- a/Digester/Algorithm.ts +++ b/Digester/Algorithm.ts @@ -7,4 +7,12 @@ export namespace Algorithm { export const type = isly.named("cryptly.Digester.Algorithm", isly.string(values)) export const is = type.is export const flaw = type.flaw + export function bits(algorithm: Algorithm): 128 | 256 | 384 | 512 { + return { + "SHA-1": 128 as const, + "SHA-256": 256 as const, + "SHA-384": 384 as const, + "SHA-512": 512 as const, + }[algorithm] + } } diff --git a/Digester/index.spec.ts b/Digester/index.spec.ts index 98e7c7c..f249f45 100644 --- a/Digester/index.spec.ts +++ b/Digester/index.spec.ts @@ -1,48 +1,48 @@ -import { Digester } from "./index" +import { cryptly } from "../index" describe("Digest", () => { it("Get Length 256", async () => { - expect(new Digester("SHA-256").length).toEqual(256) + expect(new cryptly.Digester("SHA-256").length).toEqual(256) }) it("Get Length 512", async () => { - expect(new Digester("SHA-512").length).toEqual(512) + expect(new cryptly.Digester("SHA-512").length).toEqual(512) }) it("Test SHA-1 digest Hex", async () => { - const digest = new Digester("SHA-1") + const digest = new cryptly.Digester("SHA-1") expect(await digest.digest("Test", 16)).toEqual("640ab2bae07bedc4c163f679a746f7ab7fb5d1fa") }) it("Test SHA-256 digest Hex", async () => { - const digest = new Digester("SHA-256") + const digest = new cryptly.Digester("SHA-256") expect(await digest.digest("Test", 16)).toEqual("532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25") }) it("Test SHA-384 digest Hex", async () => { - const digest = new Digester("SHA-384") + const digest = new cryptly.Digester("SHA-384") expect(await digest.digest("Test", 16)).toEqual( "7b8f4654076b80eb963911f19cfad1aaf4285ed48e826f6cde1b01a79aa73fadb5446e667fc4f90417782c91270540f3" ) }) it("Test SHA-512 digest Hex", async () => { - const digest = new Digester("SHA-512") + const digest = new cryptly.Digester("SHA-512") expect(await digest.digest("Test", 16)).toEqual( "c6ee9e33cf5c6715a1d148fd73f7318884b41adcb916021e2bc0e800a5c5dd97f5142178f6ae88c8fdd98e1afb0ce4c8d2c54b5f37b30b7da1997bb33b0b8a31" ) }) it("Test SHA-1 digest Standard", async () => { - const digest = new Digester("SHA-1") + const digest = new cryptly.Digester("SHA-1") expect(await digest.digest("Test", "standard")).toEqual("ZAqyuuB77cTBY/Z5p0b3q3+10fo") }) it("Test SHA-256 digest Standard", async () => { - const digest = new Digester("SHA-256") + const digest = new cryptly.Digester("SHA-256") expect(await digest.digest("Test", "standard")).toEqual("Uy6qvZV0iA2/drm4zACDLCCm7BE9aCKZVQ16bg80XiU") }) it("Test SHA-384 digest Standard", async () => { - const digest = new Digester("SHA-384") + const digest = new cryptly.Digester("SHA-384") expect(await digest.digest("Test", "standard")).toEqual( "e49GVAdrgOuWORHxnPrRqvQoXtSOgm9s3hsBp5qnP621RG5mf8T5BBd4LJEnBUDz" ) }) it("Test SHA-512 digest Standard", async () => { - const digest = new Digester("SHA-512") + const digest = new cryptly.Digester("SHA-512") expect(await digest.digest("Test", "standard")).toEqual( "xu6eM89cZxWh0Uj9c/cxiIS0Gty5FgIeK8DoAKXF3Zf1FCF49q6IyP3Zjhr7DOTI0sVLXzezC32hmXuzOwuKMQ" ) diff --git a/Digester/index.ts b/Digester/index.ts index 9a30f34..48ea982 100644 --- a/Digester/index.ts +++ b/Digester/index.ts @@ -1,15 +1,15 @@ -import * as Base16 from "../Base16" -import * as Base64 from "../Base64" +import { Base16 } from "../Base16" +import { Base64 } from "../Base64" import { crypto } from "../crypto" -import { TextEncoder } from "../TextEncoder" -import { Algorithm } from "./Algorithm" +import { Algorithm as DigesterAlgorithm } from "./Algorithm" export class Digester { get length(): number { - return Digester.lengths[this.algorithm] + return Digester.Algorithm.bits(this.algorithm) } - constructor(readonly algorithm: Algorithm) {} - async digest(data: string, base: 16 | Base64.Standard): Promise + constructor(readonly algorithm: Digester.Algorithm) {} + async digest(data: string, base: 16): Promise + async digest(data: string, base: Base64.Standard): Promise async digest(data: Uint8Array): Promise async digest(data: string | Uint8Array, base: 16 | Base64.Standard = "standard"): Promise { let result: string | Uint8Array @@ -20,10 +20,7 @@ export class Digester { result = new Uint8Array(await crypto.subtle.digest(this.algorithm, data)) return result } - private static lengths: Record = { - "SHA-1": 128, - "SHA-256": 256, - "SHA-384": 384, - "SHA-512": 512, - } +} +export namespace Digester { + export import Algorithm = DigesterAlgorithm } diff --git a/Encrypter/Aes/Encrypted.ts b/Encrypter/Aes/Encrypted.ts index 0943708..e11d72b 100644 --- a/Encrypter/Aes/Encrypted.ts +++ b/Encrypter/Aes/Encrypted.ts @@ -1,14 +1,15 @@ import { isly } from "isly" +import { Base64 } from "../../Base64" export interface Encrypted { key?: string - value: string - salt: string + value: Base64 + salt: Base64 } export namespace Encrypted { export const type = isly.object( - { key: isly.string().optional(), value: isly.string(), salt: isly.string() }, + { key: isly.string().optional(), value: Base64.type, salt: isly.string() }, "cryptly.Encrypter.Aes.Encrypted" ) export const is = type.is diff --git a/Encrypter/Aes/index.ts b/Encrypter/Aes/index.ts index f61f458..98b0c28 100644 --- a/Encrypter/Aes/index.ts +++ b/Encrypter/Aes/index.ts @@ -1,7 +1,5 @@ -import * as Base64 from "../../Base64" +import { Base64 } from "../../Base64" import { crypto } from "../../crypto" -import { TextDecoder } from "../../TextDecoder" -import { TextEncoder } from "../../TextEncoder" import { Encrypted as AesEncrypted } from "./Encrypted" export class Aes { @@ -25,9 +23,9 @@ export class Aes { } } async decrypt(encrypted: AesEncrypted): Promise - async decrypt(encrypted: string, salt: string): Promise - async decrypt(encrypted: AesEncrypted | string, salt?: string): Promise { - if (typeof encrypted == "string") + async decrypt(encrypted: Base64, salt: Base64): Promise + async decrypt(encrypted: AesEncrypted | Base64, salt?: Base64): Promise { + if (Base64.is(encrypted)) encrypted = { value: encrypted, salt: salt ?? "" } return new TextDecoder().decode( new Uint8Array( @@ -39,24 +37,24 @@ export class Aes { ) ) } - async export(): Promise - async export(parts: number): Promise - async export(parts: Uint8Array): Promise - async export(parts: Uint8Array[]): Promise - async export(parts: string): Promise - async export(parts: string[]): Promise - async export(parts?: number | Uint8Array | Uint8Array[] | string | string[]): Promise { - let result: string | string[] + async export(): Promise + async export(parts: number): Promise + async export(parts: Uint8Array): Promise + async export(parts: Uint8Array[]): Promise + async export(parts: Base64): Promise + async export(parts: Base64[]): Promise + async export(parts?: number | Uint8Array | Uint8Array[] | Base64 | Base64[]): Promise { + let result: Base64 | Base64[] const key = new Uint8Array(await crypto.subtle.exportKey("raw", await this.key)) if (parts == undefined) result = (await this.export(1))[0] else if (typeof parts == "number") result = await this.export(parts > 1 ? Aes.generateRandomKeys(key.length, parts - 1) : []) - else if (typeof parts == "string") + else if (Base64.is(parts)) result = await this.export(Base64.decode(parts, "url")) else if (parts instanceof Uint8Array) result = (await this.export([parts]))[0] - else if (this.isStringArray(parts)) + else if (this.isBase64Array(parts)) result = await this.export(parts.map(part => Base64.decode(part, "url"))) else { parts = [Aes.reduceKeys([key, ...parts]), ...parts] @@ -64,22 +62,22 @@ export class Aes { } return result } - private isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.length > 0 && value.every((item: any) => typeof item == "string") + private isBase64Array(value: unknown): value is Base64[] { + return Array.isArray(value) && value.length > 0 && value.every(Base64.is) } - static cbc(key: 256 | string | string[]): Aes { + static cbc(key: 256 | Base64 | Base64[]): Aes { return Aes.generate("AES-CBC", key) } - static gcm(key: 256 | string | string[]): Aes { + static gcm(key: 256 | Base64 | Base64[]): Aes { return Aes.generate("AES-GCM", key) } - static random(length: 256): string - static random(length: 256, parts: number): string[] - static random(length: 256, parts?: number): string | string[] { + static random(length: 256): Base64 + static random(length: 256, parts: number): Base64[] + static random(length: 256, parts?: number): Base64 | Base64[] { const result = Aes.generateRandomKeys(length / 8, parts && parts > 0 ? parts : 1).map(r => Base64.encode(r, "url")) return parts ? result : result[0] } - private static generate(algorithm: "AES-CBC" | "AES-GCM", key: 256 | string | string[]): Aes { + private static generate(algorithm: "AES-CBC" | "AES-GCM", key: 256 | Base64 | Base64[]): Aes { return new Aes( typeof key == "number" ? crypto.subtle.generateKey({ name: algorithm, length: key }, true, ["encrypt", "decrypt"]) diff --git a/Encrypter/Rsa/Encrypted.ts b/Encrypter/Rsa/Encrypted.ts index 261cda3..cd700ed 100644 --- a/Encrypter/Rsa/Encrypted.ts +++ b/Encrypter/Rsa/Encrypted.ts @@ -1,13 +1,14 @@ import { isly } from "isly" +import { Base64 } from "../../Base64" export interface Encrypted { key?: string - value: string + value: Base64 } export namespace Encrypted { export const type = isly.object( - { key: isly.string().optional(), value: isly.string() }, + { key: isly.string().optional(), value: Base64.type }, "cryptly.Encrypter.Rsa.Encrypted" ) export const is = type.is diff --git a/Encrypter/Rsa/index.ts b/Encrypter/Rsa/index.ts index a72112e..6fabdfb 100644 --- a/Encrypter/Rsa/index.ts +++ b/Encrypter/Rsa/index.ts @@ -1,8 +1,6 @@ -import * as Base64 from "../../Base64" +import { Base64 } from "../../Base64" import { crypto } from "../../crypto" import { Key } from "../../Key" -import { TextDecoder } from "../../TextDecoder" -import { TextEncoder } from "../../TextEncoder" import { Encrypted as RsaEncrypted } from "./Encrypted" export class Rsa { @@ -27,9 +25,9 @@ export class Rsa { : undefined } async decrypt(encrypted: RsaEncrypted): Promise - async decrypt(encrypted: string): Promise - async decrypt(encrypted: RsaEncrypted | string): Promise { - if (typeof encrypted == "string") + async decrypt(encrypted: Base64): Promise + async decrypt(encrypted: RsaEncrypted | Base64): Promise { + if (Base64.is(encrypted)) encrypted = { value: encrypted } const key = (await this.keys)?.private return key @@ -40,11 +38,11 @@ export class Rsa { ) : undefined } - async export(type: "private" | "public"): Promise - async export(): Promise<{ public: string | undefined; private: string | undefined }> + async export(type: "private" | "public"): Promise + async export(): Promise<{ public: Base64 | undefined; private: Base64 | undefined }> async export( type?: "private" | "public" - ): Promise { + ): Promise { return type ? (await this.keys)[type]?.export() : Object.fromEntries( @@ -54,9 +52,9 @@ export class Rsa { static generate(key: 1024 | 2048 | 4096): Rsa { return new Rsa(Key.Rsa.generate(key)) } - static import(type: "public" | "private", key: string | ArrayBuffer): Rsa - static import(publicKey: string | ArrayBuffer, privateKey: string | ArrayBuffer): Rsa - static import(type: "public" | "private" | string | ArrayBuffer, key: string | ArrayBuffer): Rsa { + static import(type: "public" | "private", key: Base64 | ArrayBuffer): Rsa + static import(publicKey: Base64 | ArrayBuffer, privateKey: Base64 | ArrayBuffer): Rsa + static import(type: "public" | "private" | Base64 | ArrayBuffer, key: Base64 | ArrayBuffer): Rsa { return new Rsa( type == "public" || type == "private" ? Key.Rsa.import(type, key).then(result => ({ [type]: result })) diff --git a/Encrypters.spec.ts b/Encrypters/index.spec.ts similarity index 97% rename from Encrypters.spec.ts rename to Encrypters/index.spec.ts index 1dca1d7..7242fa6 100644 --- a/Encrypters.spec.ts +++ b/Encrypters/index.spec.ts @@ -1,4 +1,4 @@ -import { cryptly } from "./index" +import { cryptly } from "../index" describe("Context.PrimarySecrets", () => { it("create", async () => { diff --git a/Encrypters.ts b/Encrypters/index.ts similarity index 72% rename from Encrypters.ts rename to Encrypters/index.ts index 9c2e8ba..2175169 100644 --- a/Encrypters.ts +++ b/Encrypters/index.ts @@ -1,5 +1,5 @@ -import { Encrypter } from "./Encrypter" -import { Tuple2 } from "./Tuple" +import { Base64 } from "../Base64" +import { Encrypter } from "../Encrypter" export type Encrypters = Record & { current: Encrypter.Aes } @@ -7,9 +7,9 @@ export namespace Encrypters { export function create( create: (keys: string[]) => Encrypter.Aes, current: string, - ...secrets: string[] | Record[] + ...secrets: Base64[] | Record[] ): Encrypters { - const [first, ...remainder] = isStringArray(secrets) + const [first, ...remainder] = isBase64Array(secrets) ? secrets.map(part => Object.fromEntries(part.split(",").map(secret => secret.split(":", 2).map(item => item.trim()))) ) @@ -17,7 +17,7 @@ export namespace Encrypters { const result = Object.assign( {}, ...Object.entries(first) - .map>(([name, secret]) => [ + .map<[Base64, Base64[]]>(([name, secret]) => [ name, [secret, ...remainder.map(part => part[name]).filter(part => part)], ]) @@ -31,6 +31,6 @@ export namespace Encrypters { } } -function isStringArray(value: string[] | any): value is string[] { +function isBase64Array(value: Base64[] | any): value is Base64[] { return Array.isArray(value) && value.length > 0 && typeof value[0] == "string" } diff --git a/Identifier/Identifier12.spec.ts b/Identifier/Identifier12.spec.ts new file mode 100644 index 0000000..7c093ae --- /dev/null +++ b/Identifier/Identifier12.spec.ts @@ -0,0 +1,65 @@ +import { cryptly } from "../index" + +describe("Identifier12", () => { + it("generate is", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(8))).toBeTruthy()) + it("generate length", () => expect(cryptly.Identifier.generate(8)).toHaveLength(8)) + const data = [ + { + identifier: "abcdabcdabcd", + binary: [105, 183, 29, 105, 183, 29, 105, 183, 29], + hexadecimal: "69b71d69b71d69b71d", + value: 116235193399069, + }, + { + identifier: "____________", + binary: [255, 255, 255, 255, 255, 255, 255, 255, 255], + hexadecimal: "ffffffffffffffffff", + value: 281474976710655, + }, + { identifier: "AAAAAAAAAAAA", binary: [0, 0, 0, 0, 0, 0, 0, 0, 0], hexadecimal: "000000000000000000", value: 0 }, + ] + it.each(data)(`toBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.toBinary(identifier)).toEqual(new Uint8Array(binary)) + ) + it.each(data)(`fromBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.fromBinary(new Uint8Array(binary))).toEqual(identifier) + ) + it.each(data)(`is %s`, ({ identifier }) => expect(cryptly.Identifier.is(identifier, 12)).toBeTruthy()) + it.each(data)(`toHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.toHexadecimal(identifier)).toEqual(hexadecimal) + ) + it.each(data)(`fromHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.fromHexadecimal(hexadecimal)).toEqual(identifier) + ) + it.each(["!", "/", "=", "."])("is not $s", c => expect(cryptly.Identifier.is(`HelloWorld${c}0`, 12)).toEqual(false)) + it.each([[1691418818480, /^---0XS0exv[\w\d-_]{2}$/]])(`generate ordered w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(12, "ordered", prefix)).toMatch(result) + ) + it.each([[1691418818480, /^zzzySXyK13z[\w\d-_]{1}$/]])(`generate reversed w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(12, "reversed", prefix)).toMatch(result) + ) + const time = 1691418818480 + it.each([ + [0, 1], + [1, 21111], + [2, 344546], + [3, 41112], + [4, 5434], + ])("order of ordered", (left, right) => + expect( + cryptly.Identifier.generate(12, "ordered", time + left) < cryptly.Identifier.generate(12, "ordered", time + right) + ).toEqual(true) + ) + it.each([ + [0, 1], + [0, 1666], + [1, 21111], + [2, 32323], + [3, 434], + ])("order of reversed", (left, right) => + expect( + cryptly.Identifier.generate(12, "reversed", time + left) > + cryptly.Identifier.generate(12, "reversed", time + right) + ).toEqual(true) + ) +}) diff --git a/Identifier/Identifier16.spec.ts b/Identifier/Identifier16.spec.ts new file mode 100644 index 0000000..8ac7d4f --- /dev/null +++ b/Identifier/Identifier16.spec.ts @@ -0,0 +1,59 @@ +import { cryptly } from "../index" + +describe("Identifier16", () => { + it("generate is", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(16))).toBeTruthy()) + it("generate length", () => expect(cryptly.Identifier.generate(16)).toHaveLength(16)) + it("fromHexadecimal length 24", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183bd3")).toEqual("XUKCtnLtPHc4GDvT")) + it.each([[1691418818480, /^---0XS0exv[\w\d-_]{6}$/]])(`generate ordered w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(16, "ordered", prefix)).toMatch(result) + ) + it.each([[1691418818480, /^zzzySXyK13z[\w\d-_]{5}$/]])(`generate reversed w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(16, "reversed", prefix)).toMatch(result) + ) + it("fromHexadecimal length 24", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183bd3")).toEqual("XUKCtnLtPHc4GDvT")) + it("toHexadecimal length 24", () => + expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvT")).toEqual("5d4282b672ed3c7738183bd3")) + it("fromHexadecimal length 23", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183bd")).toEqual("XUKCtnLtPHc4GDvQ")) + it("toHexadecimal length 23", () => + expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvQ", 23)).toEqual("5d4282b672ed3c7738183bd")) + it("fromHexadecimal length 22", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183b")).toEqual("XUKCtnLtPHc4GDs")) + it("toHexadecimal length 22", () => + expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvs", 22)).toEqual("5d4282b672ed3c7738183b")) + it("fromHexadecimal length 21", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183")).toEqual("XUKCtnLtPHc4GDA")) + it("toHexadecimal length 21", () => + expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDA", 21)).toEqual("5d4282b672ed3c7738183")) + it("fromHexadecimal length 20", () => + expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c773818")).toEqual("XUKCtnLtPHc4GA")) + it("toHexadecimal length 20", () => + expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GA", 20)).toEqual("5d4282b672ed3c773818")) + + const time = 1691418818480 + it.each([ + [0, 1], + [1, 21111], + [2, 344546], + [3, 41112], + [4, 5434], + ])("order of ordered", (left, right) => + expect( + cryptly.Identifier.generate(16, "ordered", time + left) < cryptly.Identifier.generate(16, "ordered", time + right) + ).toEqual(true) + ) + it.each([ + [0, 1], + [0, 1666], + [1, 21111], + [2, 32323], + [3, 434], + ])("order of reversed", (left, right) => + expect( + cryptly.Identifier.generate(16, "reversed", time + left) > + cryptly.Identifier.generate(16, "reversed", time + right) + ).toEqual(true) + ) +}) diff --git a/Identifier/Identifier4.spec.ts b/Identifier/Identifier4.spec.ts new file mode 100644 index 0000000..1ba71e6 --- /dev/null +++ b/Identifier/Identifier4.spec.ts @@ -0,0 +1,224 @@ +import { cryptly } from "../index" + +describe("Identifier4", () => { + it("generate is", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(4), 4)).toEqual(true)) + it("generate length", () => expect(cryptly.Identifier.generate(4)).toHaveLength(4)) + it("min", () => expect(cryptly.Identifier.min(4)).toEqual("----")) + it("max", () => expect(cryptly.Identifier.max(4)).toEqual("zzzz")) + it.each(["ordered", "reversed"] as const)("generate is %s", standard => + expect(cryptly.Identifier.is(cryptly.Identifier.generate(4, standard, ""), 4)).toEqual(true) + ) + const data = [ + { + identifier: "abcd", + binary: [105, 183, 29], + hexadecimal: "69b71d", + value: 6928157, + ordered: "PQRS", + reversed: "_ZYX", + next: "PQRT", + previous: "PQRR", + }, + { + identifier: "test", + binary: [181, 235, 45], + hexadecimal: "b5eb2d", + value: 11922221, + ordered: "hTgh", + reversed: "HWIH", + next: "hTgi", + previous: "hTgg", + }, + { + identifier: "DEMO", + binary: [12, 67, 14], + hexadecimal: "0c430e", + value: 803598, + ordered: "23BD", + reversed: "wvnl", + next: "23BE", + previous: "23BC", + }, + { + identifier: "TEST", + binary: [76, 68, 147], + hexadecimal: "4c4493", + value: 4998291, + ordered: "I3HI", + reversed: "gvhg", + next: "I3HJ", + previous: "I3HH", + }, + { + identifier: "____", + binary: [255, 255, 255], + hexadecimal: "ffffff", + value: 16777215, + ordered: "zzzz", + reversed: "----", + next: "----", + previous: "zzzy", + }, + { + identifier: "zzzz", + binary: [207, 60, 243], + hexadecimal: "cf3cf3", + value: 13581555, + ordered: "nnnn", + reversed: "BBBB", + next: "nnno", + previous: "nnnm", + }, + { + identifier: "AAAA", + binary: [0, 0, 0], + hexadecimal: "000000", + value: 0, + ordered: "----", + reversed: "zzzz", + next: "---0", + previous: "zzzz", + }, + { + identifier: "aAzZ", + binary: [104, 12, 217], + hexadecimal: "680cd9", + value: 6819033, + ordered: "P-nO", + reversed: "_zBa", + next: "P-nP", + previous: "P-nN", + }, + { + identifier: "demo", + binary: [117, 233, 168], + hexadecimal: "75e9a8", + value: 7727528, + ordered: "STac", + reversed: "XWOM", + next: "STad", + previous: "STab", + }, + { + identifier: "GX_K", + binary: [25, 127, 202], + hexadecimal: "197fca", + value: 1671114, + ordered: "5Mz9", + reversed: "tc-p", + next: "5MzA", + previous: "5Mz8", + }, + { + identifier: "GDvT", + binary: [24, 59, 211], + hexadecimal: "183bd3", + value: 1588179, + ordered: "52jI", + reversed: "twFg", + next: "52jJ", + previous: "52jH", + }, + { + identifier: "tgAg", + binary: [182, 0, 32], + hexadecimal: "b60020", + value: 11927584, + ordered: "hV-V", + reversed: "HUzU", + next: "hV-W", + previous: "hV-U", + }, + { + identifier: "LeeT", + binary: [45, 231, 147], + hexadecimal: "2de793", + value: 3008403, + ordered: "ATTI", + reversed: "oWWg", + next: "ATTJ", + previous: "ATTH", + }, + { + identifier: "AAA_", + binary: [0, 0, 63], + hexadecimal: "00003f", + value: 63, + ordered: "---z", + reversed: "zzz-", + next: "--0-", + previous: "---y", + }, + { + identifier: "___A", + binary: [255, 255, 192], + hexadecimal: "ffffc0", + value: 16777152, + ordered: "zzz-", + reversed: "---z", + next: "zzz0", + previous: "zzyz", + }, + ] + it.each(data)(`toBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.toBinary(identifier)).toEqual(new Uint8Array(binary)) + ) + it.each(data)(`fromBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.fromBinary(new Uint8Array(binary))).toEqual(identifier) + ) + it.each(data)(`is %s`, ({ identifier }) => expect(cryptly.Identifier.is(identifier, 4)).toBeTruthy()) + + it.each(data)(`toHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.toHexadecimal(identifier)).toEqual(hexadecimal) + ) + it.each(data)(`fromHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.fromHexadecimal(hexadecimal)).toEqual(identifier) + ) + it.each(data)(`toUint24 %s`, ({ identifier, value }) => + expect(cryptly.Identifier.toUint24(identifier)).toEqual(value) + ) + it.each(data)(`fromUint24 %s`, ({ identifier, value }) => + expect(cryptly.Identifier.fromUint24(value)).toEqual(identifier) + ) + it.each(data)(`ordered %s`, ({ identifier, ordered }) => + expect(cryptly.Identifier.convert(identifier, "url", "ordered")).toEqual(ordered) + ) + it.each(data)(`reversed %s`, ({ identifier, reversed }) => + expect(cryptly.Identifier.convert(identifier, "url", "reversed")).toEqual(reversed) + ) + it.each(data)(`next %s`, ({ ordered, next }) => expect(cryptly.Identifier.next(ordered)).toEqual(next)) + it.each(data)(`previous %s`, ({ ordered, previous }) => + expect(cryptly.Identifier.previous(ordered)).toEqual(previous) + ) + it.each(["!", "/", "=", "."])("is not $s", c => expect(cryptly.Identifier.is(`He${c}0`, 4)).toEqual(false)) + it.each([[1691418818480, /^---0$/]])(`generate ordered w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(4, "ordered", prefix)).toMatch(result) + ) + it.each([[1691418818480, /^zzzy$/]])(`generate reversed w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(4, "reversed", prefix)).toMatch(result) + ) + const time = 1691418818480 + it.each([ + [0, 1], + [1, 21111], + [2, 344546], + [3, 41112], + [4, 5434], + ])("order of ordered", (left, right) => + expect( + cryptly.Identifier.generate(4, "ordered", time + left) <= cryptly.Identifier.generate(4, "ordered", time + right) + ).toEqual(true) + ) + it.each([ + [0, 1], + [0, 1666], + [1, 21111], + [2, 32323], + [3, 434], + ])("order of reversed", (left, right) => + expect( + cryptly.Identifier.generate(4, "reversed", time + left) >= + cryptly.Identifier.generate(4, "reversed", time + right) + ).toEqual(true) + ) +}) diff --git a/Identifier/Identifier64.spec.ts b/Identifier/Identifier64.spec.ts new file mode 100644 index 0000000..143f107 --- /dev/null +++ b/Identifier/Identifier64.spec.ts @@ -0,0 +1,62 @@ +import { cryptly } from "../index" + +describe("Identifier64", () => { + it("generate is", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(64))).toEqual(true)) + it("generate is length 64", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(64), 64)).toEqual(true)) + it("generate is not length 64", () => + expect(cryptly.Identifier.is(cryptly.Identifier.generate(32), 64)).toEqual(false)) + it("generate length", () => expect(cryptly.Identifier.generate(64)).toHaveLength(64)) + it.each([[1691418818480, /^---0XS0exv[\w\d-_]{54}$/]])(`generate ordered w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(64, "ordered", prefix)).toMatch(result) + ) + it.each([[1691418818480, /^zzzySXyK13z[\w\d-_]{53}$/]])(`generate reversed w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(64, "reversed", prefix)).toMatch(result) + ) + const data = [ + { + identifier: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", + binary: [ + 0, 16, 131, 16, 81, 135, 32, 146, 139, 48, 211, 143, 65, 20, 147, 81, 85, 151, 97, 150, 155, 113, 215, 159, 130, + 24, 163, 146, 89, 167, 162, 154, 171, 178, 219, 175, 195, 28, 179, 211, 93, 183, 227, 158, 187, 243, 223, 191, + ], + hexadecimal: "00108310518720928b30d38f41149351559761969b71d79f8218a39259a7a29aabb2dbafc31cb3d35db7e39ebbf3dfbf", + }, + ] + it.each(data)(`is %s`, ({ identifier }) => expect(cryptly.Identifier.is(identifier, 64)).toBeTruthy()) + it.each(data)(`toBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.toBinary(identifier)).toEqual(new Uint8Array(binary)) + ) + it.each(data)(`fromBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.fromBinary(new Uint8Array(binary))).toEqual(identifier) + ) + it.each(data)(`toHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.toHexadecimal(identifier)).toEqual(hexadecimal) + ) + it.each(data)(`fromHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.fromHexadecimal(hexadecimal)).toEqual(identifier) + ) + const time = 1691418818480 + it.each([ + [0, 1], + [1, 21111], + [2, 344546], + [3, 41112], + [4, 5434], + ])("order of ordered", (left, right) => + expect( + cryptly.Identifier.generate(64, "ordered", time + left) < cryptly.Identifier.generate(64, "ordered", time + right) + ).toEqual(true) + ) + it.each([ + [0, 1], + [0, 1666], + [1, 21111], + [2, 32323], + [3, 434], + ])("order of reversed", (left, right) => + expect( + cryptly.Identifier.generate(64, "reversed", time + left) > + cryptly.Identifier.generate(64, "reversed", time + right) + ).toEqual(true) + ) +}) diff --git a/Identifier/Identifier8.spec.ts b/Identifier/Identifier8.spec.ts new file mode 100644 index 0000000..e720ce7 --- /dev/null +++ b/Identifier/Identifier8.spec.ts @@ -0,0 +1,111 @@ +import { cryptly } from "../index" + +describe("Identifier8", () => { + it("generate is", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(8))).toBeTruthy()) + it("generate length", () => expect(cryptly.Identifier.generate(8)).toHaveLength(8)) + const data = [ + { + identifier: "abcdabcd", + binary: [105, 183, 29, 105, 183, 29], + hexadecimal: "69b71d69b71d", + value: 116235193399069, + }, + { + identifier: "testtest", + binary: [181, 235, 45, 181, 235, 45], + hexadecimal: "b5eb2db5eb2d", + value: 200021688838957, + }, + { identifier: "DEMODEMO", binary: [12, 67, 14, 12, 67, 14], hexadecimal: "0c430e0c430e", value: 13482138026766 }, + { identifier: "TESTTEST", binary: [76, 68, 147, 76, 68, 147], hexadecimal: "4c44934c4493", value: 83857412736147 }, + { + identifier: "________", + binary: [255, 255, 255, 255, 255, 255], + hexadecimal: "ffffffffffff", + value: 281474976710655, + }, + { + identifier: "zzzzzzzz", + binary: [207, 60, 243, 207, 60, 243], + hexadecimal: "cf3cf3cf3cf3", + value: 227860695432435, + }, + { identifier: "AAAAAAAA", binary: [0, 0, 0, 0, 0, 0], hexadecimal: "000000000000", value: 0 }, + { + identifier: "aAzZaAzZ", + binary: [104, 12, 217, 104, 12, 217], + hexadecimal: "680cd9680cd9", + value: 114404396371161, + }, + { + identifier: "demodemo", + binary: [117, 233, 168, 117, 233, 168], + hexadecimal: "75e9a875e9a8", + value: 129646414129576, + }, + { + identifier: "GX_KGX_K", + binary: [25, 127, 202, 25, 127, 202], + hexadecimal: "197fca197fca", + value: 28036642209738, + }, + { identifier: "GDvTGDvT", binary: [24, 59, 211, 24, 59, 211], hexadecimal: "183bd3183bd3", value: 26645223717843 }, + { identifier: "tgAgtgAg", binary: [182, 0, 32, 182, 0, 32], hexadecimal: "b60020b60020", value: 200111665053728 }, + { + identifier: "LeeTLeeT", + binary: [45, 231, 147, 45, 231, 147], + hexadecimal: "2de7932de793", + value: 50472629954451, + }, + ] + it.each(data)(`is %s`, ({ identifier }) => expect(cryptly.Identifier.is(identifier, 8)).toBeTruthy()) + it.each(data)(`toBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.toBinary(identifier)).toEqual(new Uint8Array(binary)) + ) + it.each(data)(`fromBinary %s`, ({ identifier, binary }) => + expect(cryptly.Identifier.fromBinary(new Uint8Array(binary))).toEqual(identifier) + ) + it.each(data)(`toHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.toHexadecimal(identifier)).toEqual(hexadecimal) + ) + it.each(data)(`fromHexadecimal %s`, ({ identifier, hexadecimal }) => + expect(cryptly.Identifier.fromHexadecimal(hexadecimal)).toEqual(identifier) + ) + it.each(data)(`toUint48 %s`, ({ identifier, value }) => + expect(cryptly.Identifier.toUint48(identifier)).toEqual(value) + ) + it.each(data)(`fromUint48 %s`, ({ identifier, value }) => + expect(cryptly.Identifier.fromUint48(value)).toEqual(identifier) + ) + it.each(["!", "/", "=", "."])("is not $s", c => expect(cryptly.Identifier.is(`Hello${c}01`, 8)).toEqual(false)) + it.each([[1691418818480, /^---0XS0e$/]])(`generate ordered w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(8, "ordered", prefix)).toMatch(result) + ) + it.each([[1691418818480, /^zzzySXyK$/]])(`generate reversed w/ prefix %s`, (prefix, result) => + expect(cryptly.Identifier.generate(8, "reversed", prefix)).toMatch(result) + ) + const time = 1691418818480 + it.each([ + [0, 1], + [1, 21111], + [2, 344546], + [3, 41112], + [4, 5434], + ])("order of ordered", (left, right) => + expect( + cryptly.Identifier.generate(8, "ordered", time + left) <= cryptly.Identifier.generate(8, "ordered", time + right) + ).toEqual(true) + ) + it.each([ + [0, 1], + [0, 1666], + [1, 21111], + [2, 32323], + [3, 434], + ])("order of reversed", (left, right) => + expect( + cryptly.Identifier.generate(8, "reversed", time + left) >= + cryptly.Identifier.generate(8, "reversed", time + right) + ).toEqual(true) + ) +}) diff --git a/Identifier/index.spec.ts b/Identifier/index.spec.ts deleted file mode 100644 index 71c5a5b..0000000 --- a/Identifier/index.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { cryptly } from "../index" - -describe("Identifier", () => { - it("generate", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(12))).toBeTruthy()) - it("generate lengths", () => { - for (const length of cryptly.Identifier.Length.values) { - const identifier = cryptly.Identifier.generate(length) - expect(identifier).toHaveLength(length) - expect(cryptly.Identifier.fromBinary(cryptly.Identifier.toBinary(identifier))).toEqual(identifier) - expect(cryptly.Identifier.fromHexadecimal(cryptly.Identifier.toHexadecimal(identifier))).toEqual(identifier) - } - }) - it("is random", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(64))).toBeTruthy()) - it("is random length", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(64), 64)).toBeTruthy()) - it("is not length", () => expect(cryptly.Identifier.is(cryptly.Identifier.generate(64), 32)).toBeFalsy()) - it("is", () => expect(cryptly.Identifier.is("aAzZ09-_")).toBeTruthy()) - it("is all", () => - expect(cryptly.Identifier.is("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")).toBeTruthy()) - it("is not !", () => expect(cryptly.Identifier.is("Hello!0123")).toBeFalsy()) - it("is not /", () => expect(cryptly.Identifier.is("Hello/0123")).toBeFalsy()) - it("is not =", () => expect(cryptly.Identifier.is("Hello=0123")).toBeFalsy()) - it("is not .", () => expect(cryptly.Identifier.is("Hello.0123")).toBeFalsy()) - - const binary = [ - 0, 16, 131, 16, 81, 135, 32, 146, 139, 48, 211, 143, 65, 20, 147, 81, 85, 151, 97, 150, 155, 113, 215, 159, 130, 24, - 163, 146, 89, 167, 162, 154, 171, 178, 219, 175, 195, 28, 179, 211, 93, 183, 227, 158, 187, 243, 223, 191, - ] - const all = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - it("fromBinary", () => expect(cryptly.Identifier.fromBinary(Uint8Array.from(binary))).toEqual(all)) - it("toBinary", () => expect(cryptly.Identifier.toBinary(all)).toEqual(Uint8Array.from(binary))) - - it("fromHexadecimal length 24", () => - expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183bd3")).toEqual("XUKCtnLtPHc4GDvT")) - it("toHexadecimal length 24", () => - expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvT")).toEqual("5d4282b672ed3c7738183bd3")) - it("fromHexadecimal length 23", () => - expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183bd")).toEqual("XUKCtnLtPHc4GDvQ")) - it("toHexadecimal length 23", () => - expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvQ", 23)).toEqual("5d4282b672ed3c7738183bd")) - it("fromHexadecimal length 22", () => - expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183b")).toEqual("XUKCtnLtPHc4GDs")) - it("toHexadecimal length 22", () => - expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDvs", 22)).toEqual("5d4282b672ed3c7738183b")) - it("fromHexadecimal length 21", () => - expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c7738183")).toEqual("XUKCtnLtPHc4GDA")) - it("toHexadecimal length 21", () => - expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GDA", 21)).toEqual("5d4282b672ed3c7738183")) - it("fromHexadecimal length 20", () => - expect(cryptly.Identifier.fromHexadecimal("5d4282b672ed3c773818")).toEqual("XUKCtnLtPHc4GA")) - it("toHexadecimal length 20", () => - expect(cryptly.Identifier.toHexadecimal("XUKCtnLtPHc4GA", 20)).toEqual("5d4282b672ed3c773818")) - - it("toBinary length 4", () => expect(cryptly.Identifier.toBinary("tgAg")).toEqual(Uint8Array.from([182, 0, 32]))) - it("fromBinary length 4", () => expect(cryptly.Identifier.fromBinary(Uint8Array.from([182, 0, 32]))).toEqual("tgAg")) - - it("toHexadecimal length 4", () => expect(cryptly.Identifier.toHexadecimal("tgAg")).toEqual("b60020")) - it("fromHexadecimal length 4", () => expect(cryptly.Identifier.fromHexadecimal("b60020")).toEqual("tgAg")) - it("toHexadecimal test", () => expect(cryptly.Identifier.toHexadecimal("test")).toEqual("b5eb2d")) - it("fromHexadecimal test", () => expect(cryptly.Identifier.fromHexadecimal("b5eb2d")).toEqual("test")) - it("toHexadecimal demo", () => expect(cryptly.Identifier.toHexadecimal("demo")).toEqual("75e9a8")) - it("fromHexadecimal demo", () => expect(cryptly.Identifier.fromHexadecimal("75e9a8")).toEqual("demo")) - it("toHexadecimal QYklGX_K", () => expect(cryptly.Identifier.toHexadecimal("QYklGX_K")).toEqual("418925197fca")) - it("fromHexadecimal QYklGX_K", () => expect(cryptly.Identifier.fromHexadecimal("418925197fca")).toEqual("QYklGX_K")) - it("toHexadecimal length 6", () => expect(cryptly.Identifier.toHexadecimal("DvQecA")).toEqual("0ef41e70")) - it("fromHexadecimal length 6", () => expect(cryptly.Identifier.fromHexadecimal("0ef41e70")).toEqual("DvQecA")) - it("fromDecimal length 6", () => expect(cryptly.Identifier.toHexadecimal("DvQecA")).toEqual("0ef41e70")) - it("fromHexadecimal length 6", () => expect(cryptly.Identifier.fromHexadecimal("0ef41e70")).toEqual("DvQecA")) - it("toUint24 length 4", () => expect(cryptly.Identifier.toUint24("lEEt")).toEqual(9716013)) - it("fromUint24 length 4", () => expect(cryptly.Identifier.fromUint24(9716013)).toEqual("lEEt")) - it("toUint24 max safe", () => expect(cryptly.Identifier.toUint24("____")).toEqual(16777215)) - it("fromUint24 max safe", () => expect(cryptly.Identifier.fromUint24(16777215)).toEqual("____")) - it("toUint24 min safe", () => expect(cryptly.Identifier.toUint24("AAAA")).toEqual(0)) - it("fromUint24 min safe", () => expect(cryptly.Identifier.fromUint24(0)).toEqual("AAAA")) - it("toUint48 length 8", () => expect(cryptly.Identifier.toUint48("lEEtLeeT")).toEqual(163007651768211)) - it("fromUint48 length 8", () => expect(cryptly.Identifier.fromUint48(163007651768211)).toEqual("lEEtLeeT")) - it("toUint48 max safe", () => expect(cryptly.Identifier.toUint48("________")).toEqual(281474976710655)) - it("fromUint48 max safe", () => expect(cryptly.Identifier.fromUint48(281474976710655)).toEqual("________")) - it("toUint48 min safe", () => expect(cryptly.Identifier.toUint48("AAAAAAAA")).toEqual(0)) - it("fromUint48 min safe", () => expect(cryptly.Identifier.fromUint48(0)).toEqual("AAAAAAAA")) - it("min", () => expect(cryptly.Identifier.min(4)).toEqual("----")) - it("max", () => expect(cryptly.Identifier.max(4)).toEqual("zzzz")) - it("next", () => { - expect(cryptly.Identifier.next("----")).toEqual("---0") - expect(cryptly.Identifier.next("---z")).toEqual("--0-") - expect(cryptly.Identifier.next("zzzz")).toEqual("----") - }) - it("previous", () => { - expect(cryptly.Identifier.previous("zzzz")).toEqual("zzzy") - expect(cryptly.Identifier.previous("zzz-")).toEqual("zzyz") - expect(cryptly.Identifier.previous("----")).toEqual("zzzz") - }) - it.each([4, 8, 12, 16, 64] as const)(`ordered id is`, (length: cryptly.Identifier.Length) => { - expect(cryptly.Identifier.is(cryptly.Identifier.generate(length, "ordered", length), length)).toBeTruthy() - }) - const time = 1691418818480 - it("pattern", () => { - expect(cryptly.Identifier.generate(4, "ordered", time)).toMatch(/^---0$/) - expect(cryptly.Identifier.generate(8, "ordered", time)).toMatch(/^---0XS0e$/) - expect(cryptly.Identifier.generate(12, "ordered", time)).toMatch(/^---0XS0exv[\w\d-_]{2}$/) - expect(cryptly.Identifier.generate(16, "ordered", time)).toMatch(/^---0XS0exv[\w\d-_]{6}$/) - expect(cryptly.Identifier.generate(64, "ordered", time)).toMatch(/^---0XS0exv[\w\d-_]{54}$/) - }) - it("pattern", () => { - expect(cryptly.Identifier.generate(4, "reversed", time)).toMatch(/^zzzy$/) - expect(cryptly.Identifier.generate(8, "reversed", time)).toMatch(/^zzzySXyK$/) - expect(cryptly.Identifier.generate(12, "reversed", time)).toMatch(/^zzzySXyK13z[\w\d-_]{1}$/) - expect(cryptly.Identifier.generate(16, "reversed", time)).toMatch(/^zzzySXyK13z[\w\d-_]{5}$/) - expect(cryptly.Identifier.generate(64, "reversed", time)).toMatch(/^zzzySXyK13z[\w\d-_]{53}$/) - }) - it.each([4, 8, 12, 16, 64] as const)(`ordered id is ordered`, (length: cryptly.Identifier.Length) => { - expect( - cryptly.Identifier.generate(length, "ordered", time + 1) >= cryptly.Identifier.generate(length, "ordered", time) - ).toBeTruthy() - }) - it.each([4, 8, 12, 16, 64] as const)(`reverse ordered id is`, (length: cryptly.Identifier.Length) => { - expect(cryptly.Identifier.is(cryptly.Identifier.generate(length, "reversed", length), length)).toBeTruthy() - }) - it.each([4, 8, 12, 16, 64] as const)(`reverse ordered id is reverse ordered`, (length: cryptly.Identifier.Length) => { - expect( - cryptly.Identifier.generate(length, "reversed", time + 1) <= cryptly.Identifier.generate(length, "reversed", time) - ).toBeTruthy() - }) - it("many ordered", () => { - expect( - cryptly.Identifier.generate(16, "ordered", time) < cryptly.Identifier.generate(16, "ordered", time + 1) && - cryptly.Identifier.generate(16, "ordered", time + 1) < - cryptly.Identifier.generate(16, "ordered", time + 21111) && - cryptly.Identifier.generate(16, "ordered", time + 2) < - cryptly.Identifier.generate(16, "ordered", time + 344546) && - cryptly.Identifier.generate(16, "ordered", time + 3) < - cryptly.Identifier.generate(16, "ordered", time + 41112) && - cryptly.Identifier.generate(16, "ordered", time + 4) < cryptly.Identifier.generate(16, "ordered", time + 5434) - ).toBeTruthy() - }) - it("many reversed1", () => { - expect( - cryptly.Identifier.generate(16, "reversed", time) > cryptly.Identifier.generate(16, "reversed", time + 1666) - ).toBeTruthy() - }) - it("many reversed2", () => { - expect( - cryptly.Identifier.generate(16, "reversed", time + 1) > cryptly.Identifier.generate(16, "reversed", time + 211111) - ).toBeTruthy() - }) - it("many reversed3", () => { - expect( - cryptly.Identifier.generate(16, "reversed", time + 2) > cryptly.Identifier.generate(16, "reversed", time + 32323) - ).toBeTruthy() - }) - it("many reversed4", () => { - expect( - cryptly.Identifier.generate(16, "reversed", time + 3) > cryptly.Identifier.generate(16, "reversed", time + 434) - ).toBeTruthy() - }) -}) diff --git a/Identifier/index.ts b/Identifier/index.ts index 2b67a61..bf77d7d 100644 --- a/Identifier/index.ts +++ b/Identifier/index.ts @@ -1,5 +1,5 @@ import { isly } from "isly" -import * as Base64 from "../Base64" +import { Base64 } from "../Base64" import { crypto } from "../crypto" import { Length as IdentifierLength } from "./Length" @@ -12,6 +12,21 @@ export namespace Identifier { return type.is(value) && (length == undefined || value.length == length) } export const flaw = type.flaw + export function convert(identifier: Identifier, length: Length, standard?: Base64.Standard): Identifier + export function convert(identifier: Identifier, from: Base64.Standard, to?: Base64.Standard): Identifier + export function convert( + identifier: Identifier, + length: Base64.Standard | Length, + standard: Base64.Standard = "url" + ): Identifier { + return Base64.Standard.is(length) + ? Base64.convert(identifier, length, standard) + : identifier.length < length + ? min(length).slice(0, length - identifier.length) + identifier + : identifier.length > length + ? identifier.slice(identifier.length - length) + : identifier + } export function fromUint24(value: number): Identifier { return fromHexadecimal(value.toString(16).padStart(6, "0")) } @@ -82,8 +97,4 @@ export namespace Identifier { export function previous(identifier: Identifier, decrement = 1): Identifier { return next(identifier, -decrement) } - /** - * @deprecated since version 4.0.5 - */ - export const length = Length.values } diff --git a/Key/Rsa.ts b/Key/Rsa.ts index a1fc4b2..d9521b8 100644 --- a/Key/Rsa.ts +++ b/Key/Rsa.ts @@ -1,4 +1,4 @@ -import * as Base64 from "../Base64" +import { Base64 } from "../Base64" import { crypto } from "../crypto" import { Hash } from "../Signer/Hash" @@ -10,12 +10,15 @@ export class Rsa { ) {} async export(format: "jwk"): Promise async export(format: "buffer"): Promise - async export(format?: "base64" | "pem"): Promise - async export(format: "jwk" | "buffer" | "base64" | "pem"): Promise + async export(format?: "pem"): Promise + async export(format?: "base64"): Promise + async export( + format: "jwk" | "buffer" | "base64" | "pem" + ): Promise async export( format: "jwk" | "buffer" | "base64" | "pem" = "base64" - ): Promise { - let result: JsonWebKey | ArrayBuffer | string | undefined + ): Promise { + let result: JsonWebKey | ArrayBuffer | string | Base64 | undefined switch (format) { case "jwk": result = await crypto.subtle.exportKey("jwk", this.raw) @@ -36,7 +39,7 @@ export class Rsa { data && [ `-----BEGIN ${this.type.toUpperCase()} KEY-----`, - ...slice(data, 64), + ...Base64.slice(data, 64), `-----END ${this.type.toUpperCase()} KEY-----`, ].join("\n") } @@ -46,11 +49,11 @@ export class Rsa { } static async import( type: "private" | "public", - key: ArrayBuffer | string | undefined, + key: ArrayBuffer | Base64 | undefined, variant?: Rsa.Variant, hash?: Hash ): Promise { - if (typeof key == "string") + if (Base64.is(key)) key = Base64.decode(key) const parameters = getParameters(variant) return ( @@ -93,8 +96,8 @@ export namespace Rsa { } export namespace Pair { export async function load( - publicKey: string | ArrayBuffer, - privateKey: string | ArrayBuffer + publicKey: Base64 | ArrayBuffer, + privateKey: Base64 | ArrayBuffer ): Promise> { return { public: await Rsa.import("public", publicKey), @@ -120,8 +123,3 @@ function getParameters(variant: Rsa.Variant | undefined): Rsa.Parameters { ? { name: "RSASSA-PKCS1-v1_5" } : { name: "RSA-OAEP" } } -function* slice(data: string, length: number): Generator { - let start = 0 - while (start < data.length) - yield data.slice(start, (start = start + length)) -} diff --git a/Otp/Generator/Settings.ts b/Otp/Generator/Settings.ts new file mode 100644 index 0000000..15d2617 --- /dev/null +++ b/Otp/Generator/Settings.ts @@ -0,0 +1,14 @@ +import { isoly } from "isoly" +import { Digester } from "../../Digester" + +export interface Settings { + length: 6 | 7 | 8 | 9 | 10 + interval: isoly.TimeSpan + epoch: isoly.DateTime + hash: Digester.Algorithm +} +export namespace Settings { + export function create(settings: Partial): Settings { + return { length: 6, interval: { seconds: 30 }, epoch: isoly.DateTime.create(0), hash: "SHA-1", ...settings } + } +} diff --git a/Otp/Generator/index.ts b/Otp/Generator/index.ts new file mode 100644 index 0000000..6956a63 --- /dev/null +++ b/Otp/Generator/index.ts @@ -0,0 +1,59 @@ +import { isoly } from "isoly" +import { Base16 } from "../../Base16" +import { Base32 } from "../../Base32" +import { Signer } from "../../Signer" +import type { Otp } from "../index" +import { Settings as GeneratorSettings } from "./Settings" + +export class Generator { + #signer: Signer | undefined + private get signer(): Signer { + return (this.#signer ??= Signer.create("HMAC", this.settings.hash, new TextEncoder().encode(this.secret))) + } + constructor(private readonly secret: string, public readonly settings: Readonly) {} + + async generate(counter: number, count: number): Promise + async generate(counter: number | isoly.DateTime): Promise + async generate(counters: number[]): Promise + async generate(times: isoly.DateTime[]): Promise + async generate( + counter: number | number[] | isoly.DateTime | isoly.DateTime[] = isoly.DateTime.now(), + count?: number + ): Promise { + let result: Otp | Otp[] + if (typeof counter == "number" && typeof count == "number") + result = await this.generate(Array.from({ length: count }, (_, i) => i + counter)) + else if (Array.isArray(counter)) + result = await Promise.all(counter.map(async t => await this.generate(t))) + else if (isoly.DateTime.is(counter)) + result = await this.generate( + Math.floor(isoly.DateTime.epoch(counter, "seconds") / isoly.TimeSpan.toSeconds(this.settings.interval)) + ) + else { + const hash = await this.signer.sign(Base16.decode(counter.toString(16).padStart(16, "0"))) + const offset = hash[hash.length - 1] & 0xf + const value = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff) + // magic numbers explanation: + // (value % 10^digits).padstart(digits, "0") + result = (value % +"1".padEnd(this.settings.length + 1, "0")).toString().padStart(this.settings.length, "0") + } + return result + } + /** for use in QR codes */ + toUrl(issuer: string, username: string): string { + return `otpauth://totp/${issuer}:${username}?secret=${Base32.encode(this.secret)}&issuer=${issuer}` + } + change(settings: Partial): Generator { + return new Generator(this.secret, { ...this.settings, ...settings }) + } + static create(secret: string, settings = {}): Generator { + return new Generator(secret, Generator.Settings.create(settings)) + } +} +export namespace Generator { + export import Settings = GeneratorSettings +} diff --git a/Otp/index.spec.ts b/Otp/index.spec.ts new file mode 100644 index 0000000..ac7cbb1 --- /dev/null +++ b/Otp/index.spec.ts @@ -0,0 +1,31 @@ +import { isoly } from "isoly" +import { cryptly } from "../index" + +describe("Authenticator", () => { + const generator = cryptly.Otp.Generator.create("12345678901234567890") + it("seconds to DateTime", async () => + expect(isoly.DateTime.create(1706101897967, "milliseconds")).toEqual("2024-01-24T13:11:37.967Z")) + it("DateTime to seconds", async () => + expect(isoly.DateTime.epoch("2024-01-24T13:11:37.967Z", "milliseconds")).toEqual(1706101897967)) + it("Generate 6 digits TOTP", async () => { + expect(await generator.generate("2024-01-24T13:11:07.967Z")).toEqual("727592") + expect(await generator.generate("2024-01-24T13:11:37.967Z")).toEqual("097958") + }) + it("as URL for QR Code", async () => + expect(generator.toUrl("TestApp", "test.testsson@test.test")).toEqual( + "otpauth://totp/TestApp:test.testsson@test.test?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=TestApp" + )) + it("Generate 8 digits TOTP", async () => { + const g = generator.change({ length: 8 }) + expect(await g.generate("2024-01-24T13:11:07.967Z")).toEqual("22727592") + expect(await g.generate("2024-01-24T13:11:37.967Z")).toEqual("30097958") + }) + it("old backup codes", async () => + expect( + await generator + .change({ length: 8 }) + .generate([123456, 234567, 345678].map(t => isoly.DateTime.create(t, "milliseconds"))) + ).toEqual(["40338314", "82162583", "43481090"])) + it("correct backup codes", async () => + expect(await generator.change({ length: 8 }).generate(1, 3)).toEqual(["94287082", "37359152", "26969429"])) +}) diff --git a/Otp/index.ts b/Otp/index.ts new file mode 100644 index 0000000..de09d1b --- /dev/null +++ b/Otp/index.ts @@ -0,0 +1,11 @@ +import { isly } from "isly" +import { Generator as OtpGenerator } from "./Generator" + +export type Otp = string + +export namespace Otp { + export import Generator = OtpGenerator + export const type = isly.named("cryptly.Otp", isly.string(/\d{6,10}/)) + export const is = type.is + export const flaw = type.flaw +} diff --git a/Password/Hash.ts b/Password/Hash.ts index 777147f..6208d8f 100644 --- a/Password/Hash.ts +++ b/Password/Hash.ts @@ -1,11 +1,12 @@ import { isly } from "isly" +import { Base64 } from "../Base64" export interface Hash { - hash: string - salt: string + hash: Base64 + salt: Base64 } export namespace Hash { - export const type = isly.object({ hash: isly.string(), salt: isly.string() }, "cryptly.Password.Hash") + export const type = isly.object({ hash: Base64.type, salt: Base64.type }, "cryptly.Password.Hash") export const is = type.is export const flaw = type.flaw } diff --git a/Password/index.spec.ts b/Password/index.spec.ts index fdd8fb1..97ab299 100644 --- a/Password/index.spec.ts +++ b/Password/index.spec.ts @@ -12,7 +12,7 @@ describe("Password", () => { }) it("verify", async () => { expect( - await cryptly.Password.verify(cryptly.Signer.create("HMAC", "SHA-512", "secret-pepper"), hash, "password") + await cryptly.Password.verify(cryptly.Signer.create("HMAC", "SHA-512", "secret-pepper"), "password", hash) ).toBeTruthy() }) }) diff --git a/Password/index.ts b/Password/index.ts index fe2a879..228ca4c 100644 --- a/Password/index.ts +++ b/Password/index.ts @@ -1,6 +1,5 @@ import { isly } from "isly" -import * as Base64 from "../Base64" -import { crypto } from "../crypto" +import { Base64 } from "../Base64" import { Hash as PasswordHash } from "./Hash" export type Password = string | Password.Hash @@ -13,26 +12,20 @@ export namespace Password { export async function hash( algorithm: { sign: (data: string) => Promise }, password: string, - salt?: string + salt?: string, + pepper = "" ): Promise { - if (!salt) - salt = Base64.encode(crypto.getRandomValues(new Uint8Array(64))) return { - hash: await algorithm.sign(salt + password), + hash: await algorithm.sign(pepper + (salt ??= Base64.random(64)) + password), salt, } } export async function verify( algorithm: { sign: (data: string) => Promise }, + password: string, hash: Hash, - password: string + pepper = "" ): Promise { - return (await Password.hash(algorithm, password, hash.salt)).hash == hash.hash - } - export namespace Hashed { - /** - * @deprecated since version 4.0.5 - */ - export const is = Hash.type.is + return (await Password.hash(algorithm, password, hash.salt, pepper)).hash == hash.hash } } diff --git a/RandomValue.ts b/RandomValue.ts deleted file mode 100644 index cedbde2..0000000 --- a/RandomValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { crypto } from "./crypto" - -export namespace RandomValue { - export function generate< - T extends - | Int8Array - | Int16Array - | Int32Array - | Uint8Array - | Uint16Array - | Uint32Array - | Uint8ClampedArray - | Float32Array - | Float64Array - | DataView - | null - >(array: T): T { - return crypto.getRandomValues(array) - } -} diff --git a/Signer/Base.ts b/Signer/Base.ts index c0cd3e4..91abbfb 100644 --- a/Signer/Base.ts +++ b/Signer/Base.ts @@ -1,17 +1,16 @@ -import * as Base64 from "../Base64" -import { TextEncoder } from "../TextEncoder" +import { Base64 } from "../Base64" export abstract class Base { async sign(data: Uint8Array): Promise - async sign(data: string): Promise - async sign(data: string | Uint8Array): Promise { + async sign(data: string): Promise + async sign(data: string | Uint8Array): Promise { return typeof data == "string" ? Base64.encode(await this.signBinary(new TextEncoder().encode(data)), "url") : this.signBinary(data) } protected abstract signBinary(data: Uint8Array): Promise - verify(data: string | Uint8Array, signature: string | Uint8Array): Promise { - if (typeof signature == "string") + verify(data: string | Uint8Array, signature: Base64 | Uint8Array): Promise { + if (Base64.is(signature)) signature = Base64.decode(signature, "url") return typeof data == "string" ? this.verifyBinary(new TextEncoder().encode(data), signature) diff --git a/Signer/ECDSA.ts b/Signer/Ecdsa.ts similarity index 84% rename from Signer/ECDSA.ts rename to Signer/Ecdsa.ts index 48fe2bb..522322d 100644 --- a/Signer/ECDSA.ts +++ b/Signer/Ecdsa.ts @@ -1,19 +1,19 @@ -import * as Base64 from "../Base64" +import { Base64 } from "../Base64" import { crypto } from "../crypto" import { Base } from "./Base" import { Hash } from "./Hash" -export class ECDSA extends Base { +export class Ecdsa extends Base { private publicKey: PromiseLike private privateKey: PromiseLike constructor( private readonly hash: Hash, - publicKey: Uint8Array | string | undefined, - privateKey?: Uint8Array | string + publicKey: Uint8Array | Base64 | undefined, + privateKey?: Uint8Array | Base64 ) { super() if (publicKey) { - if (typeof publicKey == "string") + if (Base64.is(publicKey)) publicKey = Base64.decode(publicKey) this.publicKey = crypto.subtle.importKey( "spki", @@ -24,7 +24,7 @@ export class ECDSA extends Base { ) } if (privateKey) { - if (typeof privateKey == "string") + if (Base64.is(privateKey)) privateKey = Base64.decode(privateKey) this.privateKey = crypto.subtle.importKey( "pkcs8", diff --git a/Signer/HMAC.ts b/Signer/Hmac.ts similarity index 72% rename from Signer/HMAC.ts rename to Signer/Hmac.ts index 1f520ed..db5ff3a 100644 --- a/Signer/HMAC.ts +++ b/Signer/Hmac.ts @@ -1,13 +1,13 @@ -import * as Base64 from "../Base64" +import { Base64 } from "../Base64" import { crypto } from "../crypto" import { Hash } from "./Hash" import { Symmetric } from "./Symmetric" -export class HMAC extends Symmetric { +export class Hmac extends Symmetric { private key: PromiseLike - constructor(private readonly hash: Hash, key: Uint8Array | string) { + constructor(private readonly hash: Hash, key: Uint8Array | Base64) { super() - if (typeof key == "string") + if (Base64.is(key)) key = Base64.decode(key, "url") this.key = crypto.subtle.importKey("raw", key, { name: "HMAC", hash: { name: hash } }, false, ["sign", "verify"]) } diff --git a/Signer/Rsa.ts b/Signer/Rsa.ts index fabec7b..61036e9 100644 --- a/Signer/Rsa.ts +++ b/Signer/Rsa.ts @@ -1,3 +1,4 @@ +import { Base64 } from "../Base64" import { crypto } from "../crypto" import { Key } from "../Key" import { Base } from "./Base" @@ -17,19 +18,20 @@ export class Rsa extends Base { } async export(type: "private" | "public", format: "jwk"): Promise async export(type: "private" | "public", format: "buffer"): Promise - async export(type: "private" | "public", format?: "base64" | "pem"): Promise + async export(type: "private" | "public", format?: "pem"): Promise + async export(type: "private" | "public", format?: "base64"): Promise async export( type: "private" | "public", format: "jwk" | "buffer" | "base64" | "pem" = "base64" - ): Promise { + ): Promise { const key = (await this.keys)[type] return key?.export(format) } static import( variant: Key.Rsa.Variant, hash: Hash, - publicKey: Uint8Array | string | undefined, - privateKey?: Uint8Array | string + publicKey: Uint8Array | Base64 | undefined, + privateKey?: Uint8Array | Base64 ): Rsa { return new Rsa( Promise.all([ diff --git a/Signer/Symmetric.ts b/Signer/Symmetric.ts index 7206c19..4c2d39d 100644 --- a/Signer/Symmetric.ts +++ b/Signer/Symmetric.ts @@ -1,4 +1,4 @@ -import * as Base64 from "../Base64" +import { Base64 } from "../Base64" import { Base } from "./Base" export abstract class Symmetric extends Base { diff --git a/Signer/index.spec.ts b/Signer/index.spec.ts index d8d554e..db22081 100644 --- a/Signer/index.spec.ts +++ b/Signer/index.spec.ts @@ -14,11 +14,11 @@ const privateKey = "eieQppqI61xxLBVPhKf9l4eaBpVLGwIhAL3xxhKrp1jQiAIhaEmR1Zpft6LPXy5a" + "gQFvTAQYTjgh" -const publicKeyECDSA521 = +const publicKeyEcdsa521 = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE5FCaqhKm7af7Jar/qw1ii5ayIMoTOwlD" + "LsKyXQyOio2di2hWvVRpBVs3ESLi85Sk2YqLKUh1JOGW+KQGpcAPTg==" -const privateKeyECDSA521 = +const privateKeyEcdsa521 = "MHQCAQEEIEGmJObKJLXhUXkWZbdtZ1Whe5WKojkcaDFo6TvaH8JBoAcGBSuBBAAK" + "oUQDQgAE5FCaqhKm7af7Jar/qw1ii5ayIMoTOwlDLsKyXQyOio2di2hWvVRpBVs3" + "ESLi85Sk2YqLKUh1JOGW+KQGpcAPTg==" @@ -32,7 +32,7 @@ const publicKeyPSS = "V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9" + "MwIDAQAB" -const privateKeyPSS = +const privateKeyPss = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfPKKzVmN80HRs" + "GAoUxK++RO3CW8GxomrtLnAD6TN5U5WlVbCRZ1WFrizfxcz+lr/Kvjtq/v7PdVOa" + "8NHIAdxpP3bCFEQWku/1yPmVN4lKJvKv8yub9i2MJlVaBo5giHCtfAouo+v/XWKd" + @@ -67,7 +67,7 @@ describe("Signer", () => { expect(await signer.verify("Some string", signature)).toEqual(true) }) it("Create HMAC", async () => { - const signer = Signer.create("HMAC", "SHA-256", "Super Secret Encryption Key") + const signer = Signer.create("HMAC", "SHA-256", "Super-Secret-Encryption-Key") const signature = await signer.sign("Some string") expect(await signer.verify("Some string", signature)).toEqual(true) }) @@ -89,7 +89,7 @@ describe("Signer", () => { expect(await signer.verify("Some string", signature)).toEqual(true) }) it("Create RSA-PSS from private Key", async () => { - const signer = Signer.create("RSA-PSS", "SHA-256", publicKeyPSS, privateKeyPSS) + const signer = Signer.create("RSA-PSS", "SHA-256", publicKeyPSS, privateKeyPss) const signature = await signer.sign("Some string") expect(await signer.verify("Some string", signature)).toEqual(true) }) @@ -102,7 +102,7 @@ describe("Signer", () => { it.skip("Create ECDSA 512", async () => { // does not work currently on Node - const signer = Signer.create("ECDSA", "SHA-512", publicKeyECDSA521, privateKeyECDSA521) + const signer = Signer.create("ECDSA", "SHA-512", publicKeyEcdsa521, privateKeyEcdsa521) const signature = await signer.sign("Some string") expect(await signer.verify("Some string", signature)).toEqual(true) }) diff --git a/Signer/index.ts b/Signer/index.ts index db65b6c..7169a8c 100644 --- a/Signer/index.ts +++ b/Signer/index.ts @@ -1,8 +1,9 @@ +import { Base64 } from "../Base64" import { Algorithm as SignerAlgorithm } from "./Algorithm" import { Base } from "./Base" -import { ECDSA } from "./ECDSA" +import { Ecdsa } from "./Ecdsa" import { Hash as SignerHash } from "./Hash" -import { HMAC } from "./HMAC" +import { Hmac } from "./Hmac" import { None } from "./None" import { Rsa } from "./Rsa" @@ -32,38 +33,38 @@ export namespace Signer { return result } export function create(algorithm: "None"): Signer - export function create(algorithm: "HMAC", hash: SignerHash, key: string | Uint8Array): Signer - export function create(algorithm: "RSA", hash: SignerHash, publicKey: string | Uint8Array): Rsa - export function create(algorithm: "RSA-PSS", hash: SignerHash, publicKey: string | Uint8Array): Rsa - export function create(algorithm: "ECDSA", hash: SignerHash, publicKey: string | Uint8Array): Signer + export function create(algorithm: "HMAC", hash: SignerHash, key: Base64 | Uint8Array): Signer + export function create(algorithm: "RSA", hash: SignerHash, publicKey: Base64 | Uint8Array): Rsa + export function create(algorithm: "RSA-PSS", hash: SignerHash, publicKey: Base64 | Uint8Array): Rsa + export function create(algorithm: "ECDSA", hash: SignerHash, publicKey: Base64 | Uint8Array): Signer export function create( algorithm: "RSA", hash: SignerHash, - publicKey: string | Uint8Array | undefined, - privateKey: string | Uint8Array | undefined + publicKey: Base64 | Uint8Array | undefined, + privateKey: Base64 | Uint8Array | undefined ): Rsa export function create( algorithm: "RSA-PSS", hash: SignerHash, - publicKey: string | Uint8Array | undefined, - privateKey: string | Uint8Array | undefined + publicKey: Base64 | Uint8Array | undefined, + privateKey: Base64 | Uint8Array | undefined ): Rsa export function create( algorithm: "ECDSA", hash: SignerHash, - publicKey: string | Uint8Array | undefined, - privateKey: string | Uint8Array | undefined + publicKey: Base64 | Uint8Array | undefined, + privateKey: Base64 | Uint8Array | undefined ): Signer export function create( algorithm: SignerAlgorithm | "None", hash?: SignerHash | undefined, - ...keys: (string | Uint8Array)[] + ...keys: (Base64 | Uint8Array)[] ): Signer | undefined { let result: Signer | undefined if (hash != undefined) switch (algorithm) { case "HMAC": - result = new HMAC(hash, keys[0]) + result = new Hmac(hash, keys[0]) break case "RSA": result = Rsa.import("SSA", hash, keys[0], keys[1]) @@ -72,7 +73,7 @@ export namespace Signer { result = Rsa.import("PSS", hash, keys[0], keys[1]) break case "ECDSA": - result = new ECDSA(hash, keys[0], keys[1]) + result = new Ecdsa(hash, keys[0], keys[1]) break } else if (algorithm == "None") diff --git a/TextDecoder.ts b/TextDecoder.ts deleted file mode 100644 index 4f70004..0000000 --- a/TextDecoder.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class TextDecoder { - readonly encoding = "utf-8" - decode(view: ArrayBufferView | undefined, options?: { stream?: boolean }): string { - return !view - ? "" - : decodeURIComponent( - escape( - Array.from(new Uint8Array(view.buffer, view.byteOffset, view.byteLength), c => String.fromCharCode(c)).join( - "" - ) - ) - ) - } -} diff --git a/TextEncoder.ts b/TextEncoder.ts deleted file mode 100644 index 7c1a692..0000000 --- a/TextEncoder.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class TextEncoder { - readonly encoding = "utf-8" - encode(data: string): Uint8Array { - return Uint8Array.from(unescape(encodeURIComponent(data)).split(""), c => c.charCodeAt(0)) - } -} diff --git a/Tuple.ts b/Tuple.ts deleted file mode 100644 index 6d4c61c..0000000 --- a/Tuple.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Workaround for ESLint parsing issues with Tuples -export type Tuple2 = [T1, T2] diff --git a/authenticator.spec.ts b/authenticator.spec.ts deleted file mode 100644 index c1404ec..0000000 --- a/authenticator.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { cryptly } from "./index" - -describe("authenticator", () => { - const key = "12345678901234567890" - it("HS256 authenticator", async () => { - const time = 1706101897967 - expect(await cryptly.authenticator.generate(key, time - 30000)).toEqual("727592") - expect(await cryptly.authenticator.generate(key, time)).toEqual("097958") - expect(cryptly.authenticator.toQrCode(key, "TestApp", "test.testsson@test.test")).toEqual( - "otpauth://totp/TestApp:test.testsson@test.test?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=TestApp" - ) - expect(await cryptly.authenticator.generate(key, time - 30000, 8)).toEqual("22727592") - expect(await cryptly.authenticator.generate(key, time, 8)).toEqual("30097958") - }) - it("Backup codes", async () => { - const times = [123456, 234567, 345678] - const backups = await cryptly.authenticator.generateRecoveryCodes(key, times) - expect(backups).toEqual(["40338314", "82162583", "43481090"]) - }) -}) diff --git a/authenticator.ts b/authenticator.ts deleted file mode 100644 index a8dc771..0000000 --- a/authenticator.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Base16 from "./Base16" -import * as Base32 from "./Base32" -import { Signer } from "./Signer" - -export namespace authenticator { - export async function generate(key: string, time: number, length = 6): Promise { - const hash = await Signer.create("HMAC", "SHA-1", new TextEncoder().encode(key)).sign( - Base16.decode( - Math.floor(time / 1000 / 30) - .toString(16) - .padStart(16, "0") - ) - ) - const offset = hash[hash.length - 1] & 0xf - const value = - ((hash[offset] & 0x7f) << 24) | - ((hash[offset + 1] & 0xff) << 16) | - ((hash[offset + 2] & 0xff) << 8) | - (hash[offset + 3] & 0xff) - // magic numbers explanation: - // (value % 10^digits).padstart(digits, "0") - return (value % +"1".padEnd(length + 1, "0")).toString().padStart(length, "0") - } - export async function generateRecoveryCodes(key: string, times: number[]): Promise { - const result = Array(times.length) - for (let index = 0; index < times.length; index++) { - result[index] = await generate(key, times[index], 8) - } - return result - } - export function toQrCode(key: string, issuer: string, username: string): string { - return `otpauth://totp/${issuer}:${username}?secret=${Base32.encode(key)}&issuer=${issuer}` - } -} diff --git a/cryptly.ts b/cryptly.ts deleted file mode 100644 index 7afb827..0000000 --- a/cryptly.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * as ArrayBuffer from "./ArrayBuffer" -export { authenticator } from "./authenticator" -export { Encrypter } from "./Encrypter" -export { Encrypters } from "./Encrypters" -export * as Base16 from "./Base16" -export * as Base32 from "./Base32" -export * as Base64 from "./Base64" -export { Digester } from "./Digester" -export { Identifier } from "./Identifier" -export { Password } from "./Password" -export { RandomValue } from "./RandomValue" -export { Signer } from "./Signer" -export { TextDecoder } from "./TextDecoder" -export { TextEncoder } from "./TextEncoder" diff --git a/index.ts b/index.ts index ef4b499..da8536e 100644 --- a/index.ts +++ b/index.ts @@ -1 +1,25 @@ -export * as cryptly from "./cryptly" +import { ArrayBuffer as cryptlyArrayBuffer } from "./ArrayBuffer" +import { Base16 as cryptlyBase16 } from "./Base16" +import { Base32 as cryptlyBase32 } from "./Base32" +import { Base64 as cryptlyBase64 } from "./Base64" +import { Digester as cryptlyDigester } from "./Digester" +import { Encrypter as cryptlyEncrypter } from "./Encrypter" +import { Encrypters as cryptlyEncrypters } from "./Encrypters" +import { Identifier as cryptlyIdentifier } from "./Identifier" +import { Otp as cryptlyOtp } from "./Otp" +import { Password as cryptlyPassword } from "./Password" +import { Signer as cryptlySigner } from "./Signer" + +export namespace cryptly { + export import ArrayBuffer = cryptlyArrayBuffer + export import Base16 = cryptlyBase16 + export import Base32 = cryptlyBase32 + export import Base64 = cryptlyBase64 + export import Digester = cryptlyDigester + export import Encrypter = cryptlyEncrypter + export import Encrypters = cryptlyEncrypters + export import Identifier = cryptlyIdentifier + export import Password = cryptlyPassword + export import Signer = cryptlySigner + export import Otp = cryptlyOtp +} diff --git a/package-lock.json b/package-lock.json index 7df5bcb..82fceeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "cryptly", - "version": "4.0.6", + "version": "5.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cryptly", - "version": "4.0.6", + "version": "5.0.0", "license": "MIT", "dependencies": { - "isly": "^0.1.20" + "isly": "^0.1.20", + "isoly": "^2.3.11" }, "devDependencies": { "@types/jest": "^29.5.13", @@ -2490,10 +2491,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3733,6 +3735,12 @@ "integrity": "sha512-biYKDiE1LiIqpdOWfobqu8VaZ2Lwj9Hs9IFs7VaeVV8Hsd/WbUqyCdAO2imNUWOSfb2nSDKlIHa/z05USBX6bw==", "license": "MIT" }, + "node_modules/isoly": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/isoly/-/isoly-2.3.11.tgz", + "integrity": "sha512-81pWrkWsGUnUbEZ4YknE67m+4LMrohVlXaqK7tvrO8n0/85bgxCwqAWzbMnlc8O+xd3joxzjXxjvoO2XRY3cNw==", + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", diff --git a/package.json b/package.json index 50324bc..b28565d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cryptly", - "version": "4.0.6", + "version": "5.0.0", "description": "Fullstack encryption library.", "author": "Utily Contributors", "license": "MIT", @@ -81,6 +81,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "isly": "^0.1.20" + "isly": "^0.1.20", + "isoly": "^2.3.11" } }