Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 123 additions & 5 deletions src/utils/helpers/CryptoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,91 @@
return computed === hash;
}

/**
* Encrypt a string using AES-256-GCM
* @param str - Plaintext string to encrypt
* @param key - Secret key string
* @returns Base64 encoded string containing [iv:12bytes][ciphertext]
* @security Uses AES-GCM with a random 96-bit IV for each encryption
*/
static async encrypt(str: string, key: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);

// Derive a 256-bit key from the input string using SHA-256
const keyHash = await this.hashBytes(key);

const cryptoKey = await globalThis.crypto.subtle.importKey(

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / size

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / Run Tests (20.x)

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (20.x, ubuntu-latest)

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / Run Tests (22.x)

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (22.x, ubuntu-latest)

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / coverage

No overload matches this call.

Check failure on line 251 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (20.x, windows-latest)

No overload matches this call.
"raw",
keyHash,
{ name: "AES-GCM" } as any,
false,
["encrypt"]
);

const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await globalThis.crypto.subtle.encrypt(
{ name: "AES-GCM", iv } as any,
cryptoKey,
data
);

const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);

return this.uint8ArrayToBase64(result);
}

/**
* Decrypt a string using AES-256-GCM
* @param encryptedBase64 - Base64 encoded string containing [iv:12bytes][ciphertext]
* @param key - Secret key string
* @returns Decrypted plaintext string
* @throws {Error} If decryption fails
*/
static async decrypt(
encryptedBase64: string,
key: string
): Promise<string> {
const combined = this.base64ToUint8Array(encryptedBase64);
if (combined.length < 13) {
throw new Error("Invalid encrypted data: too short");
}

const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);

const keyHash = await this.hashBytes(key);

const cryptoKey = await globalThis.crypto.subtle.importKey(

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / size

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / Run Tests (20.x)

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (20.x, ubuntu-latest)

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / Run Tests (22.x)

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (22.x, ubuntu-latest)

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / coverage

No overload matches this call.

Check failure on line 294 in src/utils/helpers/CryptoUtils.ts

View workflow job for this annotation

GitHub Actions / build (20.x, windows-latest)

No overload matches this call.
"raw",
keyHash,
{ name: "AES-GCM" } as any,
false,
["decrypt"]
);

try {
const decrypted = await globalThis.crypto.subtle.decrypt(
{ name: "AES-GCM", iv } as any,
cryptoKey,
ciphertext
);

return new TextDecoder().decode(decrypted);
} catch (error) {
throw new Error(`Decryption failed: ${error}`);
}
}

