crypto.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * @module lib/crypto
  3. * This module contains various cryptographic functions using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#table-of-contents)
  4. */
  5. import { getUnsafeWindow } from "./dom.js";
  6. import { mapRange, randRange } from "./math.js";
  7. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
  8. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  9. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
  10. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  11. /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string or ArrayBuffer, depending on what {@linkcode outputType} is set to */
  12. export async function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  13. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  14. const comp = new CompressionStream(compressionFormat);
  15. const writer = comp.writable.getWriter();
  16. writer.write(byteArray);
  17. writer.close();
  18. const buf = await (new Response(comp.readable).arrayBuffer());
  19. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  20. }
  21. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string */
  22. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
  23. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
  24. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
  25. /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string or ArrayBuffer, depending on what {@linkcode outputType} is set to */
  26. export async function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "string" | "arrayBuffer" = "string"): Promise<ArrayBuffer | string> {
  27. const byteArray = typeof input === "string" ? str2ab(input) : input;
  28. const decomp = new DecompressionStream(compressionFormat);
  29. const writer = decomp.writable.getWriter();
  30. writer.write(byteArray);
  31. writer.close();
  32. const buf = await (new Response(decomp.readable).arrayBuffer());
  33. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  34. }
  35. /** Converts an ArrayBuffer to a base64-encoded string */
  36. function ab2str(buf: ArrayBuffer): string {
  37. return getUnsafeWindow().btoa(
  38. new Uint8Array(buf)
  39. .reduce((data, byte) => data + String.fromCharCode(byte), "")
  40. );
  41. }
  42. /** Converts a base64-encoded string to an ArrayBuffer representation of its bytes */
  43. function str2ab(str: string): ArrayBuffer {
  44. return Uint8Array.from(getUnsafeWindow().atob(str), c => c.charCodeAt(0));
  45. }
  46. /**
  47. * Creates a hash / checksum of the given {@linkcode input} string or ArrayBuffer using the specified {@linkcode algorithm} ("SHA-256" by default).
  48. *
  49. * - ⚠️ Uses the SubtleCrypto API so it needs to run in a secure context (HTTPS).
  50. * - ⚠️ If you use this for cryptography, make sure to use a secure algorithm (under no circumstances use SHA-1) and to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) your input data.
  51. */
  52. export async function computeHash(input: string | ArrayBuffer, algorithm = "SHA-256"): Promise<string> {
  53. let data: ArrayBuffer;
  54. if(typeof input === "string") {
  55. const encoder = new TextEncoder();
  56. data = encoder.encode(input);
  57. }
  58. else
  59. data = input;
  60. const hashBuffer = await crypto.subtle.digest(algorithm, data);
  61. const hashArray = Array.from(new Uint8Array(hashBuffer));
  62. const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
  63. return hashHex;
  64. }
  65. /**
  66. * Generates a random ID with the specified length and radix (16 characters and hexadecimal by default)
  67. *
  68. * - ⚠️ Not suitable for generating anything related to cryptography! Use [SubtleCrypto's `generateKey()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey) for that instead.
  69. * @param length The length of the ID to generate (defaults to 16)
  70. * @param radix The [radix](https://en.wikipedia.org/wiki/Radix) of each digit (defaults to 16 which is hexadecimal. Use 2 for binary, 10 for decimal, 36 for alphanumeric, etc.)
  71. * @param enhancedEntropy If set to true, uses [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) for better cryptographic randomness (this also makes it take longer to generate)
  72. * @param randomCase If set to false, the generated ID will be lowercase only - also makes use of the `enhancedEntropy` parameter unless the output doesn't contain letters
  73. */
  74. export function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true): string {
  75. if(radix < 2 || radix > 36)
  76. throw new RangeError("The radix argument must be between 2 and 36");
  77. let arr: string[] = [];
  78. const caseArr = randomCase ? [0, 1] : [0];
  79. if(enhancedEntropy) {
  80. const uintArr = new Uint8Array(length);
  81. crypto.getRandomValues(uintArr);
  82. arr = Array.from(
  83. uintArr,
  84. (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1),
  85. );
  86. }
  87. else {
  88. arr = Array.from(
  89. { length },
  90. () => Math.floor(Math.random() * radix).toString(radix),
  91. );
  92. }
  93. if(!arr.some((v) => /[a-zA-Z]/.test(v)))
  94. return arr.join("");
  95. return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join("");
  96. }