-
Notifications
You must be signed in to change notification settings - Fork 0
feat(crypto): Phase 1 - AES-GCM-256 Encryption Utilities #177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1b523d5
feat(crypto): implement Phase 1 - AES-GCM-256 encryption utilities
kevalyq 43551ed
fix(crypto): Address Copilot review - remove NIST placeholder, fix duβ¦
kevalyq 93ff9c9
docs: fix markdownlint - add language identifiers to code blocks
kevalyq b9cb180
fix(docs): address Copilot comments - remove orphaned JSDoc, fix markβ¦
kevalyq d40451e
fix(crypto): add missing BufferSource cast for iv parameter in decrypβ¦
kevalyq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| // SPDX-FileCopyrightText: 2025 SecPal | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| import { describe, it, expect } from "vitest"; | ||
| import { calculateChecksum, verifyChecksum } from "./checksum"; | ||
| import { CHECKSUM_TEST_VECTORS } from "./testVectors"; | ||
|
|
||
| describe("SHA-256 Checksums", () => { | ||
| describe("calculateChecksum", () => { | ||
| it("should calculate SHA-256 checksum for empty input", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[0]; | ||
| const checksum = await calculateChecksum(testVector.input); | ||
|
|
||
| expect(checksum).toBe(testVector.expectedChecksum); | ||
| }); | ||
|
|
||
| it("should calculate SHA-256 checksum for simple text", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[1]; | ||
| const checksum = await calculateChecksum(testVector.input); | ||
|
|
||
| expect(checksum).toBe(testVector.expectedChecksum); | ||
| }); | ||
|
|
||
| it("should calculate SHA-256 checksum for binary data", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[2]; | ||
| const checksum = await calculateChecksum(testVector.input); | ||
|
|
||
| expect(checksum).toBe(testVector.expectedChecksum); | ||
| }); | ||
|
|
||
| it("should return lowercase hex string", async () => { | ||
| const data = new TextEncoder().encode("test"); | ||
| const checksum = await calculateChecksum(data); | ||
|
|
||
| expect(checksum).toMatch(/^[0-9a-f]{64}$/); // 64 hex chars (256 bits) | ||
| expect(checksum).toBe(checksum.toLowerCase()); | ||
| }); | ||
|
|
||
| it("should produce consistent results for same input", async () => { | ||
| const data = new TextEncoder().encode("SecPal Test Data"); | ||
|
|
||
| const checksum1 = await calculateChecksum(data); | ||
| const checksum2 = await calculateChecksum(data); | ||
|
|
||
| expect(checksum1).toBe(checksum2); | ||
| }); | ||
|
|
||
| it("should produce different checksums for different inputs", async () => { | ||
| const data1 = new TextEncoder().encode("file1.pdf"); | ||
| const data2 = new TextEncoder().encode("file2.pdf"); | ||
|
|
||
| const checksum1 = await calculateChecksum(data1); | ||
| const checksum2 = await calculateChecksum(data2); | ||
|
|
||
| expect(checksum1).not.toBe(checksum2); | ||
| }); | ||
|
|
||
| it("should handle large data (1MB)", async () => { | ||
| const largeData = new Uint8Array(1024 * 1024).fill(0x42); | ||
|
|
||
| const checksum = await calculateChecksum(largeData); | ||
|
|
||
| expect(checksum).toMatch(/^[0-9a-f]{64}$/); | ||
| expect(checksum.length).toBe(64); // 256 bits in hex | ||
| }); | ||
| }); | ||
|
|
||
| describe("verifyChecksum", () => { | ||
| it("should return true for matching checksums (empty input)", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[0]; | ||
|
|
||
| const isValid = await verifyChecksum( | ||
| testVector.input, | ||
| testVector.expectedChecksum | ||
| ); | ||
|
|
||
| expect(isValid).toBe(true); | ||
| }); | ||
|
|
||
| it("should return true for matching checksums (simple text)", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[1]; | ||
|
|
||
| const isValid = await verifyChecksum( | ||
| testVector.input, | ||
| testVector.expectedChecksum | ||
| ); | ||
|
|
||
| expect(isValid).toBe(true); | ||
| }); | ||
|
|
||
| it("should return true for matching checksums (binary data)", async () => { | ||
| const testVector = CHECKSUM_TEST_VECTORS[2]; | ||
|
|
||
| const isValid = await verifyChecksum( | ||
| testVector.input, | ||
| testVector.expectedChecksum | ||
| ); | ||
|
|
||
| expect(isValid).toBe(true); | ||
| }); | ||
|
|
||
| it("should return false for mismatched checksums", async () => { | ||
| const data = new TextEncoder().encode("test data"); | ||
| const wrongChecksum = "a".repeat(64); // Invalid checksum | ||
|
|
||
| const isValid = await verifyChecksum(data, wrongChecksum); | ||
|
|
||
| expect(isValid).toBe(false); | ||
| }); | ||
|
|
||
| it("should return false if data is tampered", async () => { | ||
| const originalData = new TextEncoder().encode("original data"); | ||
| const expectedChecksum = await calculateChecksum(originalData); | ||
|
|
||
| const tamperedData = new TextEncoder().encode("tampered data"); | ||
| const isValid = await verifyChecksum(tamperedData, expectedChecksum); | ||
|
|
||
| expect(isValid).toBe(false); | ||
| }); | ||
|
|
||
| it("should handle uppercase and lowercase checksums", async () => { | ||
| const data = new TextEncoder().encode("test"); | ||
| const checksum = await calculateChecksum(data); | ||
|
|
||
| const uppercaseChecksum = checksum.toUpperCase(); | ||
|
|
||
| const isValidLower = await verifyChecksum(data, checksum); | ||
| const isValidUpper = await verifyChecksum(data, uppercaseChecksum); | ||
|
|
||
| expect(isValidLower).toBe(true); | ||
| expect(isValidUpper).toBe(true); | ||
| }); | ||
|
|
||
| it("should reject invalid checksum format", async () => { | ||
| const data = new TextEncoder().encode("test"); | ||
|
|
||
| // Too short | ||
| const tooShort = "abc123"; | ||
| const validShort = await verifyChecksum(data, tooShort); | ||
| expect(validShort).toBe(false); | ||
|
|
||
| // Invalid characters | ||
| const invalidChars = "z".repeat(64); | ||
| const validChars = await verifyChecksum(data, invalidChars); | ||
| expect(validChars).toBe(false); | ||
| }); | ||
|
|
||
| it("should verify large data (1MB)", async () => { | ||
| const largeData = new Uint8Array(1024 * 1024).fill(0x42); | ||
| const checksum = await calculateChecksum(largeData); | ||
|
|
||
| const isValid = await verifyChecksum(largeData, checksum); | ||
|
|
||
| expect(isValid).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Integration tests", () => { | ||
| it("should verify freshly calculated checksums", async () => { | ||
| const testData = [ | ||
| new TextEncoder().encode("file1.pdf"), | ||
| new TextEncoder().encode("image.jpg"), | ||
| new Uint8Array([0x00, 0x01, 0x02, 0x03]), | ||
| new Uint8Array(1024).fill(0xff), | ||
| ]; | ||
|
|
||
| for (const data of testData) { | ||
| const checksum = await calculateChecksum(data); | ||
| const isValid = await verifyChecksum(data, checksum); | ||
|
|
||
| expect(isValid).toBe(true); | ||
| } | ||
| }); | ||
|
|
||
| it("should detect single-bit tampering", async () => { | ||
| const originalData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04]); | ||
| const checksum = await calculateChecksum(originalData); | ||
|
|
||
| // Flip one bit | ||
| const tamperedData = new Uint8Array(originalData); | ||
| if (tamperedData.length > 2 && tamperedData[2] !== undefined) { | ||
| tamperedData[2] ^= 0x01; // Flip bit in 3rd byte | ||
| } | ||
|
|
||
| const isValid = await verifyChecksum(tamperedData, checksum); | ||
|
|
||
| expect(isValid).toBe(false); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // SPDX-FileCopyrightText: 2025 SecPal | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| /** | ||
| * SHA-256 Checksum Module | ||
| * | ||
| * Provides file integrity verification using SHA-256 checksums. | ||
| * Checksums are calculated on: | ||
| * - Original plaintext (before encryption) | ||
| * - Encrypted ciphertext (after encryption) | ||
| * | ||
| * This allows verification of: | ||
| * - Plaintext integrity before encryption | ||
| * - Ciphertext integrity during transit/storage | ||
| * - Decrypted plaintext integrity after decryption | ||
| * | ||
| * @module lib/crypto/checksum | ||
| */ | ||
|
|
||
| /** | ||
| * Calculate SHA-256 checksum of data | ||
| * | ||
| * @param data - Binary data to hash | ||
| * @returns Promise resolving to hex-encoded SHA-256 checksum (64 characters) | ||
| * @throws {Error} If hashing fails | ||
| */ | ||
| export async function calculateChecksum(data: Uint8Array): Promise<string> { | ||
| const hashBuffer = await crypto.subtle.digest( | ||
| "SHA-256", | ||
| data as BufferSource | ||
| ); | ||
| const hashArray = new Uint8Array(hashBuffer); | ||
|
|
||
| // Convert to hex string | ||
| return Array.from(hashArray) | ||
| .map((byte) => byte.toString(16).padStart(2, "0")) | ||
| .join(""); | ||
| } | ||
|
|
||
| /** | ||
| * Verify data integrity by comparing checksums | ||
| * | ||
| * @param data - Binary data to verify | ||
| * @param expectedChecksum - Expected SHA-256 checksum (hex string, case-insensitive) | ||
| * @returns Promise resolving to true if checksums match, false otherwise | ||
| * @throws {Error} If checksum calculation fails | ||
| */ | ||
| export async function verifyChecksum( | ||
| data: Uint8Array, | ||
| expectedChecksum: string | ||
| ): Promise<boolean> { | ||
| // Validate checksum format (64 hex characters) | ||
| const checksumRegex = /^[0-9a-fA-F]{64}$/; | ||
| if (!checksumRegex.test(expectedChecksum)) { | ||
| return false; | ||
| } | ||
|
|
||
| const actualChecksum = await calculateChecksum(data); | ||
|
|
||
| // Case-insensitive comparison | ||
| return actualChecksum.toLowerCase() === expectedChecksum.toLowerCase(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.