/**
* Encrypt string (simple XOR - NOT for production)
* For production, use proper encryption libraries like TweetNaCl or libsodium
* @deprecated XOR encryption is NOT cryptographically secure. Use proper encryption libraries.
* @deprecated XOR encryption is NOT cryptographically secure. Use encrypt() instead.
*/
static xorEncrypt(str: string, key: string): string {
console.warn(
"WARNING: XOR encryption is not cryptographically secure. Use proper encryption libraries like TweetNaCl or libsodium."
"WARNING: XOR encryption is not cryptographically secure. Use encrypt() for a secure alternative."
);
let result = "";
for (let i = 0; i < str.length; i++) {
Expand All @@ -254,11 +331,11 @@

/**
* Decrypt string (simple XOR - NOT for production)
* @deprecated XOR encryption is NOT cryptographically secure. Use proper encryption libraries.
* @deprecated XOR encryption is NOT cryptographically secure. Use decrypt() instead.
*/
static xorDecrypt(encrypted: string, key: string): string {
console.warn(
"WARNING: XOR encryption is not cryptographically secure. Use proper encryption libraries like TweetNaCl or libsodium."
"WARNING: XOR encryption is not cryptographically secure. Use decrypt() for a secure alternative."
);
const str = this.base64Decode(encrypted);
let result = "";
Expand All @@ -270,6 +347,47 @@
return result;
}

/**
* Internal helper to hash a string and return bytes
*/
private static async hashBytes(str: string): Promise<Uint8Array> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await globalThis.crypto.subtle.digest(
"SHA-256",
data
);
return new Uint8Array(hashBuffer);
}

/**
* Internal helper for bytes to base64
*/
private static uint8ArrayToBase64(bytes: Uint8Array): string {
if (typeof Buffer !== "undefined") {
return Buffer.from(bytes).toString("base64");
}
const binaryString = Array.from(bytes, (byte) =>
String.fromCharCode(byte)

Check warning on line 371 in src/utils/helpers/CryptoUtils.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String.fromCodePoint()` over `String.fromCharCode()`.

See more on https://sonarcloud.io/project/issues?id=sebamar88_bytekit&issues=AZz8pKtCgSeiREYWm_zT&open=AZz8pKtCgSeiREYWm_zT&pullRequest=7
).join("");
return globalThis.btoa(binaryString);
}

/**
* Internal helper for base64 to bytes
*/
private static base64ToUint8Array(base64: string): Uint8Array {
if (typeof Buffer !== "undefined") {
return new Uint8Array(Buffer.from(base64, "base64"));
}
const binaryString = globalThis.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);

Check warning on line 386 in src/utils/helpers/CryptoUtils.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#codePointAt()` over `String#charCodeAt()`.

See more on https://sonarcloud.io/project/issues?id=sebamar88_bytekit&issues=AZz8pKtCgSeiREYWm_zU&open=AZz8pKtCgSeiREYWm_zU&pullRequest=7
}
return bytes;
}

/**
* Generate random bytes
*/
Expand Down
52 changes: 46 additions & 6 deletions tests/crypto-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ test("CryptoUtils.verifyHash works correctly", async () => {
assert.equal(await CryptoUtils.verifyHash(data, "wrong-hash"), false);
});

test("CryptoUtils.encrypt and decrypt", async () => {
const original = "secret message";
const key = "pass";

const encrypted = await CryptoUtils.encrypt(original, key);
assert.notEqual(encrypted, original);

const decrypted = await CryptoUtils.decrypt(encrypted, key);
assert.equal(decrypted, original);
});

test("CryptoUtils.xorEncrypt and xorDecrypt", () => {
const original = "secret message";
const key = "pass";
Expand All @@ -91,6 +102,35 @@ test("CryptoUtils.xorEncrypt and xorDecrypt", () => {
assert.equal(decrypted, original);
});

test("CryptoUtils.encrypt is non-deterministic (uses random IV)", async () => {
const original = "secret message";
const key = "pass";

const encrypted1 = await CryptoUtils.encrypt(original, key);
const encrypted2 = await CryptoUtils.encrypt(original, key);

assert.notEqual(encrypted1, encrypted2);

const decrypted1 = await CryptoUtils.decrypt(encrypted1, key);
const decrypted2 = await CryptoUtils.decrypt(encrypted2, key);

assert.equal(decrypted1, original);
assert.equal(decrypted2, original);
});

test("CryptoUtils.decrypt fails with wrong key", async () => {
const original = "secret message";
const key = "pass";
const wrongKey = "wrong";

const encrypted = await CryptoUtils.encrypt(original, key);

await assert.rejects(
async () => await CryptoUtils.decrypt(encrypted, wrongKey),
/Decryption failed/
);
});

test("CryptoUtils.randomBytes generates Uint8Array", () => {
const bytes = CryptoUtils.randomBytes(16);
assert.ok(bytes instanceof Uint8Array);
Expand Down Expand Up @@ -243,9 +283,9 @@ test("CryptoUtils.hmac with default algorithm", async () => {
assert.match(hmac, /^[0-9a-f]+$/);
});

test("CryptoUtils.xorEncrypt handles empty strings", () => {
const encrypted = CryptoUtils.xorEncrypt("", "key");
const decrypted = CryptoUtils.xorDecrypt(encrypted, "key");
test("CryptoUtils.encrypt handles empty strings", async () => {
const encrypted = await CryptoUtils.encrypt("", "key");
const decrypted = await CryptoUtils.decrypt(encrypted, "key");
assert.equal(decrypted, "");
});

Expand Down Expand Up @@ -282,12 +322,12 @@ test("CryptoUtils.randomBytes generates different values", () => {
assert.notDeepEqual(bytes1, bytes2);
});

test("CryptoUtils.xorEncrypt with long key", () => {
test("CryptoUtils.encrypt with long key", async () => {
const original = "short";
const key = "very-long-encryption-key-that-exceeds-message-length";

const encrypted = CryptoUtils.xorEncrypt(original, key);
const decrypted = CryptoUtils.xorDecrypt(encrypted, key);
const encrypted = await CryptoUtils.encrypt(original, key);
const decrypted = await CryptoUtils.decrypt(encrypted, key);

assert.equal(decrypted, original);
});
Loading