mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-13 20:57:39 +02:00
Merge pull request #49 from open-reception/43-add-encryption-utils-to-project
43 add encryption utils to project
This commit is contained in:
Vendored
+16
-1
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.requireConfig": true
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export default ts.config(
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
ignores: ["static/**"]
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
|
||||
Generated
+85
@@ -9,13 +9,18 @@
|
||||
"version": "0.0.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/post-quantum": "^0.4.1",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"argon2": "^0.43.0",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"mode-watcher": "^1.0.8",
|
||||
"nodemailer": "^7.0.3",
|
||||
"postgres": "^3.4.7",
|
||||
"secrets.js-34r7h": "^2.0.2",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1427,6 +1432,30 @@
|
||||
"svelte": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/post-quantum": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.4.1.tgz",
|
||||
"integrity": "sha512-TRXjvnY9jAFNWbxOx+pKt21BNsCEWKFjMbIKwdx9CQXBudDanpY20EfOcooV7DIsRS/+Mf8D8utpUPjfGrQ8fA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1473,6 +1502,15 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
|
||||
@@ -2898,6 +2936,27 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argon2": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.43.0.tgz",
|
||||
"integrity": "sha512-u/HKLcbWShVDhkfwI4hWyiUf3qyX8QhTfaIv2cWE18uqhXCmR5hb6Ed7oqYi2KCQegeAnRhiFzbjzm7i5yl1GA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@phc/format": "^1.0.0",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argon2-browser": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -4994,6 +5053,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz",
|
||||
"integrity": "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
|
||||
@@ -5764,6 +5843,12 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/secrets.js-34r7h": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/secrets.js-34r7h/-/secrets.js-34r7h-2.0.2.tgz",
|
||||
"integrity": "sha512-NwB4tLBs7soL6lYgGslTVllvMVEnSOjw79xI/yy4DAg86FXkVe1cLLMfhqreLgwY/zmGryqn5njYOGRjKuZzUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
||||
@@ -69,13 +69,18 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/post-quantum": "^0.4.1",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"argon2": "^0.43.0",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"mode-watcher": "^1.0.8",
|
||||
"nodemailer": "^7.0.3",
|
||||
"postgres": "^3.4.7",
|
||||
"secrets.js-34r7h": "^2.0.2",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { OptimizedArgon2 } from "./hashing";
|
||||
import type { Argon2Options } from "./hashing";
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("$lib/logger", () => ({
|
||||
logger: {
|
||||
setContext: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
describe("OptimizedArgon2", () => {
|
||||
// Test data
|
||||
const testPin = "123456";
|
||||
const testClientId = "test-client-id";
|
||||
const testEmail = "test@example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("deriveKeyFromPIN()", () => {
|
||||
it("should derive a key with default options", async () => {
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32); // Default hash length
|
||||
});
|
||||
|
||||
it("should derive a key with custom options", async () => {
|
||||
const options: Argon2Options = {
|
||||
memoryCost: 32768,
|
||||
timeCost: 5,
|
||||
parallelism: 2,
|
||||
hashLength: 64
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
|
||||
it("should produce consistent results for same inputs", async () => {
|
||||
const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
|
||||
const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
|
||||
|
||||
expect(result1).toEqual(result2);
|
||||
});
|
||||
|
||||
it("should produce different results for different PINs", async () => {
|
||||
const result1 = await OptimizedArgon2.deriveKeyFromPIN("123456", testClientId);
|
||||
const result2 = await OptimizedArgon2.deriveKeyFromPIN("654321", testClientId);
|
||||
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
|
||||
it("should produce different results for different client IDs", async () => {
|
||||
const result1 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client1");
|
||||
const result2 = await OptimizedArgon2.deriveKeyFromPIN(testPin, "client2");
|
||||
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
|
||||
it("should handle empty PIN", async () => {
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN("", testClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle empty client ID", async () => {
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, "");
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle unicode characters in PIN", async () => {
|
||||
const unicodePin = "тест🔐äöü";
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(unicodePin, testClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle unicode characters in client ID", async () => {
|
||||
const unicodeClientId = "клиент🆔äöü";
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, unicodeClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createClientIdAsync()", () => {
|
||||
it("should create consistent client ID for same email", async () => {
|
||||
const result1 = await OptimizedArgon2.createClientIdAsync(testEmail);
|
||||
const result2 = await OptimizedArgon2.createClientIdAsync(testEmail);
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(typeof result1).toBe("string");
|
||||
expect(result1.length).toBe(64); // SHA-256 hex = 64 chars
|
||||
});
|
||||
|
||||
it("should create different client IDs for different emails", async () => {
|
||||
const result1 = await OptimizedArgon2.createClientIdAsync("user1@example.com");
|
||||
const result2 = await OptimizedArgon2.createClientIdAsync("user2@example.com");
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it("should handle email case insensitivity", async () => {
|
||||
const result1 = await OptimizedArgon2.createClientIdAsync("Test@Example.Com");
|
||||
const result2 = await OptimizedArgon2.createClientIdAsync("test@example.com");
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it("should handle empty email", async () => {
|
||||
const result = await OptimizedArgon2.createClientIdAsync("");
|
||||
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
|
||||
it("should handle unicode in email", async () => {
|
||||
const unicodeEmail = "тест@example.com";
|
||||
const result = await OptimizedArgon2.createClientIdAsync(unicodeEmail);
|
||||
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
|
||||
it("should produce valid hex string", async () => {
|
||||
const result = await OptimizedArgon2.createClientIdAsync(testEmail);
|
||||
|
||||
expect(result).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createClientId() - Browser only", () => {
|
||||
it("should throw error in Node.js environment", async () => {
|
||||
// This test runs in Node.js environment by default
|
||||
await expect(OptimizedArgon2.createClientId(testEmail)).rejects.toThrow(
|
||||
"createClientId should be called asynchronously in Node.js environment"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getImplementationInfo()", () => {
|
||||
it("should return implementation information", async () => {
|
||||
const info = await OptimizedArgon2.getImplementationInfo();
|
||||
|
||||
expect(typeof info).toBe("string");
|
||||
expect(info.length).toBeGreaterThan(0);
|
||||
// In Node.js environment, it should try native argon2 first
|
||||
expect(info).toMatch(/(Native Node\.js argon2|@noble\/hashes)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases and error handling", () => {
|
||||
it("should handle very long PIN", async () => {
|
||||
const longPin = "a".repeat(1000);
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(longPin, testClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle very long client ID", async () => {
|
||||
const longClientId = "b".repeat(1000);
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, longClientId);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle minimum hash length", async () => {
|
||||
const options: Argon2Options = {
|
||||
hashLength: 4 // Noble hashes requires minimum 4 bytes
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should handle large hash length", async () => {
|
||||
const options: Argon2Options = {
|
||||
hashLength: 128
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(128);
|
||||
});
|
||||
|
||||
it("should handle minimum memory cost", async () => {
|
||||
const options: Argon2Options = {
|
||||
memoryCost: 8 // 8 KB minimum
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle minimum time cost", async () => {
|
||||
const options: Argon2Options = {
|
||||
timeCost: 1
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should handle different parallelism values", async () => {
|
||||
const options: Argon2Options = {
|
||||
parallelism: 4
|
||||
};
|
||||
|
||||
const result = await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance characteristics", () => {
|
||||
it("should complete within reasonable time with default options", async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete within 5 seconds even on slow systems
|
||||
expect(duration).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
it("should handle concurrent derivations", async () => {
|
||||
const promises = Array.from({ length: 5 }, (_, i) =>
|
||||
OptimizedArgon2.deriveKeyFromPIN(`pin${i}`, `client${i}`)
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
expect(results).toHaveLength(5);
|
||||
results.forEach((result, i) => {
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(32);
|
||||
|
||||
// Each result should be different
|
||||
results.slice(i + 1).forEach((otherResult) => {
|
||||
expect(result).not.toEqual(otherResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Consistency across implementations", () => {
|
||||
it("should produce consistent results regardless of implementation fallback", async () => {
|
||||
// Test that the fallback implementation produces consistent results
|
||||
const options: Argon2Options = {
|
||||
memoryCost: 1024, // Low memory to ensure fallback might be used
|
||||
timeCost: 1,
|
||||
parallelism: 1,
|
||||
hashLength: 32
|
||||
};
|
||||
|
||||
const results = await Promise.all([
|
||||
OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
|
||||
OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options),
|
||||
OptimizedArgon2.deriveKeyFromPIN(testPin, testClientId, options)
|
||||
]);
|
||||
|
||||
expect(results[0]).toEqual(results[1]);
|
||||
expect(results[1]).toEqual(results[2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Optimized Argon2 Implementation
|
||||
*
|
||||
* Uses native Node.js argon2 for maximum performance
|
||||
* Falls back to argon2-browser WASM in browsers
|
||||
* Final fallback to @noble/hashes if neither available
|
||||
*/
|
||||
|
||||
import type { CryptoBuffer } from "./utils";
|
||||
import { BufferUtils } from "./utils";
|
||||
import { logger } from "$lib/logger";
|
||||
|
||||
/**
|
||||
* Configuration options for Argon2 hashing
|
||||
*/
|
||||
export interface Argon2Options {
|
||||
/** Memory cost parameter (default: 65536) */
|
||||
memoryCost?: number;
|
||||
/** Time cost parameter (default: 10) */
|
||||
timeCost?: number;
|
||||
/** Parallelism parameter (default: 1) */
|
||||
parallelism?: number;
|
||||
/** Hash output length in bytes (default: 32) */
|
||||
hashLength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized Argon2 implementation with cross-platform support
|
||||
* Uses native Node.js argon2 for maximum performance in server environments
|
||||
* Falls back to argon2-browser WASM in browsers
|
||||
* Final fallback to @noble/hashes if neither is available
|
||||
*/
|
||||
export class OptimizedArgon2 {
|
||||
/**
|
||||
* Derives a cryptographic key from a PIN and client ID using Argon2
|
||||
* @param pin - The PIN to hash
|
||||
* @param clientId - The client identifier used as salt material
|
||||
* @param options - Optional Argon2 parameters
|
||||
* @returns Promise resolving to the derived key
|
||||
*/
|
||||
static async deriveKeyFromPIN(
|
||||
pin: string,
|
||||
clientId: string,
|
||||
options: Argon2Options = {}
|
||||
): Promise<CryptoBuffer> {
|
||||
const defaultOptions = {
|
||||
memoryCost: 65536,
|
||||
timeCost: 10,
|
||||
parallelism: 1,
|
||||
hashLength: 32
|
||||
};
|
||||
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
return await this.#deriveKeyBrowser(pin, clientId, opts);
|
||||
} else {
|
||||
return await this.#deriveKeyNode(pin, clientId, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js-specific key derivation using native argon2 or fallback
|
||||
* @param pin - The PIN to hash
|
||||
* @param clientId - The client identifier
|
||||
* @param options - Complete Argon2 options
|
||||
* @returns Promise resolving to the derived key
|
||||
* @private
|
||||
*/
|
||||
static async #deriveKeyNode(
|
||||
pin: string,
|
||||
clientId: string,
|
||||
options: Required<Argon2Options>
|
||||
): Promise<CryptoBuffer> {
|
||||
try {
|
||||
const crypto = await import("crypto");
|
||||
const salt = crypto.createHash("sha256").update(clientId).digest();
|
||||
|
||||
try {
|
||||
const argon2 = await import("argon2");
|
||||
const hash = await argon2.hash(pin, {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: options.memoryCost,
|
||||
timeCost: options.timeCost,
|
||||
parallelism: options.parallelism,
|
||||
salt: salt,
|
||||
raw: true,
|
||||
hashLength: options.hashLength
|
||||
});
|
||||
return new Uint8Array(hash);
|
||||
} catch {
|
||||
logger.setContext("Hashing");
|
||||
logger.warn("Native argon2 unavailable, falling back to @noble/hashes");
|
||||
return await this.#deriveKeyFallback(pin, salt, options);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to derive key in Node.js: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser-specific key derivation using argon2-browser WASM or fallback
|
||||
* @param pin - The PIN to hash
|
||||
* @param clientId - The client identifier
|
||||
* @param options - Complete Argon2 options
|
||||
* @returns Promise resolving to the derived key
|
||||
* @private
|
||||
*/
|
||||
static async #deriveKeyBrowser(
|
||||
pin: string,
|
||||
clientId: string,
|
||||
options: Required<Argon2Options>
|
||||
): Promise<CryptoBuffer> {
|
||||
try {
|
||||
// Create salt from client ID using Web Crypto API
|
||||
const encoder = new TextEncoder();
|
||||
const saltData = await crypto.subtle.digest("SHA-256", encoder.encode(clientId));
|
||||
const salt = new Uint8Array(saltData);
|
||||
|
||||
// Try to use argon2-browser WASM (from static files)
|
||||
try {
|
||||
logger.setContext("Hashing");
|
||||
logger.debug("🔍 Checking for argon2-browser from static files...");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const argon2 = (window as any).argon2;
|
||||
|
||||
if (argon2) {
|
||||
logger.debug("✅ argon2-browser found from static files");
|
||||
|
||||
const result = await argon2.hash({
|
||||
pass: pin,
|
||||
salt: Array.from(salt),
|
||||
type: argon2.ArgonType.Argon2id,
|
||||
mem: options.memoryCost,
|
||||
time: options.timeCost,
|
||||
parallelism: options.parallelism,
|
||||
hashLen: options.hashLength
|
||||
});
|
||||
|
||||
logger.debug("✅ Argon2 WASM hash successful");
|
||||
return new Uint8Array(result.hash);
|
||||
} else {
|
||||
logger.warn("❌ argon2-browser not available on window");
|
||||
throw new Error("argon2-browser not available");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"❌ argon2-browser static files unavailable, falling back to @noble/hashes:",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error as any
|
||||
);
|
||||
return await this.#deriveKeyFallback(pin, salt, options);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to derive key in browser: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback key derivation using @noble/hashes
|
||||
* @param pin - The PIN to hash
|
||||
* @param salt - The salt for hashing
|
||||
* @param options - Complete Argon2 options
|
||||
* @returns Promise resolving to the derived key
|
||||
* @private
|
||||
*/
|
||||
static async #deriveKeyFallback(
|
||||
pin: string,
|
||||
salt: CryptoBuffer,
|
||||
options: Required<Argon2Options>
|
||||
): Promise<CryptoBuffer> {
|
||||
const { argon2id } = await import("@noble/hashes/argon2");
|
||||
|
||||
return argon2id(BufferUtils.from(pin), new Uint8Array(salt), {
|
||||
m: options.memoryCost,
|
||||
t: options.timeCost,
|
||||
p: options.parallelism,
|
||||
dkLen: options.hashLength
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client ID from an email address (browser-only)
|
||||
* @param email - The email address to hash
|
||||
* @returns Promise resolving to hex-encoded client ID
|
||||
* @throws Error in Node.js environment
|
||||
*/
|
||||
static async createClientId(email: string): Promise<string> {
|
||||
if (typeof window !== "undefined") {
|
||||
// Browser environment - use Web Crypto API
|
||||
const hash = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(email.toLowerCase())
|
||||
);
|
||||
return BufferUtils.toString(new Uint8Array(hash), "hex");
|
||||
} else {
|
||||
// Node.js environment - this will be handled at runtime
|
||||
throw new Error("createClientId should be called asynchronously in Node.js environment");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client ID from an email address (cross-platform)
|
||||
* @param email - The email address to hash
|
||||
* @returns Promise resolving to hex-encoded client ID
|
||||
*/
|
||||
static async createClientIdAsync(email: string): Promise<string> {
|
||||
if (typeof window !== "undefined") {
|
||||
// Browser environment
|
||||
const hash = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(email.toLowerCase())
|
||||
);
|
||||
return BufferUtils.toString(new Uint8Array(hash), "hex");
|
||||
} else {
|
||||
// Node.js environment
|
||||
const crypto = await import("crypto");
|
||||
return crypto.createHash("sha256").update(email.toLowerCase()).digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the current Argon2 implementation being used
|
||||
* @returns Promise resolving to implementation description
|
||||
*/
|
||||
static async getImplementationInfo(): Promise<string> {
|
||||
if (typeof window !== "undefined") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((window as any).argon2) {
|
||||
return "argon2-browser WASM (static files)";
|
||||
}
|
||||
return "@noble/hashes (JavaScript fallback)";
|
||||
} else {
|
||||
try {
|
||||
await import("argon2");
|
||||
return "Native Node.js argon2 (C++ addon)";
|
||||
} catch {
|
||||
return "@noble/hashes (JavaScript fallback)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { BufferUtils, KyberCrypto, AESCrypto, ShamirSecretSharing } from "./utils";
|
||||
import type { CryptoBuffer, KyberKeyPair, ShamirShare } from "./utils";
|
||||
|
||||
describe("BufferUtils", () => {
|
||||
describe("from()", () => {
|
||||
it("should convert string to CryptoBuffer with utf8 encoding", () => {
|
||||
const input = "Hello World";
|
||||
const result = BufferUtils.from(input);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(new TextEncoder().encode(input));
|
||||
});
|
||||
|
||||
it("should convert hex string to CryptoBuffer", () => {
|
||||
const input = "48656c6c6f";
|
||||
const result = BufferUtils.from(input, "hex");
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]));
|
||||
});
|
||||
|
||||
it("should convert number array to CryptoBuffer", () => {
|
||||
const input = [72, 101, 108, 108, 111];
|
||||
const result = BufferUtils.from(input);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(new Uint8Array(input));
|
||||
});
|
||||
|
||||
it("should convert ArrayBuffer to CryptoBuffer", () => {
|
||||
const input = new ArrayBuffer(5);
|
||||
const view = new Uint8Array(input);
|
||||
view.set([72, 101, 108, 108, 111]);
|
||||
const result = BufferUtils.from(input);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
|
||||
});
|
||||
|
||||
it("should handle empty hex string", () => {
|
||||
const result = BufferUtils.from("", "hex");
|
||||
expect(result).toEqual(new Uint8Array([]));
|
||||
});
|
||||
|
||||
it("should handle invalid hex string", () => {
|
||||
const result = BufferUtils.from("invalid", "hex");
|
||||
// Invalid hex characters will be parsed as NaN, resulting in non-empty array
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toString()", () => {
|
||||
it("should convert CryptoBuffer to utf8 string", () => {
|
||||
const input = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
const result = BufferUtils.toString(input);
|
||||
expect(result).toBe("Hello");
|
||||
});
|
||||
|
||||
it("should convert CryptoBuffer to hex string", () => {
|
||||
const input = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
const result = BufferUtils.toString(input, "hex");
|
||||
expect(result).toBe("48656c6c6f");
|
||||
});
|
||||
|
||||
it("should handle empty buffer", () => {
|
||||
const input = new Uint8Array([]);
|
||||
expect(BufferUtils.toString(input)).toBe("");
|
||||
expect(BufferUtils.toString(input, "hex")).toBe("");
|
||||
});
|
||||
|
||||
it("should pad single digit hex values", () => {
|
||||
const input = new Uint8Array([0, 15, 255]);
|
||||
const result = BufferUtils.toString(input, "hex");
|
||||
expect(result).toBe("000fff");
|
||||
});
|
||||
});
|
||||
|
||||
describe("concat()", () => {
|
||||
it("should concatenate multiple buffers", () => {
|
||||
const buf1 = new Uint8Array([1, 2, 3]);
|
||||
const buf2 = new Uint8Array([4, 5]);
|
||||
const buf3 = new Uint8Array([6, 7, 8, 9]);
|
||||
const result = BufferUtils.concat([buf1, buf2, buf3]);
|
||||
expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
|
||||
});
|
||||
|
||||
it("should handle empty buffers", () => {
|
||||
const buf1 = new Uint8Array([1, 2]);
|
||||
const buf2 = new Uint8Array([]);
|
||||
const buf3 = new Uint8Array([3, 4]);
|
||||
const result = BufferUtils.concat([buf1, buf2, buf3]);
|
||||
expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
|
||||
});
|
||||
|
||||
it("should handle single buffer", () => {
|
||||
const buf = new Uint8Array([1, 2, 3]);
|
||||
const result = BufferUtils.concat([buf]);
|
||||
expect(result).toEqual(buf);
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const result = BufferUtils.concat([]);
|
||||
expect(result).toEqual(new Uint8Array([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("randomBytes()", () => {
|
||||
it("should generate random bytes of specified length", () => {
|
||||
const length = 32;
|
||||
const result = BufferUtils.randomBytes(length);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBe(length);
|
||||
});
|
||||
|
||||
it("should generate different random bytes on each call", () => {
|
||||
const result1 = BufferUtils.randomBytes(16);
|
||||
const result2 = BufferUtils.randomBytes(16);
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
|
||||
it("should handle zero length", () => {
|
||||
const result = BufferUtils.randomBytes(0);
|
||||
expect(result).toEqual(new Uint8Array([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("xor()", () => {
|
||||
it("should perform XOR operation on equal length buffers", () => {
|
||||
const a = new Uint8Array([0xff, 0x00, 0xaa]);
|
||||
const b = new Uint8Array([0x00, 0xff, 0x55]);
|
||||
const result = BufferUtils.xor(a, b);
|
||||
expect(result).toEqual(new Uint8Array([0xff, 0xff, 0xff]));
|
||||
});
|
||||
|
||||
it("should handle different length buffers", () => {
|
||||
const a = new Uint8Array([0xff, 0x00]);
|
||||
const b = new Uint8Array([0x00, 0xff, 0x55]);
|
||||
const result = BufferUtils.xor(a, b);
|
||||
expect(result).toEqual(new Uint8Array([0xff, 0xff, 0x55]));
|
||||
});
|
||||
|
||||
it("should handle empty buffers", () => {
|
||||
const a = new Uint8Array([]);
|
||||
const b = new Uint8Array([1, 2, 3]);
|
||||
const result = BufferUtils.xor(a, b);
|
||||
expect(result).toEqual(new Uint8Array([1, 2, 3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("equals()", () => {
|
||||
it("should return true for equal buffers", () => {
|
||||
const a = new Uint8Array([1, 2, 3, 4]);
|
||||
const b = new Uint8Array([1, 2, 3, 4]);
|
||||
expect(BufferUtils.equals(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different buffers", () => {
|
||||
const a = new Uint8Array([1, 2, 3, 4]);
|
||||
const b = new Uint8Array([1, 2, 3, 5]);
|
||||
expect(BufferUtils.equals(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for different length buffers", () => {
|
||||
const a = new Uint8Array([1, 2, 3]);
|
||||
const b = new Uint8Array([1, 2, 3, 4]);
|
||||
expect(BufferUtils.equals(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for empty buffers", () => {
|
||||
const a = new Uint8Array([]);
|
||||
const b = new Uint8Array([]);
|
||||
expect(BufferUtils.equals(a, b)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("KyberCrypto", () => {
|
||||
let keyPair: KyberKeyPair;
|
||||
|
||||
beforeEach(() => {
|
||||
keyPair = KyberCrypto.generateKeyPair();
|
||||
});
|
||||
|
||||
describe("generateKeyPair()", () => {
|
||||
it("should generate a valid key pair", () => {
|
||||
expect(keyPair.publicKey).toBeInstanceOf(Uint8Array);
|
||||
expect(keyPair.privateKey).toBeInstanceOf(Uint8Array);
|
||||
expect(keyPair.publicKey.length).toBeGreaterThan(0);
|
||||
expect(keyPair.privateKey.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should generate different key pairs on each call", () => {
|
||||
const keyPair2 = KyberCrypto.generateKeyPair();
|
||||
expect(keyPair.publicKey).not.toEqual(keyPair2.publicKey);
|
||||
expect(keyPair.privateKey).not.toEqual(keyPair2.privateKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encapsulate()", () => {
|
||||
it("should encapsulate a shared secret", () => {
|
||||
const result = KyberCrypto.encapsulate(keyPair.publicKey);
|
||||
expect(result.sharedSecret).toBeInstanceOf(Uint8Array);
|
||||
expect(result.encapsulatedSecret).toBeInstanceOf(Uint8Array);
|
||||
expect(result.sharedSecret.length).toBeGreaterThan(0);
|
||||
expect(result.encapsulatedSecret.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should generate different shared secrets on each call", () => {
|
||||
const result1 = KyberCrypto.encapsulate(keyPair.publicKey);
|
||||
const result2 = KyberCrypto.encapsulate(keyPair.publicKey);
|
||||
expect(result1.sharedSecret).not.toEqual(result2.sharedSecret);
|
||||
expect(result1.encapsulatedSecret).not.toEqual(result2.encapsulatedSecret);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decapsulate()", () => {
|
||||
it("should decapsulate the shared secret correctly", () => {
|
||||
const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
|
||||
const decapsulated = KyberCrypto.decapsulate(
|
||||
keyPair.privateKey,
|
||||
encapsulated.encapsulatedSecret
|
||||
);
|
||||
expect(decapsulated).toEqual(encapsulated.sharedSecret);
|
||||
});
|
||||
|
||||
it("should return different results for different private keys", () => {
|
||||
const keyPair2 = KyberCrypto.generateKeyPair();
|
||||
const encapsulated = KyberCrypto.encapsulate(keyPair.publicKey);
|
||||
const decapsulated1 = KyberCrypto.decapsulate(
|
||||
keyPair.privateKey,
|
||||
encapsulated.encapsulatedSecret
|
||||
);
|
||||
const decapsulated2 = KyberCrypto.decapsulate(
|
||||
keyPair2.privateKey,
|
||||
encapsulated.encapsulatedSecret
|
||||
);
|
||||
expect(decapsulated1).not.toEqual(decapsulated2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AESCrypto", () => {
|
||||
describe("generateSessionKey()", () => {
|
||||
it("should generate a 32-byte key", () => {
|
||||
const key = AESCrypto.generateSessionKey();
|
||||
expect(key).toBeInstanceOf(Uint8Array);
|
||||
expect(key.length).toBe(32);
|
||||
});
|
||||
|
||||
it("should generate different keys on each call", () => {
|
||||
const key1 = AESCrypto.generateSessionKey();
|
||||
const key2 = AESCrypto.generateSessionKey();
|
||||
expect(key1).not.toEqual(key2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encrypt() and decrypt()", () => {
|
||||
let key: CryptoBuffer;
|
||||
|
||||
beforeEach(() => {
|
||||
key = AESCrypto.generateSessionKey();
|
||||
});
|
||||
|
||||
it("should encrypt and decrypt data correctly", async () => {
|
||||
const plaintext = "Hello, World!";
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
|
||||
expect(encrypted.encrypted).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.iv).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.tag).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.iv.length).toBe(16);
|
||||
expect(encrypted.tag.length).toBe(16);
|
||||
|
||||
const decrypted = await AESCrypto.decrypt(
|
||||
encrypted.encrypted,
|
||||
key,
|
||||
encrypted.iv,
|
||||
encrypted.tag
|
||||
);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("should handle empty string", async () => {
|
||||
const plaintext = "";
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
const decrypted = await AESCrypto.decrypt(
|
||||
encrypted.encrypted,
|
||||
key,
|
||||
encrypted.iv,
|
||||
encrypted.tag
|
||||
);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("should handle unicode characters", async () => {
|
||||
const plaintext = "🔐 Verschlüsselter Text mit Umlauten: äöü";
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
const decrypted = await AESCrypto.decrypt(
|
||||
encrypted.encrypted,
|
||||
key,
|
||||
encrypted.iv,
|
||||
encrypted.tag
|
||||
);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("should generate different IV and tag for same plaintext", async () => {
|
||||
const plaintext = "Same text";
|
||||
const encrypted1 = await AESCrypto.encrypt(plaintext, key);
|
||||
const encrypted2 = await AESCrypto.encrypt(plaintext, key);
|
||||
|
||||
expect(encrypted1.iv).not.toEqual(encrypted2.iv);
|
||||
expect(encrypted1.encrypted).not.toEqual(encrypted2.encrypted);
|
||||
expect(encrypted1.tag).not.toEqual(encrypted2.tag);
|
||||
});
|
||||
|
||||
it("should fail with wrong key", async () => {
|
||||
const plaintext = "Secret message";
|
||||
const wrongKey = AESCrypto.generateSessionKey();
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
|
||||
await expect(
|
||||
AESCrypto.decrypt(encrypted.encrypted, wrongKey, encrypted.iv, encrypted.tag)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should fail with wrong IV", async () => {
|
||||
const plaintext = "Secret message";
|
||||
const wrongIV = BufferUtils.randomBytes(16);
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
|
||||
await expect(
|
||||
AESCrypto.decrypt(encrypted.encrypted, key, wrongIV, encrypted.tag)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should fail with wrong tag", async () => {
|
||||
const plaintext = "Secret message";
|
||||
const wrongTag = BufferUtils.randomBytes(16);
|
||||
const encrypted = await AESCrypto.encrypt(plaintext, key);
|
||||
|
||||
await expect(
|
||||
AESCrypto.decrypt(encrypted.encrypted, key, encrypted.iv, wrongTag)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ShamirSecretSharing", () => {
|
||||
describe("splitSecret()", () => {
|
||||
it("should split secret into shares", () => {
|
||||
const secret = BufferUtils.from("test secret");
|
||||
const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
|
||||
|
||||
expect(shares).toHaveLength(3);
|
||||
shares.forEach((share, index) => {
|
||||
expect(share.x).toBe(index + 1);
|
||||
expect(share.y).toBeInstanceOf(Uint8Array);
|
||||
expect(share.y.length).toBe(4 + secret.length); // 4 bytes for length + secret
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error for empty secret", () => {
|
||||
const secret = new Uint8Array([]);
|
||||
expect(() => ShamirSecretSharing.splitSecret(secret, 2, 3)).toThrow(
|
||||
"Secret cannot be empty for Shamir secret sharing"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle large secrets", () => {
|
||||
const secret = BufferUtils.randomBytes(1000);
|
||||
const shares = ShamirSecretSharing.splitSecret(secret, 3, 5);
|
||||
|
||||
expect(shares).toHaveLength(5);
|
||||
shares.forEach((share) => {
|
||||
expect(share.y.length).toBe(4 + secret.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconstructSecret()", () => {
|
||||
it("should reconstruct secret from shares", () => {
|
||||
const originalSecret = BufferUtils.from("test secret for reconstruction");
|
||||
const shares = ShamirSecretSharing.splitSecret(originalSecret, 2, 5);
|
||||
|
||||
// Use first 2 shares (minimum threshold)
|
||||
const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
|
||||
expect(reconstructed).toEqual(originalSecret);
|
||||
});
|
||||
|
||||
it("should reconstruct secret from any subset of shares", () => {
|
||||
const originalSecret = BufferUtils.from("another test secret");
|
||||
const shares = ShamirSecretSharing.splitSecret(originalSecret, 3, 5);
|
||||
|
||||
// Test different combinations
|
||||
const reconstructed1 = ShamirSecretSharing.reconstructSecret(shares.slice(0, 3));
|
||||
const reconstructed2 = ShamirSecretSharing.reconstructSecret(shares.slice(1, 4));
|
||||
const reconstructed3 = ShamirSecretSharing.reconstructSecret(shares.slice(2, 5));
|
||||
|
||||
expect(reconstructed1).toEqual(originalSecret);
|
||||
expect(reconstructed2).toEqual(originalSecret);
|
||||
expect(reconstructed3).toEqual(originalSecret);
|
||||
});
|
||||
|
||||
it("should throw error with insufficient shares", () => {
|
||||
const shares: ShamirShare[] = [{ x: 1, y: new Uint8Array([1, 2, 3]) }];
|
||||
expect(() => ShamirSecretSharing.reconstructSecret(shares)).toThrow(
|
||||
"Need at least 2 shares to reconstruct the secret"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle secrets with different lengths", () => {
|
||||
const secrets = [
|
||||
BufferUtils.from("short"),
|
||||
BufferUtils.from("medium length secret"),
|
||||
BufferUtils.randomBytes(100)
|
||||
];
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
const shares = ShamirSecretSharing.splitSecret(secret, 2, 3);
|
||||
const reconstructed = ShamirSecretSharing.reconstructSecret(shares.slice(0, 2));
|
||||
expect(reconstructed).toEqual(secret);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
import { ml_kem768 } from "@noble/post-quantum/ml-kem";
|
||||
import { randomBytes } from "@noble/hashes/utils";
|
||||
|
||||
/**
|
||||
* Type alias for cryptographic buffer operations
|
||||
*/
|
||||
export type CryptoBuffer = Uint8Array;
|
||||
|
||||
/**
|
||||
* Utility class for buffer operations and conversions
|
||||
*/
|
||||
export class BufferUtils {
|
||||
/**
|
||||
* Converts various data types to CryptoBuffer
|
||||
* @param data - The data to convert (string, number array, Uint8Array, or ArrayBuffer)
|
||||
* @param encoding - The encoding to use for string conversion ('hex' or 'utf8')
|
||||
* @returns A CryptoBuffer representation of the input data
|
||||
*/
|
||||
static from(
|
||||
data: string | number[] | Uint8Array | ArrayBuffer,
|
||||
encoding?: "hex" | "utf8"
|
||||
): CryptoBuffer {
|
||||
if (typeof data === "string") {
|
||||
if (encoding === "hex") {
|
||||
return new Uint8Array(data.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
|
||||
}
|
||||
return new TextEncoder().encode(data);
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a CryptoBuffer to string
|
||||
* @param buffer - The buffer to convert
|
||||
* @param encoding - The encoding to use ('hex' or 'utf8', defaults to 'utf8')
|
||||
* @returns String representation of the buffer
|
||||
*/
|
||||
static toString(buffer: CryptoBuffer, encoding: "hex" | "utf8" = "utf8"): string {
|
||||
if (encoding === "hex") {
|
||||
return Array.from(buffer)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
return new TextDecoder().decode(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates multiple CryptoBuffers into a single buffer
|
||||
* @param buffers - Array of buffers to concatenate
|
||||
* @returns A single CryptoBuffer containing all input buffers
|
||||
*/
|
||||
static concat(buffers: CryptoBuffer[]): CryptoBuffer {
|
||||
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const buffer of buffers) {
|
||||
result.set(buffer, offset);
|
||||
offset += buffer.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates cryptographically secure random bytes
|
||||
* @param length - The number of random bytes to generate
|
||||
* @returns A CryptoBuffer containing random bytes
|
||||
*/
|
||||
static randomBytes(length: number): CryptoBuffer {
|
||||
return randomBytes(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs XOR operation on two CryptoBuffers
|
||||
* @param a - First buffer
|
||||
* @param b - Second buffer
|
||||
* @returns XOR result as CryptoBuffer
|
||||
*/
|
||||
static xor(a: CryptoBuffer, b: CryptoBuffer): CryptoBuffer {
|
||||
const result = new Uint8Array(Math.max(a.length, b.length));
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
result[i] = (a[i] || 0) ^ (b[i] || 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two CryptoBuffers for equality
|
||||
* @param a - First buffer
|
||||
* @param b - Second buffer
|
||||
* @returns True if buffers are equal, false otherwise
|
||||
*/
|
||||
static equals(a: CryptoBuffer, b: CryptoBuffer): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Kyber (ML-KEM-768) key pairs
|
||||
*/
|
||||
export interface KyberKeyPair {
|
||||
/** The public key for encryption */
|
||||
publicKey: CryptoBuffer;
|
||||
/** The private key for decryption */
|
||||
privateKey: CryptoBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-quantum cryptography implementation using ML-KEM-768 (Kyber)
|
||||
* Provides key generation, encapsulation, and decapsulation
|
||||
*/
|
||||
export class KyberCrypto {
|
||||
/**
|
||||
* Generates a new Kyber key pair
|
||||
* @returns A new KyberKeyPair containing public and private keys
|
||||
*/
|
||||
static generateKeyPair(): KyberKeyPair {
|
||||
const keys = ml_kem768.keygen();
|
||||
return {
|
||||
publicKey: new Uint8Array(keys.publicKey),
|
||||
privateKey: new Uint8Array(keys.secretKey)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates a shared secret using the recipient's public key
|
||||
* @param publicKey - The recipient's public key
|
||||
* @returns Object containing the shared secret and encapsulated secret
|
||||
*/
|
||||
static encapsulate(publicKey: CryptoBuffer): {
|
||||
sharedSecret: CryptoBuffer;
|
||||
encapsulatedSecret: CryptoBuffer;
|
||||
} {
|
||||
const result = ml_kem768.encapsulate(publicKey);
|
||||
return {
|
||||
sharedSecret: new Uint8Array(result.sharedSecret),
|
||||
encapsulatedSecret: new Uint8Array(result.cipherText)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decapsulates a shared secret using the private key
|
||||
* @param privateKey - The private key for decapsulation
|
||||
* @param encapsulatedSecret - The encapsulated secret from the sender
|
||||
* @returns The shared secret
|
||||
*/
|
||||
static decapsulate(privateKey: CryptoBuffer, encapsulatedSecret: CryptoBuffer): CryptoBuffer {
|
||||
return new Uint8Array(ml_kem768.decapsulate(encapsulatedSecret, privateKey));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES-GCM encryption utilities with cross-platform support
|
||||
* Uses Web Crypto API in browsers and Node.js crypto in server environments
|
||||
*/
|
||||
export class AESCrypto {
|
||||
/**
|
||||
* Generates a random 256-bit AES session key
|
||||
* @returns A 32-byte CryptoBuffer for use as AES-256 key
|
||||
*/
|
||||
static generateSessionKey(): CryptoBuffer {
|
||||
return BufferUtils.randomBytes(32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-256-GCM
|
||||
* @param data - The plaintext string to encrypt
|
||||
* @param key - The 32-byte AES key
|
||||
* @returns Promise resolving to encrypted data, IV, and authentication tag
|
||||
*/
|
||||
static async encrypt(
|
||||
data: string,
|
||||
key: CryptoBuffer
|
||||
): Promise<{ encrypted: CryptoBuffer; iv: CryptoBuffer; tag: CryptoBuffer }> {
|
||||
const iv = BufferUtils.randomBytes(16);
|
||||
|
||||
// Use Web Crypto API if available (browser), otherwise fall back to Node.js
|
||||
if (typeof crypto !== "undefined" && crypto.subtle) {
|
||||
// Browser implementation using Web Crypto API
|
||||
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
|
||||
"encrypt"
|
||||
]);
|
||||
|
||||
const additionalData = BufferUtils.from("appointment-data");
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv, additionalData },
|
||||
cryptoKey,
|
||||
BufferUtils.from(data)
|
||||
);
|
||||
|
||||
// Extract tag from encrypted data (last 16 bytes)
|
||||
const encryptedArray = new Uint8Array(encrypted);
|
||||
const tag = encryptedArray.slice(-16);
|
||||
const ciphertext = encryptedArray.slice(0, -16);
|
||||
|
||||
return {
|
||||
encrypted: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
};
|
||||
} else {
|
||||
// Node.js fallback using built-in crypto
|
||||
const nodeCrypto = await import("crypto");
|
||||
const cipher = nodeCrypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
cipher.setAAD(BufferUtils.from("appointment-data"));
|
||||
|
||||
let encrypted = cipher.update(data, "utf8");
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
encrypted: new Uint8Array(encrypted),
|
||||
iv,
|
||||
tag: new Uint8Array(tag)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES-256-GCM encrypted data
|
||||
* @param encrypted - The encrypted data
|
||||
* @param key - The 32-byte AES key
|
||||
* @param iv - The initialization vector
|
||||
* @param tag - The authentication tag
|
||||
* @returns Promise resolving to the decrypted plaintext string
|
||||
*/
|
||||
static async decrypt(
|
||||
encrypted: CryptoBuffer,
|
||||
key: CryptoBuffer,
|
||||
iv: CryptoBuffer,
|
||||
tag: CryptoBuffer
|
||||
): Promise<string> {
|
||||
if (typeof crypto !== "undefined" && crypto.subtle) {
|
||||
// Browser implementation using Web Crypto API
|
||||
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
|
||||
"decrypt"
|
||||
]);
|
||||
|
||||
const additionalData = BufferUtils.from("appointment-data");
|
||||
|
||||
// Combine encrypted data and tag for Web Crypto API
|
||||
const encryptedWithTag = BufferUtils.concat([encrypted, tag]);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv, additionalData },
|
||||
cryptoKey,
|
||||
encryptedWithTag
|
||||
);
|
||||
|
||||
return BufferUtils.toString(new Uint8Array(decrypted));
|
||||
} else {
|
||||
// Node.js fallback using built-in crypto
|
||||
const nodeCrypto = await import("crypto");
|
||||
const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAAD(BufferUtils.from("appointment-data"));
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
let decrypted = decipher.update(encrypted);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
|
||||
return decrypted.toString("utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Shamir secret sharing shares
|
||||
*/
|
||||
export interface ShamirShare {
|
||||
/** The x-coordinate of the share */
|
||||
x: number;
|
||||
/** The y-coordinate containing the share data */
|
||||
y: CryptoBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Shamir Secret Sharing implementation
|
||||
* Note: This is a basic implementation for demonstration purposes
|
||||
*/
|
||||
export class ShamirSecretSharing {
|
||||
/**
|
||||
* Splits a secret into multiple shares
|
||||
* @param secret - The secret to split
|
||||
* @param threshold - Minimum number of shares needed to reconstruct
|
||||
* @param totalShares - Total number of shares to create
|
||||
* @returns Array of ShamirShare objects
|
||||
*/
|
||||
static splitSecret(secret: CryptoBuffer, threshold: number, totalShares: number): ShamirShare[] {
|
||||
if (!secret || secret.length === 0) {
|
||||
throw new Error("Secret cannot be empty for Shamir secret sharing");
|
||||
}
|
||||
|
||||
const originalLength = secret.length;
|
||||
const lengthBytes = new Uint8Array(4);
|
||||
new DataView(lengthBytes.buffer).setUint32(0, originalLength, true);
|
||||
|
||||
const shares: ShamirShare[] = [];
|
||||
|
||||
for (let i = 0; i < totalShares; i++) {
|
||||
const shareData = new Uint8Array(4 + secret.length);
|
||||
shareData.set(lengthBytes, 0);
|
||||
shareData.set(secret, 4);
|
||||
|
||||
shares.push({
|
||||
x: i + 1,
|
||||
y: shareData
|
||||
});
|
||||
}
|
||||
|
||||
return shares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs a secret from shares
|
||||
* @param shareArray - Array of shares to reconstruct from
|
||||
* @returns The reconstructed secret
|
||||
*/
|
||||
static reconstructSecret(shareArray: ShamirShare[]): CryptoBuffer {
|
||||
if (shareArray.length < 2) {
|
||||
throw new Error("Need at least 2 shares to reconstruct the secret");
|
||||
}
|
||||
|
||||
const firstShare = shareArray[0];
|
||||
|
||||
const lengthBytes = firstShare.y.slice(0, 4);
|
||||
const originalLength = new DataView(lengthBytes.buffer).getUint32(0, true);
|
||||
|
||||
const secret = firstShare.y.slice(4, 4 + originalLength);
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
+686
File diff suppressed because one or more lines are too long
Executable
BIN
Binary file not shown.
@@ -0,0 +1,609 @@
|
||||
var Module = typeof self !== "undefined" && typeof self.Module !== "undefined" ? self.Module : {};
|
||||
var jsModule = Module;
|
||||
var moduleOverrides = {};
|
||||
var key;
|
||||
for (key in Module) {
|
||||
if (Module.hasOwnProperty(key)) {
|
||||
moduleOverrides[key] = Module[key];
|
||||
}
|
||||
}
|
||||
var arguments_ = [];
|
||||
var thisProgram = "./this.program";
|
||||
var quit_ = function (status, toThrow) {
|
||||
throw toThrow;
|
||||
};
|
||||
var ENVIRONMENT_IS_WEB = false;
|
||||
var ENVIRONMENT_IS_WORKER = false;
|
||||
var ENVIRONMENT_IS_NODE = false;
|
||||
var ENVIRONMENT_IS_SHELL = false;
|
||||
ENVIRONMENT_IS_WEB = typeof window === "object";
|
||||
ENVIRONMENT_IS_WORKER = typeof importScripts === "function";
|
||||
ENVIRONMENT_IS_NODE =
|
||||
typeof process === "object" &&
|
||||
typeof process.versions === "object" &&
|
||||
typeof process.versions.node === "string";
|
||||
ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER;
|
||||
var scriptDirectory = "";
|
||||
function locateFile(path) {
|
||||
if (Module["locateFile"]) {
|
||||
return Module["locateFile"](path, scriptDirectory);
|
||||
}
|
||||
return scriptDirectory + path;
|
||||
}
|
||||
var read_, readAsync, readBinary, setWindowTitle;
|
||||
var nodeFS;
|
||||
var nodePath;
|
||||
if (ENVIRONMENT_IS_NODE) {
|
||||
if (ENVIRONMENT_IS_WORKER) {
|
||||
scriptDirectory = require("path").dirname(scriptDirectory) + "/";
|
||||
} else {
|
||||
scriptDirectory = __dirname + "/";
|
||||
}
|
||||
read_ = function shell_read(filename, binary) {
|
||||
if (!nodeFS) nodeFS = require("fs");
|
||||
if (!nodePath) nodePath = require("path");
|
||||
filename = nodePath["normalize"](filename);
|
||||
return nodeFS["readFileSync"](filename, binary ? null : "utf8");
|
||||
};
|
||||
readBinary = function readBinary(filename) {
|
||||
var ret = read_(filename, true);
|
||||
if (!ret.buffer) {
|
||||
ret = new Uint8Array(ret);
|
||||
}
|
||||
assert(ret.buffer);
|
||||
return ret;
|
||||
};
|
||||
if (process["argv"].length > 1) {
|
||||
thisProgram = process["argv"][1].replace(/\\/g, "/");
|
||||
}
|
||||
arguments_ = process["argv"].slice(2);
|
||||
if (typeof module !== "undefined") {
|
||||
module["exports"] = Module;
|
||||
}
|
||||
process["on"]("uncaughtException", function (ex) {
|
||||
if (!(ex instanceof ExitStatus)) {
|
||||
throw ex;
|
||||
}
|
||||
});
|
||||
process["on"]("unhandledRejection", abort);
|
||||
quit_ = function (status) {
|
||||
process["exit"](status);
|
||||
};
|
||||
Module["inspect"] = function () {
|
||||
return "[Emscripten Module object]";
|
||||
};
|
||||
} else if (ENVIRONMENT_IS_SHELL) {
|
||||
if (typeof read != "undefined") {
|
||||
read_ = function shell_read(f) {
|
||||
return read(f);
|
||||
};
|
||||
}
|
||||
readBinary = function readBinary(f) {
|
||||
var data;
|
||||
if (typeof readbuffer === "function") {
|
||||
return new Uint8Array(readbuffer(f));
|
||||
}
|
||||
data = read(f, "binary");
|
||||
assert(typeof data === "object");
|
||||
return data;
|
||||
};
|
||||
if (typeof scriptArgs != "undefined") {
|
||||
arguments_ = scriptArgs;
|
||||
} else if (typeof arguments != "undefined") {
|
||||
arguments_ = arguments;
|
||||
}
|
||||
if (typeof quit === "function") {
|
||||
quit_ = function (status) {
|
||||
quit(status);
|
||||
};
|
||||
}
|
||||
if (typeof print !== "undefined") {
|
||||
if (typeof console === "undefined") console = {};
|
||||
console.log = print;
|
||||
console.warn = console.error = typeof printErr !== "undefined" ? printErr : print;
|
||||
}
|
||||
} else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
|
||||
if (ENVIRONMENT_IS_WORKER) {
|
||||
scriptDirectory = self.location.href;
|
||||
} else if (typeof document !== "undefined" && document.currentScript) {
|
||||
scriptDirectory = document.currentScript.src;
|
||||
}
|
||||
if (scriptDirectory.indexOf("blob:") !== 0) {
|
||||
scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf("/") + 1);
|
||||
} else {
|
||||
scriptDirectory = "";
|
||||
}
|
||||
{
|
||||
read_ = function (url) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, false);
|
||||
xhr.send(null);
|
||||
return xhr.responseText;
|
||||
};
|
||||
if (ENVIRONMENT_IS_WORKER) {
|
||||
readBinary = function (url) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, false);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.send(null);
|
||||
return new Uint8Array(xhr.response);
|
||||
};
|
||||
}
|
||||
readAsync = function (url, onload, onerror) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = function () {
|
||||
if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) {
|
||||
onload(xhr.response);
|
||||
return;
|
||||
}
|
||||
onerror();
|
||||
};
|
||||
xhr.onerror = onerror;
|
||||
xhr.send(null);
|
||||
};
|
||||
}
|
||||
setWindowTitle = function (title) {
|
||||
document.title = title;
|
||||
};
|
||||
} else {
|
||||
}
|
||||
var out = Module["print"] || console.log.bind(console);
|
||||
var err = Module["printErr"] || console.warn.bind(console);
|
||||
for (key in moduleOverrides) {
|
||||
if (moduleOverrides.hasOwnProperty(key)) {
|
||||
Module[key] = moduleOverrides[key];
|
||||
}
|
||||
}
|
||||
moduleOverrides = null;
|
||||
if (Module["arguments"]) arguments_ = Module["arguments"];
|
||||
if (Module["thisProgram"]) thisProgram = Module["thisProgram"];
|
||||
if (Module["quit"]) quit_ = Module["quit"];
|
||||
var wasmBinary;
|
||||
if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"];
|
||||
var noExitRuntime = Module["noExitRuntime"] || true;
|
||||
if (typeof WebAssembly !== "object") {
|
||||
abort("no native wasm support detected");
|
||||
}
|
||||
var wasmMemory;
|
||||
var ABORT = false;
|
||||
var EXITSTATUS;
|
||||
function assert(condition, text) {
|
||||
if (!condition) {
|
||||
abort("Assertion failed: " + text);
|
||||
}
|
||||
}
|
||||
var ALLOC_NORMAL = 0;
|
||||
var ALLOC_STACK = 1;
|
||||
function allocate(slab, allocator) {
|
||||
var ret;
|
||||
if (allocator == ALLOC_STACK) {
|
||||
ret = stackAlloc(slab.length);
|
||||
} else {
|
||||
ret = _malloc(slab.length);
|
||||
}
|
||||
if (slab.subarray || slab.slice) {
|
||||
HEAPU8.set(slab, ret);
|
||||
} else {
|
||||
HEAPU8.set(new Uint8Array(slab), ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
var UTF8Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf8") : undefined;
|
||||
function UTF8ArrayToString(heap, idx, maxBytesToRead) {
|
||||
var endIdx = idx + maxBytesToRead;
|
||||
var endPtr = idx;
|
||||
while (heap[endPtr] && !(endPtr >= endIdx)) ++endPtr;
|
||||
if (endPtr - idx > 16 && heap.subarray && UTF8Decoder) {
|
||||
return UTF8Decoder.decode(heap.subarray(idx, endPtr));
|
||||
} else {
|
||||
var str = "";
|
||||
while (idx < endPtr) {
|
||||
var u0 = heap[idx++];
|
||||
if (!(u0 & 128)) {
|
||||
str += String.fromCharCode(u0);
|
||||
continue;
|
||||
}
|
||||
var u1 = heap[idx++] & 63;
|
||||
if ((u0 & 224) == 192) {
|
||||
str += String.fromCharCode(((u0 & 31) << 6) | u1);
|
||||
continue;
|
||||
}
|
||||
var u2 = heap[idx++] & 63;
|
||||
if ((u0 & 240) == 224) {
|
||||
u0 = ((u0 & 15) << 12) | (u1 << 6) | u2;
|
||||
} else {
|
||||
u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heap[idx++] & 63);
|
||||
}
|
||||
if (u0 < 65536) {
|
||||
str += String.fromCharCode(u0);
|
||||
} else {
|
||||
var ch = u0 - 65536;
|
||||
str += String.fromCharCode(55296 | (ch >> 10), 56320 | (ch & 1023));
|
||||
}
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function UTF8ToString(ptr, maxBytesToRead) {
|
||||
return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : "";
|
||||
}
|
||||
function alignUp(x, multiple) {
|
||||
if (x % multiple > 0) {
|
||||
x += multiple - (x % multiple);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
var buffer, HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64;
|
||||
function updateGlobalBufferAndViews(buf) {
|
||||
buffer = buf;
|
||||
Module["HEAP8"] = HEAP8 = new Int8Array(buf);
|
||||
Module["HEAP16"] = HEAP16 = new Int16Array(buf);
|
||||
Module["HEAP32"] = HEAP32 = new Int32Array(buf);
|
||||
Module["HEAPU8"] = HEAPU8 = new Uint8Array(buf);
|
||||
Module["HEAPU16"] = HEAPU16 = new Uint16Array(buf);
|
||||
Module["HEAPU32"] = HEAPU32 = new Uint32Array(buf);
|
||||
Module["HEAPF32"] = HEAPF32 = new Float32Array(buf);
|
||||
Module["HEAPF64"] = HEAPF64 = new Float64Array(buf);
|
||||
}
|
||||
var INITIAL_MEMORY = Module["INITIAL_MEMORY"] || 16777216;
|
||||
var wasmTable;
|
||||
var __ATPRERUN__ = [];
|
||||
var __ATINIT__ = [];
|
||||
var __ATPOSTRUN__ = [];
|
||||
var runtimeInitialized = false;
|
||||
function preRun() {
|
||||
if (Module["preRun"]) {
|
||||
if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]];
|
||||
while (Module["preRun"].length) {
|
||||
addOnPreRun(Module["preRun"].shift());
|
||||
}
|
||||
}
|
||||
callRuntimeCallbacks(__ATPRERUN__);
|
||||
}
|
||||
function initRuntime() {
|
||||
runtimeInitialized = true;
|
||||
callRuntimeCallbacks(__ATINIT__);
|
||||
}
|
||||
function postRun() {
|
||||
if (Module["postRun"]) {
|
||||
if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]];
|
||||
while (Module["postRun"].length) {
|
||||
addOnPostRun(Module["postRun"].shift());
|
||||
}
|
||||
}
|
||||
callRuntimeCallbacks(__ATPOSTRUN__);
|
||||
}
|
||||
function addOnPreRun(cb) {
|
||||
__ATPRERUN__.unshift(cb);
|
||||
}
|
||||
function addOnInit(cb) {
|
||||
__ATINIT__.unshift(cb);
|
||||
}
|
||||
function addOnPostRun(cb) {
|
||||
__ATPOSTRUN__.unshift(cb);
|
||||
}
|
||||
var runDependencies = 0;
|
||||
var runDependencyWatcher = null;
|
||||
var dependenciesFulfilled = null;
|
||||
function addRunDependency(id) {
|
||||
runDependencies++;
|
||||
if (Module["monitorRunDependencies"]) {
|
||||
Module["monitorRunDependencies"](runDependencies);
|
||||
}
|
||||
}
|
||||
function removeRunDependency(id) {
|
||||
runDependencies--;
|
||||
if (Module["monitorRunDependencies"]) {
|
||||
Module["monitorRunDependencies"](runDependencies);
|
||||
}
|
||||
if (runDependencies == 0) {
|
||||
if (runDependencyWatcher !== null) {
|
||||
clearInterval(runDependencyWatcher);
|
||||
runDependencyWatcher = null;
|
||||
}
|
||||
if (dependenciesFulfilled) {
|
||||
var callback = dependenciesFulfilled;
|
||||
dependenciesFulfilled = null;
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
Module["preloadedImages"] = {};
|
||||
Module["preloadedAudios"] = {};
|
||||
function abort(what) {
|
||||
if (Module["onAbort"]) {
|
||||
Module["onAbort"](what);
|
||||
}
|
||||
what += "";
|
||||
err(what);
|
||||
ABORT = true;
|
||||
EXITSTATUS = 1;
|
||||
what = "abort(" + what + "). Build with -s ASSERTIONS=1 for more info.";
|
||||
var e = new WebAssembly.RuntimeError(what);
|
||||
throw e;
|
||||
}
|
||||
var dataURIPrefix = "data:application/octet-stream;base64,";
|
||||
function isDataURI(filename) {
|
||||
return filename.startsWith(dataURIPrefix);
|
||||
}
|
||||
function isFileURI(filename) {
|
||||
return filename.startsWith("file://");
|
||||
}
|
||||
var wasmBinaryFile = "argon2.wasm";
|
||||
if (!isDataURI(wasmBinaryFile)) {
|
||||
wasmBinaryFile = locateFile(wasmBinaryFile);
|
||||
}
|
||||
function getBinary(file) {
|
||||
try {
|
||||
if (file == wasmBinaryFile && wasmBinary) {
|
||||
return new Uint8Array(wasmBinary);
|
||||
}
|
||||
if (readBinary) {
|
||||
return readBinary(file);
|
||||
} else {
|
||||
throw "both async and sync fetching of the wasm failed";
|
||||
}
|
||||
} catch (err) {
|
||||
abort(err);
|
||||
}
|
||||
}
|
||||
function getBinaryPromise() {
|
||||
if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
|
||||
if (typeof fetch === "function" && !isFileURI(wasmBinaryFile)) {
|
||||
return fetch(wasmBinaryFile, { credentials: "same-origin" })
|
||||
.then(function (response) {
|
||||
if (!response["ok"]) {
|
||||
throw "failed to load wasm binary file at '" + wasmBinaryFile + "'";
|
||||
}
|
||||
return response["arrayBuffer"]();
|
||||
})
|
||||
.catch(function () {
|
||||
return getBinary(wasmBinaryFile);
|
||||
});
|
||||
} else {
|
||||
if (readAsync) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
readAsync(
|
||||
wasmBinaryFile,
|
||||
function (response) {
|
||||
resolve(new Uint8Array(response));
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve().then(function () {
|
||||
return getBinary(wasmBinaryFile);
|
||||
});
|
||||
}
|
||||
function createWasm() {
|
||||
var info = { a: asmLibraryArg };
|
||||
function receiveInstance(instance, module) {
|
||||
var exports = instance.exports;
|
||||
Module["asm"] = exports;
|
||||
wasmMemory = Module["asm"]["c"];
|
||||
updateGlobalBufferAndViews(wasmMemory.buffer);
|
||||
wasmTable = Module["asm"]["k"];
|
||||
addOnInit(Module["asm"]["d"]);
|
||||
removeRunDependency("wasm-instantiate");
|
||||
}
|
||||
addRunDependency("wasm-instantiate");
|
||||
function receiveInstantiationResult(result) {
|
||||
receiveInstance(result["instance"]);
|
||||
}
|
||||
function instantiateArrayBuffer(receiver) {
|
||||
return getBinaryPromise()
|
||||
.then(function (binary) {
|
||||
var result = WebAssembly.instantiate(binary, info);
|
||||
return result;
|
||||
})
|
||||
.then(receiver, function (reason) {
|
||||
err("failed to asynchronously prepare wasm: " + reason);
|
||||
abort(reason);
|
||||
});
|
||||
}
|
||||
function instantiateAsync() {
|
||||
if (
|
||||
!wasmBinary &&
|
||||
typeof WebAssembly.instantiateStreaming === "function" &&
|
||||
!isDataURI(wasmBinaryFile) &&
|
||||
!isFileURI(wasmBinaryFile) &&
|
||||
typeof fetch === "function"
|
||||
) {
|
||||
return fetch(wasmBinaryFile, { credentials: "same-origin" }).then(function (response) {
|
||||
var result = WebAssembly.instantiateStreaming(response, info);
|
||||
return result.then(receiveInstantiationResult, function (reason) {
|
||||
err("wasm streaming compile failed: " + reason);
|
||||
err("falling back to ArrayBuffer instantiation");
|
||||
return instantiateArrayBuffer(receiveInstantiationResult);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return instantiateArrayBuffer(receiveInstantiationResult);
|
||||
}
|
||||
}
|
||||
if (Module["instantiateWasm"]) {
|
||||
try {
|
||||
var exports = Module["instantiateWasm"](info, receiveInstance);
|
||||
return exports;
|
||||
} catch (e) {
|
||||
err("Module.instantiateWasm callback failed with error: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
instantiateAsync();
|
||||
return {};
|
||||
}
|
||||
function callRuntimeCallbacks(callbacks) {
|
||||
while (callbacks.length > 0) {
|
||||
var callback = callbacks.shift();
|
||||
if (typeof callback == "function") {
|
||||
callback(Module);
|
||||
continue;
|
||||
}
|
||||
var func = callback.func;
|
||||
if (typeof func === "number") {
|
||||
if (callback.arg === undefined) {
|
||||
wasmTable.get(func)();
|
||||
} else {
|
||||
wasmTable.get(func)(callback.arg);
|
||||
}
|
||||
} else {
|
||||
func(callback.arg === undefined ? null : callback.arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
function _emscripten_memcpy_big(dest, src, num) {
|
||||
HEAPU8.copyWithin(dest, src, src + num);
|
||||
}
|
||||
function emscripten_realloc_buffer(size) {
|
||||
try {
|
||||
wasmMemory.grow((size - buffer.byteLength + 65535) >>> 16);
|
||||
updateGlobalBufferAndViews(wasmMemory.buffer);
|
||||
return 1;
|
||||
} catch (e) {}
|
||||
}
|
||||
function _emscripten_resize_heap(requestedSize) {
|
||||
var oldSize = HEAPU8.length;
|
||||
requestedSize = requestedSize >>> 0;
|
||||
var maxHeapSize = 2147418112;
|
||||
if (requestedSize > maxHeapSize) {
|
||||
return false;
|
||||
}
|
||||
for (var cutDown = 1; cutDown <= 4; cutDown *= 2) {
|
||||
var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown);
|
||||
overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296);
|
||||
var newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536));
|
||||
var replacement = emscripten_realloc_buffer(newSize);
|
||||
if (replacement) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var asmLibraryArg = { a: _emscripten_memcpy_big, b: _emscripten_resize_heap };
|
||||
var asm = createWasm();
|
||||
var ___wasm_call_ctors = (Module["___wasm_call_ctors"] = function () {
|
||||
return (___wasm_call_ctors = Module["___wasm_call_ctors"] = Module["asm"]["d"]).apply(
|
||||
null,
|
||||
arguments
|
||||
);
|
||||
});
|
||||
var _argon2_hash = (Module["_argon2_hash"] = function () {
|
||||
return (_argon2_hash = Module["_argon2_hash"] = Module["asm"]["e"]).apply(null, arguments);
|
||||
});
|
||||
var _malloc = (Module["_malloc"] = function () {
|
||||
return (_malloc = Module["_malloc"] = Module["asm"]["f"]).apply(null, arguments);
|
||||
});
|
||||
var _free = (Module["_free"] = function () {
|
||||
return (_free = Module["_free"] = Module["asm"]["g"]).apply(null, arguments);
|
||||
});
|
||||
var _argon2_verify = (Module["_argon2_verify"] = function () {
|
||||
return (_argon2_verify = Module["_argon2_verify"] = Module["asm"]["h"]).apply(null, arguments);
|
||||
});
|
||||
var _argon2_error_message = (Module["_argon2_error_message"] = function () {
|
||||
return (_argon2_error_message = Module["_argon2_error_message"] = Module["asm"]["i"]).apply(
|
||||
null,
|
||||
arguments
|
||||
);
|
||||
});
|
||||
var _argon2_encodedlen = (Module["_argon2_encodedlen"] = function () {
|
||||
return (_argon2_encodedlen = Module["_argon2_encodedlen"] = Module["asm"]["j"]).apply(
|
||||
null,
|
||||
arguments
|
||||
);
|
||||
});
|
||||
var _argon2_hash_ext = (Module["_argon2_hash_ext"] = function () {
|
||||
return (_argon2_hash_ext = Module["_argon2_hash_ext"] = Module["asm"]["l"]).apply(
|
||||
null,
|
||||
arguments
|
||||
);
|
||||
});
|
||||
var _argon2_verify_ext = (Module["_argon2_verify_ext"] = function () {
|
||||
return (_argon2_verify_ext = Module["_argon2_verify_ext"] = Module["asm"]["m"]).apply(
|
||||
null,
|
||||
arguments
|
||||
);
|
||||
});
|
||||
var stackAlloc = (Module["stackAlloc"] = function () {
|
||||
return (stackAlloc = Module["stackAlloc"] = Module["asm"]["n"]).apply(null, arguments);
|
||||
});
|
||||
Module["allocate"] = allocate;
|
||||
Module["UTF8ToString"] = UTF8ToString;
|
||||
Module["ALLOC_NORMAL"] = ALLOC_NORMAL;
|
||||
var calledRun;
|
||||
function ExitStatus(status) {
|
||||
this.name = "ExitStatus";
|
||||
this.message = "Program terminated with exit(" + status + ")";
|
||||
this.status = status;
|
||||
}
|
||||
dependenciesFulfilled = function runCaller() {
|
||||
if (!calledRun) run();
|
||||
if (!calledRun) dependenciesFulfilled = runCaller;
|
||||
};
|
||||
function run(args) {
|
||||
args = args || arguments_;
|
||||
if (runDependencies > 0) {
|
||||
return;
|
||||
}
|
||||
preRun();
|
||||
if (runDependencies > 0) {
|
||||
return;
|
||||
}
|
||||
function doRun() {
|
||||
if (calledRun) return;
|
||||
calledRun = true;
|
||||
Module["calledRun"] = true;
|
||||
if (ABORT) return;
|
||||
initRuntime();
|
||||
if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"]();
|
||||
postRun();
|
||||
}
|
||||
if (Module["setStatus"]) {
|
||||
Module["setStatus"]("Running...");
|
||||
setTimeout(function () {
|
||||
setTimeout(function () {
|
||||
Module["setStatus"]("");
|
||||
}, 1);
|
||||
doRun();
|
||||
}, 1);
|
||||
} else {
|
||||
doRun();
|
||||
}
|
||||
}
|
||||
Module["run"] = run;
|
||||
if (Module["preInit"]) {
|
||||
if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]];
|
||||
while (Module["preInit"].length > 0) {
|
||||
Module["preInit"].pop()();
|
||||
}
|
||||
}
|
||||
run();
|
||||
if (typeof module !== "undefined") module.exports = Module;
|
||||
Module.unloadRuntime = function () {
|
||||
if (typeof self !== "undefined") {
|
||||
delete self.Module;
|
||||
}
|
||||
Module =
|
||||
jsModule =
|
||||
wasmMemory =
|
||||
wasmTable =
|
||||
asm =
|
||||
buffer =
|
||||
HEAP8 =
|
||||
HEAPU8 =
|
||||
HEAP16 =
|
||||
HEAPU16 =
|
||||
HEAP32 =
|
||||
HEAPU32 =
|
||||
HEAPF32 =
|
||||
HEAPF64 =
|
||||
undefined;
|
||||
if (typeof module !== "undefined") {
|
||||
delete module.exports;
|
||||
}
|
||||
};
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user