diff --git a/README.md b/README.md index 3b5ca51..ae07319 100644 --- a/README.md +++ b/README.md @@ -18,22 +18,14 @@ This project is a monorepo that contains the following packages: ## Features -This package provides two main modes for image fragmentation: +This package provides image fragmentation functionality: -### πŸ”€ Shuffle Only Mode -- If `secretKey` is not set, only shuffling is performed. +### πŸ”€ Shuffle Mode ``` Original Image β†’ Load β†’ Convert to RGBA β†’ Shuffle β†’ Fragmented PNG Output ``` -### πŸ” Shuffle + Encrypt Mode (Deprecated) -- If `secretKey` is set, both shuffling and encryption are performed. - -``` -Original Image β†’ Load β†’ Convert to RGBA β†’ Encrypt β†’ Shuffle β†’ Fragmented PNG Output -``` - ## Installation ``` @@ -46,11 +38,9 @@ npm i image-shield import ImageShield from "image-shield"; ``` -### πŸ”€ Shuffle Only Mode - -If you do not set the `secretKey`, only shuffling will be applied to the images. +### πŸ”€ Shuffle Mode -**Encrypt** +**Fragment** ```ts await ImageShield.encrypt({ @@ -61,7 +51,6 @@ await ImageShield.encrypt({ "./input_3.png", ], outputDir: "./output/fragmented", - // secretKey: undefined }); ``` @@ -89,7 +78,7 @@ output -**Decrypt** +**Restore** ```ts await ImageShield.decrypt({ @@ -100,7 +89,6 @@ await ImageShield.decrypt({ "./output/fragmented/img_3_fragmented.png", ], outputDir: "./output/restored", - // secretKey: undefined }); ``` @@ -129,88 +117,6 @@ output --- -### πŸ” Shuffle + Encryptγ€€Mode - -If you set the `secretKey`, both shuffling and encryption will be applied to the images. - -**Encrypt** - -```ts -await ImageShield.encrypt({ - // config: { /** FragmentationConfig */ }, - imagePaths: [ - "./input_1.png", - "./input_2.png", - "./input_3.png", - ], - outputDir: "./output/fragmented", - secretKey: "secret", -}); -``` - -
-Output: - -``` -output -└── fragmented - β”œβ”€β”€ img_1_fragmented.png - β”œβ”€β”€ img_2_fragmented.png - β”œβ”€β”€ img_3_fragmented.png - └── manifest.json -``` - -| input 1 | input 2 | input 3 | -|:-------:|:---------------:|:---------------:| -| ![](.docs/input_sample.png) | ![](.docs/input_sample_mono.png) | ![](.docs/input_sample_blue.png) | -| 500 x 500px (109KB) | 400 x 600px (4KB) | 600 x 400px (3KB) | - -| output 1 | output 2 | output 3 | -|:-------:|:---------------:|:---------------:| -| ![](.docs/fragmented2/img_1_fragmented.png) | ![](.docs/fragmented2/img_2_fragmented.png) | ![](.docs/fragmented2/img_3_fragmented.png) | -| 494 x 494px (976KB) | 494 x 494px (976KB) | 494 x 494px (976KB) | - -
- -**Decrypt** - -```ts -await ImageShield.decrypt({ - manifestPath: "./output/fragmented/manifest.json", - imagePaths: [ - "./output/fragmented/img_1_fragmented.png", - "./output/fragmented/img_2_fragmented.png", - "./output/fragmented/img_3_fragmented.png", - ], - outputDir: "./output/restored", - secretKey: "secret", -}); -``` - -
-Output: - -``` -output -└── restored - β”œβ”€β”€ img_1.png - β”œβ”€β”€ img_2.png - └── img_3.png -``` - -| input 1 | input 2 | input 3 | -|:-------:|:---------------:|:---------------:| -| ![](.docs/fragmented2/img_1_fragmented.png) | ![](.docs/fragmented2/img_2_fragmented.png) | ![](.docs/fragmented2/img_3_fragmented.png) | -| 494 x 494px (976KB) | 494 x 494px (976KB) | 494 x 494px (976KB) | - -| output 1 | output 2 | output 3 | -|:-------:|:---------------:|:---------------:| -| ![](.docs/restored2/img_1.png) | ![](.docs/restored2/img_2.png) | ![](.docs/restored2/img_3.png) | -| 500 x 500px (117KB) | 400 x 600px (2KB) | 600 x 400px (2KB) | - -
- - ## Shuffle Overview ### List by blockSize @@ -272,9 +178,7 @@ manifest.json: "x": 245, "y": 245 } - ], - "algorithm": "aes-256-cbc", - "secure": true + ] } ``` @@ -282,7 +186,7 @@ manifest.json: --- > [!NOTE] -> - The `manifest.json` file contains the necessary information for restoration, but it does not include the secret key. +> - The `manifest.json` file contains the necessary information for restoration. > - Input images are converted to PNG format. ## Clients diff --git a/packages/cli/README.md b/packages/cli/README.md index 9662acd..5d3660d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -28,14 +28,14 @@ Options: -h, --help display help for command Commands: - encrypt [options] Fragment and encrypt images + encrypt [options] Fragment images decrypt [options] Restore fragmented images help [command] display help for command ``` ### Encrypt Command -Fragment and optionally encrypt images into multiple pieces. +Fragment images into multiple pieces. ```bash image-shield encrypt -o [options] @@ -46,11 +46,10 @@ image-shield encrypt -o [options] | Option | Description | Required | Default | |--------|-------------|----------|---------| | `-o, --output ` | Output directory for fragments and manifest | βœ… | - | -| `-k, --key ` | Secret key for encryption | ❌ | - | | `-b, --block-size ` | Pixel block size (positive integer) | ❌ | 10 | | `-p, --prefix ` | Prefix for fragment files | ❌ | "fragment" | | `-s, --seed ` | Random seed (integer) | ❌ | auto-generated | -| `--restore-filename` | Restore original file names when decrypting | ❌ | false | +| `--restore-filename` | Restore original file names when restoring | ❌ | false | #### Examples @@ -59,11 +58,6 @@ image-shield encrypt -o [options] image-shield encrypt image1.jpg image2.png -o ./fragments ``` -**With encryption:** -```bash -image-shield encrypt image.jpg -o ./secure-fragments -k mySecretKey123 -``` - **Custom configuration:** ```bash image-shield encrypt *.jpg -o ./output -b 20 -p "my_fragment" --restore-filename @@ -76,7 +70,7 @@ image-shield encrypt image.png -o ./output -s 12345 #### Output Structure -After encryption, the output directory will contain: +After fragmentation, the output directory will contain: ``` output/ β”œβ”€β”€ manifest.json # Metadata for restoration @@ -99,7 +93,6 @@ image-shield decrypt -m -o [op |--------|-------------|----------| | `-m, --manifest ` | Path to the manifest.json file | βœ… | | `-o, --output ` | Output directory for restored images | βœ… | -| `-k, --key ` | Secret key for decryption (if encrypted) | ❌ | #### Examples @@ -108,11 +101,6 @@ image-shield decrypt -m -o [op image-shield decrypt ./fragments/*.png -m ./fragments/manifest.json -o ./restored ``` -**With decryption:** -```bash -image-shield decrypt ./secure-fragments/*.png -m ./secure-fragments/manifest.json -o ./restored -k mySecretKey123 -``` - **Specific fragments:** ```bash image-shield decrypt fragment_0000.png fragment_0001.png fragment_0002.png -m manifest.json -o ./output @@ -124,7 +112,7 @@ The CLI provides clear error messages for common issues: - **File not found**: When input images or manifest don't exist - **Invalid options**: When required options are missing or invalid -- **Decryption errors**: When wrong key is provided or fragments are corrupted +- **Restoration errors**: When fragments are corrupted or manifest doesn't match - **Permission errors**: When output directory cannot be created ## Examples Workflow @@ -137,13 +125,13 @@ The CLI provides clear error messages for common issues: # photo1.jpg photo2.png document.pdf ``` -2. **Fragment and encrypt:** +2. **Fragment images:** ```bash - image-shield encrypt images/photo1.jpg images/photo2.png -o ./backup -k "myPassword123" --restore-filename + image-shield encrypt images/photo1.jpg images/photo2.png -o ./backup --restore-filename ``` ``` - πŸ” Starting image encryption... - βœ… Images encrypted successfully to: /path/to/backup + πŸ”€ Starting image fragmentation... + βœ… Images fragmented successfully to: /path/to/backup ``` 3. **Check output:** @@ -154,10 +142,10 @@ The CLI provides clear error messages for common issues: 4. **Restore images:** ```bash - image-shield decrypt backup/*.png -m backup/manifest.json -o ./restored -k "myPassword123" + image-shield decrypt backup/*.png -m backup/manifest.json -o ./restored ``` ``` - πŸ”“ Starting image decryption... + πŸ”€ Starting image restoration... βœ… Images restored successfully to: /path/to/restored ``` @@ -169,13 +157,12 @@ The CLI provides clear error messages for common issues: ### Advanced Configuration Example -For maximum security and custom fragmentation: +For custom fragmentation: ```bash -# Encrypt with custom settings +# Fragment with custom settings image-shield encrypt sensitive/*.jpg \ -o ./vault \ - -k "$(openssl rand -hex 32)" \ -b 5 \ -p "secure_chunk" \ -s 42 \ @@ -193,14 +180,12 @@ image-shield encrypt sensitive/*.jpg \ # Backup script IMAGES_DIR="./photos" -BACKUP_DIR="./encrypted_backup" -SECRET_KEY="your-secret-key" +BACKUP_DIR="./backup" # Create backup -echo "Creating encrypted backup..." +echo "Creating backup..." image-shield encrypt "$IMAGES_DIR"/*.{jpg,png} \ -o "$BACKUP_DIR" \ - -k "$SECRET_KEY" \ --restore-filename if [ $? -eq 0 ]; then @@ -218,16 +203,14 @@ fi #!/bin/bash # Recovery script -BACKUP_DIR="./encrypted_backup" +BACKUP_DIR="./backup" RESTORE_DIR="./recovered_photos" -SECRET_KEY="your-secret-key" # Restore from backup -echo "Restoring from encrypted backup..." +echo "Restoring from backup..." image-shield decrypt "$BACKUP_DIR"/fragment_*.png \ -m "$BACKUP_DIR/manifest.json" \ - -o "$RESTORE_DIR" \ - -k "$SECRET_KEY" + -o "$RESTORE_DIR" if [ $? -eq 0 ]; then echo "βœ… Recovery completed successfully" diff --git a/packages/cli/src/commands/decrypt.ts b/packages/cli/src/commands/decrypt.ts index bdcfa0e..c8345b4 100644 --- a/packages/cli/src/commands/decrypt.ts +++ b/packages/cli/src/commands/decrypt.ts @@ -18,7 +18,6 @@ export function registerDecryptCommand(program: Command): void { .argument("", "Fragment file paths") .requiredOption("-m, --manifest ", "Manifest file path") .requiredOption("-o, --output ", "Output directory") - .option("-k, --key ", "Secret key for decryption") .action(handleDecryptCommand); } @@ -32,7 +31,7 @@ async function handleDecryptCommand( options: DecryptOptions, ): Promise { try { - console.log("πŸ”“ Starting image decryption..."); + console.log("πŸ”€ Starting image restoration..."); const imagePaths = validateImagePaths(fragments); const manifestPath = validateManifestPath(options.manifest); @@ -42,13 +41,12 @@ async function handleDecryptCommand( imagePaths, manifestPath, outputDir, - secretKey: options.key, }); console.log(`βœ… Images restored successfully to: ${outputDir}`); } catch (error) { console.error( - `❌ Decryption failed: ${error instanceof Error ? error.message : String(error)}`, + `❌ Restoration failed: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } diff --git a/packages/cli/src/commands/encrypt.ts b/packages/cli/src/commands/encrypt.ts index f41c558..c528241 100644 --- a/packages/cli/src/commands/encrypt.ts +++ b/packages/cli/src/commands/encrypt.ts @@ -11,10 +11,9 @@ import { validateImagePaths, validateOutputDirectory } from "../validators"; export function registerEncryptCommand(program: Command): void { program .command("encrypt") - .description("Fragment and encrypt images") + .description("Fragment images") .argument("", "Input image file paths") .requiredOption("-o, --output ", "Output directory") - .option("-k, --key ", "Secret key for encryption") .option("-b, --block-size ", "Pixel block size", (value: string) => { const num = Number.parseInt(value, 10); if (Number.isNaN(num) || num <= 0) { @@ -44,7 +43,7 @@ async function handleEncryptCommand( options: EncryptOptions, ): Promise { try { - console.log("πŸ” Starting image encryption..."); + console.log("πŸ”€ Starting image fragmentation..."); const imagePaths = validateImagePaths(images); const outputDir = validateOutputDirectory(options.output); @@ -59,13 +58,12 @@ async function handleEncryptCommand( imagePaths, outputDir, config: Object.keys(config).length > 0 ? config : undefined, - secretKey: options.key, }); - console.log(`βœ… Images encrypted successfully to: ${outputDir}`); + console.log(`βœ… Images fragmented successfully to: ${outputDir}`); } catch (error) { console.error( - `❌ Encryption failed: ${error instanceof Error ? error.message : String(error)}`, + `❌ Fragmentation failed: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 93ec746..d0e8fe4 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,6 +1,5 @@ export interface EncryptOptions { output: string; - key?: string; blockSize?: number; prefix?: string; seed?: number; @@ -10,5 +9,4 @@ export interface EncryptOptions { export interface DecryptOptions { manifest: string; output: string; - key?: string; } diff --git a/packages/core/src/block-utils.ts b/packages/core/src/block-utils.ts index 0045ae1..b8b7bb1 100644 --- a/packages/core/src/block-utils.ts +++ b/packages/core/src/block-utils.ts @@ -1,3 +1,31 @@ +/** + * Number of channels in RGBA format + */ +export const RGBA_CHANNELS = 4; + +export interface BlockCounts { + blockCountX: number; + blockCountY: number; +} + +/** + * Calculate block counts for width and height + * @param width Image width + * @param height Image height + * @param blockSize Block size + * @returns Object with blockCountX and blockCountY + */ +export function calculateBlockCounts( + width: number, + height: number, + blockSize: number, +): BlockCounts { + return { + blockCountX: Math.ceil(width / blockSize), + blockCountY: Math.ceil(height / blockSize), + }; +} + /** * Calculate how many blocks each fragment should contain * @param totalBlocks Total number of blocks to distribute diff --git a/packages/core/src/crypto.ts b/packages/core/src/crypto.ts deleted file mode 100644 index 3c1ddd2..0000000 --- a/packages/core/src/crypto.ts +++ /dev/null @@ -1,51 +0,0 @@ -export class InvalidUUIDFormatError extends Error { - constructor(message: string) { - super(message); - this.name = "InvalidUUIDFormatError"; - } -} - -export interface CryptoProvider { - encryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer; - decryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer; - keyTo32(key: string): Buffer; - generateUUID(): string; - uuidToIV(uuid: string): Buffer; -} - -export class CryptoUtils { - private static provider: CryptoProvider | null = null; - - static setProvider(provider: CryptoProvider): void { - CryptoUtils.provider = provider; - } - - static getProvider(): CryptoProvider { - if (!CryptoUtils.provider) { - throw new Error( - "Crypto provider not set. Call CryptoUtils.setProvider() first.", - ); - } - return CryptoUtils.provider; - } - - static encryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer { - return CryptoUtils.getProvider().encryptBuffer(buffer, key, iv); - } - - static decryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer { - return CryptoUtils.getProvider().decryptBuffer(buffer, key, iv); - } - - static keyTo32(key: string): Buffer { - return CryptoUtils.getProvider().keyTo32(key); - } - - static generateUUID(): string { - return CryptoUtils.getProvider().generateUUID(); - } - - static uuidToIV(uuid: string): Buffer { - return CryptoUtils.getProvider().uuidToIV(uuid); - } -} diff --git a/packages/core/src/helpers.test.ts b/packages/core/src/helpers.test.ts index 344d4cf..e2d08dc 100644 --- a/packages/core/src/helpers.test.ts +++ b/packages/core/src/helpers.test.ts @@ -3,24 +3,9 @@ import { generateFragmentFileName, generateRestoredFileName, generateRestoredOriginalFileName, - verifySecretKey, } from "./helpers"; import type { ManifestData } from "./types"; -describe("verifySecretKey", () => { - test("should return secret key when valid", () => { - expect(verifySecretKey("validKey")).toBe("validKey"); - expect(verifySecretKey(" validKey ")).toBe(" validKey "); - }); - - test("should return undefined when invalid", () => { - expect(verifySecretKey("")).toBeUndefined(); - expect(verifySecretKey(" ")).toBeUndefined(); - expect(verifySecretKey(null)).toBeUndefined(); - expect(verifySecretKey(undefined)).toBeUndefined(); - }); -}); - describe("generateFileName", () => { const mockManifest = { config: { prefix: "img" }, diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 1ccf067..15e4fec 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -1,19 +1,5 @@ import type { ManifestData } from "./types"; -/** - * Verify if a secretKey is valid - * @param secretKey - The secret key to verify - * @returns The secret key if valid, undefined otherwise - */ -export function verifySecretKey( - secretKey: string | undefined | null, -): string | undefined { - if (!!secretKey && secretKey.trim().length > 0) { - return secretKey; - } - return undefined; -} - /** * Generate a file name with prefix, 1-based zero-padded index, and extension * @param manifest - Manifest data diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b92fcba..3a5f49f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,5 @@ export * from "./types"; export * from "./constants"; -export * from "./crypto"; export * from "./helpers"; export * from "./block-utils"; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6968b3b..00e859c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -5,8 +5,6 @@ export interface EncryptOptions { config?: FragmentationConfig; /** Output directory (e.g., "./output/fragments") */ outputDir: string; - /** Secret key (optional) */ - secretKey?: string; } export interface DecryptOptions { @@ -16,8 +14,6 @@ export interface DecryptOptions { manifestPath: string; /** Output directory (e.g., "./output/restored") */ outputDir: string; - /** Secret key (optional) */ - secretKey?: string; } export interface FragmentationConfig { @@ -26,7 +22,7 @@ export interface FragmentationConfig { /** Prefix for fragment files (optional, default: "fragment") */ prefix?: string; /** Random seed (auto-generated if not specified) */ - seed?: number; + seed?: number | string; /** Restore original file name (optional, default: false) */ restoreFileName?: boolean; } @@ -34,32 +30,8 @@ export interface FragmentationConfig { /** * Information about the image before fragmentation. * This includes dimensions, channels, and block counts. - * - * For non-encrypted images: Contains original image information. - * For encrypted images: Contains information after encryption processing. - * Note: This is NOT information about the final fragmented images. - * - * The dimensions may differ from the original image due to encryption padding. */ export interface ImageInfo { - /** Width */ - width: number; - /** Height */ - height: number; - /** Number of channels */ - channels: number; - /** Number of blocks X */ - blockCountX: number; - /** Number of blocks Y */ - blockCountY: number; - /** Original file name (optional) */ - name?: string; -} - -/** - * Shortened version of ImageInfo - */ -export interface ShortImageInfo { /** Width */ w: number; /** Height */ @@ -74,8 +46,6 @@ export interface ShortImageInfo { name?: string; } -export type EncryptionAlgorithm = "aes-256-cbc"; - export interface ManifestData { /** UUID */ id: string; @@ -86,11 +56,7 @@ export interface ManifestData { /** Config */ config: Required; /** Image information */ - images: ShortImageInfo[]; - /** Algorithm (only set if secure is true) */ - algorithm?: EncryptionAlgorithm; - /** Secure (true if encrypted) */ - secure: boolean; + images: ImageInfo[]; } export interface FragmentationResult { diff --git a/packages/node/src/block.test.ts b/packages/node/src/block.test.ts index eb0068f..76a3dba 100644 --- a/packages/node/src/block.test.ts +++ b/packages/node/src/block.test.ts @@ -138,68 +138,6 @@ describe("splitImageToBlocks & blocksToImageBuffer", () => { }); }); -describe("imageFileToBlocks & blocksToPngImage (integration)", () => { - const tmpDir = path.join(tmpdir(), "block_test_tmp"); - const tmpPng = path.join(tmpDir, "test.png"); - const width = 4; - const height = 4; - const blockSize = 2; - const channels = 4; - // RGBA 4x4 pixels = 4*4*4 = 64 bytes - const buffer = Buffer.from([ - // 1st row - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - // 2nd row - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - // 3rd row - 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, - // 4th row - 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, - ]); - - beforeAll(async () => { - // Create tmp directory and PNG file - if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); - const image = new Jimp({ data: buffer, width, height }); - await image.write(tmpPng as `${string}.${string}`); - }); - - afterAll(() => { - // Clean up tmp files - if (fs.existsSync(tmpPng)) fs.unlinkSync(tmpPng); - if (fs.existsSync(tmpDir)) fs.rmdirSync(tmpDir); - }); - - test("imageFileToBlocks splits PNG into correct blocks", async () => { - const { - blocks, - width: w, - height: h, - channels: c, - blockCountX: x, - blockCountY: y, - } = await imageFileToBlocks(tmpPng, blockSize); - expect(w).toBe(width); - expect(h).toBe(height); - expect(c).toBe(channels); - expect(x).toBe(2); - expect(y).toBe(2); - expect(blocks.length).toBe(4); // 2x2 blocks - // Check block contents (top-left block) - expect(blocks[0]).toEqual( - Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24]), - ); - }); - - test("blocksToPngImage reconstructs PNG from blocks", async () => { - const { blocks } = await imageFileToBlocks(tmpPng, blockSize); - const pngBuffer = await blocksToPngImage(blocks, width, height, blockSize); - // Decode PNG and check raw buffer - const jimpImage = await Jimp.read(pngBuffer); - expect(jimpImage.bitmap.data).toEqual(buffer); - }); -}); - describe("calcBlocksPerFragment", () => { test("evenly divisible blocks", () => { // 12 blocks, 3 fragments => [4, 4, 4] @@ -223,8 +161,9 @@ describe("calcBlocksPerFragment", () => { }); }); -describe("integration: fragmentImages and restoreImages", () => { - const tmpDir = path.join(tmpdir(), "block_integration_tmp"); +describe("imageFileToBlocks & blocksToPngImage (integration)", () => { + const tmpDir = path.join(tmpdir(), "block_test_tmp"); + const tmpPng = path.join(tmpDir, "test.png"); const width = 4; const height = 4; const blockSize = 2; @@ -245,13 +184,12 @@ describe("integration: fragmentImages and restoreImages", () => { // Create tmp directory and PNG file if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); const image = new Jimp({ data: buffer, width, height }); - await image.write(path.join(tmpDir, "test.png") as `${string}.${string}`); + await image.write(tmpPng as `${string}.${string}`); }); afterAll(() => { // Clean up tmp files - if (fs.existsSync(path.join(tmpDir, "test.png"))) - fs.unlinkSync(path.join(tmpDir, "test.png")); + if (fs.existsSync(tmpPng)) fs.unlinkSync(tmpPng); if (fs.existsSync(tmpDir)) fs.rmdirSync(tmpDir); }); @@ -263,7 +201,7 @@ describe("integration: fragmentImages and restoreImages", () => { channels: c, blockCountX: x, blockCountY: y, - } = await imageFileToBlocks(path.join(tmpDir, "test.png"), blockSize); + } = await imageFileToBlocks(tmpPng, blockSize); expect(w).toBe(width); expect(h).toBe(height); expect(c).toBe(channels); @@ -277,10 +215,7 @@ describe("integration: fragmentImages and restoreImages", () => { }); test("blocksToPngImage reconstructs PNG from blocks", async () => { - const { blocks } = await imageFileToBlocks( - path.join(tmpDir, "test.png"), - blockSize, - ); + const { blocks } = await imageFileToBlocks(tmpPng, blockSize); const pngBuffer = await blocksToPngImage(blocks, width, height, blockSize); // Decode PNG and check raw buffer const jimpImage = await Jimp.read(pngBuffer); diff --git a/packages/node/src/block.ts b/packages/node/src/block.ts index f3ff3ef..f2e2939 100644 --- a/packages/node/src/block.ts +++ b/packages/node/src/block.ts @@ -1,15 +1,10 @@ -import { CryptoUtils } from "@image-shield/core"; +import { + type BlockCounts, + RGBA_CHANNELS, + calculateBlockCounts, +} from "@image-shield/core"; import { Jimp, JimpMime } from "jimp"; -const RGBA_CHANNELS = 4; -const PNG_UINT32_BYTES = 4; -const PNG_METADATA_SIZE = PNG_UINT32_BYTES * 3; // width + height + imageBufferLength - -interface BlockCounts { - blockCountX: number; - blockCountY: number; -} - interface ImageFileToBlocksResult { blocks: Buffer[]; width: number; @@ -59,40 +54,6 @@ function formatErrorMessage(operation: string, error: unknown): string { return `${operation}: ${errorMessage}`; } -/** - * Convert raw RGBA image buffer to PNG buffer using Jimp - * @param imageBuffer Raw RGBA image buffer - * @param width Image width in pixels - * @param height Image height in pixels - * @returns Promise resolving to PNG buffer - */ -async function imageBufferToPng( - imageBuffer: Buffer, - width: number, - height: number, -): Promise { - const image = createJimpFromImageBuffer(imageBuffer, width, height); - return await image.getBuffer(JimpMime.png); -} - -/** - * Calculate block counts for width and height - * @param width Image width - * @param height Image height - * @param blockSize Block size - * @returns Object with blockCountX and blockCountY - */ -function calculateBlockCounts( - width: number, - height: number, - blockSize: number, -): BlockCounts { - return { - blockCountX: Math.ceil(width / blockSize), - blockCountY: Math.ceil(height / blockSize), - }; -} - /** * Calculate actual block dimensions at edge positions * @param position Block position (x or y) @@ -354,10 +315,9 @@ export async function imageFileToBlocks( blockCountY: blockCounts.blockCountY, }; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - console.error(`Error processing image file: ${errorMessage}`); - throw new Error("The manifest file may not match the image data."); + throw new Error( + `${formatErrorMessage("Failed to process image file", error)}. The manifest file may not match the image data.`, + ); } } @@ -419,168 +379,11 @@ export async function createPngFromImageBuffer( height: number, ): Promise { try { - return await imageBufferToPng(imageBuffer, width, height); + const image = createJimpFromImageBuffer(imageBuffer, width, height); + return await image.getBuffer(JimpMime.png); } catch (error) { throw new Error( formatErrorMessage("Failed to create PNG from image buffer", error), ); } } - -/** - * Create metadata buffer with image dimensions and image buffer length - * @param width Image width - * @param height Image height - * @param imageBufferLength Image buffer length - * @returns Metadata buffer (12 bytes) - */ -export function createImageBufferMetadata( - width: number, - height: number, - imageBufferLength: number, -): Buffer { - const metadata = Buffer.alloc(PNG_METADATA_SIZE); // 4 bytes for width, 4 bytes for height, 4 bytes for data length - metadata.writeUInt32BE(width, 0); - metadata.writeUInt32BE(height, PNG_UINT32_BYTES); - metadata.writeUInt32BE(imageBufferLength, PNG_UINT32_BYTES * 2); - return metadata; -} - -/** - * Parse metadata from buffer - * @param metadataBuffer Buffer containing metadata - * @returns Parsed metadata object - */ -export function parseImageBufferMetadata(metadataBuffer: Buffer): { - width: number; - height: number; - imageBufferLength: number; -} { - const width = metadataBuffer.readUInt32BE(0); - const height = metadataBuffer.readUInt32BE(PNG_UINT32_BYTES); - const imageBufferLength = metadataBuffer.readUInt32BE(PNG_UINT32_BYTES * 2); - return { width, height, imageBufferLength }; -} - -/** - * Remove trailing zero padding from buffer - * @param buffer Buffer to trim - * @returns Trimmed buffer - */ -export function removePadding(buffer: Buffer): Buffer { - let actualDataLength = buffer.length; - for (let i = buffer.length - 1; i >= 0; i--) { - if (buffer[i] !== 0) { - actualDataLength = i + 1; - break; - } - } - return buffer.subarray(0, actualDataLength); -} - -/** - * Calculate optimal square dimensions for given data length - * @param dataLength Length of data in bytes - * @returns Optimal width and height for square-like image - */ -export function calculateOptimalDimensions(dataLength: number): { - width: number; - height: number; -} { - const width = Math.ceil(Math.sqrt(dataLength / RGBA_CHANNELS)); - const height = Math.ceil(dataLength / (width * RGBA_CHANNELS)); - return { width, height }; -} - -/** - * Encrypt PNG image buffer with metadata - * @param pngBuffer Original PNG buffer - * @param secretKey Secret key for encryption - * @param manifestId Manifest ID used for IV generation - * @returns Encrypted PNG buffer - */ -export async function encryptPngImageBuffer( - pngBuffer: Buffer, - secretKey: string, - manifestId: string, -): Promise { - // Extract raw RGBA image buffer using block utility - const { imageBuffer, width, height } = - await extractImageBufferFromPng(pngBuffer); - - // Create metadata with original image buffer size and dimensions - const metadata = createImageBufferMetadata(width, height, imageBuffer.length); - - // Combine metadata and image buffer - const dataToEncrypt = Buffer.concat([metadata, imageBuffer]); - - // Encrypt the combined data - const encryptedData = CryptoUtils.encryptBuffer( - dataToEncrypt, - secretKey, - CryptoUtils.uuidToIV(manifestId), - ); - - // Calculate optimal dimensions for encrypted data - const { width: encryptedWidth, height: encryptedHeight } = - calculateOptimalDimensions(encryptedData.length); - - // Create a padded buffer to fit exactly in the image dimensions - const paddedSize = encryptedWidth * encryptedHeight * RGBA_CHANNELS; - const paddedData = Buffer.alloc(paddedSize); - encryptedData.copy(paddedData, 0); - - // Convert encrypted data back to PNG using createPngFromImageBuffer - return await createPngFromImageBuffer( - paddedData, - encryptedWidth, - encryptedHeight, - ); -} - -/** - * Decrypt PNG image buffer and extract original PNG - * @param encryptedPngBuffer Encrypted PNG buffer - * @param secretKey Secret key for decryption - * @param manifestId Manifest ID used for IV generation - * @returns Original PNG buffer - */ -export async function decryptPngImageBuffer( - encryptedPngBuffer: Buffer, - secretKey: string, - manifestId: string, -): Promise { - // Extract raw RGBA image buffer from encrypted PNG using block utility - const { imageBuffer: encryptedImageData } = - await extractImageBufferFromPng(encryptedPngBuffer); - - // Remove padding from encrypted data - const encryptedData = removePadding(encryptedImageData); - - // Decrypt the data - const decryptedData = CryptoUtils.decryptBuffer( - encryptedData, - secretKey, - CryptoUtils.uuidToIV(manifestId), - ); - - // Parse metadata from the beginning of decrypted data - const { - width: originalWidth, - height: originalHeight, - imageBufferLength: originalImageBufferLength, - } = parseImageBufferMetadata(decryptedData.subarray(0, PNG_METADATA_SIZE)); - - // Extract the original image buffer (skip 12 bytes of metadata) - const originalImageBuffer = decryptedData.subarray( - PNG_METADATA_SIZE, - PNG_METADATA_SIZE + originalImageBufferLength, - ); - - // Create PNG from image buffer using block utility - return await createPngFromImageBuffer( - originalImageBuffer, - originalWidth, - originalHeight, - ); -} diff --git a/packages/node/src/crypto.ts b/packages/node/src/crypto.ts deleted file mode 100644 index 7c82d10..0000000 --- a/packages/node/src/crypto.ts +++ /dev/null @@ -1,36 +0,0 @@ -import crypto from "node:crypto"; -import { - type CryptoProvider, - InvalidUUIDFormatError, -} from "@image-shield/core"; - -export class NodeCryptoProvider implements CryptoProvider { - encryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer { - const cipher = crypto.createCipheriv("aes-256-cbc", this.keyTo32(key), iv); - return Buffer.concat([cipher.update(buffer), cipher.final()]); - } - - decryptBuffer(buffer: Buffer, key: string, iv: Buffer): Buffer { - const decipher = crypto.createDecipheriv( - "aes-256-cbc", - this.keyTo32(key), - iv, - ); - return Buffer.concat([decipher.update(buffer), decipher.final()]); - } - - keyTo32(key: string): Buffer { - return crypto.createHash("sha256").update(key).digest(); - } - - generateUUID(): string { - return crypto.randomUUID(); - } - - uuidToIV(uuid: string): Buffer { - const hex = uuid.replace(/-/g, ""); - if (hex.length !== 32) - throw new InvalidUUIDFormatError("Invalid UUID format"); - return Buffer.from(hex, "hex"); - } -} diff --git a/packages/node/src/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index 9dfb257..cc78d7b 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -2,18 +2,13 @@ import fs from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { - CryptoUtils, DEFAULT_FRAGMENTATION_CONFIG, type FragmentationConfig, type ManifestData, } from "@image-shield/core"; import { Jimp, JimpMime } from "jimp"; -import { NodeCryptoProvider } from "./crypto"; import { ImageFragmenter } from "./fragmenter"; -// Initialize crypto provider -CryptoUtils.setProvider(new NodeCryptoProvider()); - describe("ImageFragmenter", () => { const tmpDir = path.join(tmpdir(), "fragmenter_test_tmp"); let testImagePath: string; @@ -125,15 +120,10 @@ describe("ImageFragmenter", () => { const fragmenter = new ImageFragmenter(config); expect(fragmenter).toBeInstanceOf(ImageFragmenter); }); - - test("initializes with secret key", () => { - const fragmenter = new ImageFragmenter({}, "test-secret"); - expect(fragmenter).toBeInstanceOf(ImageFragmenter); - }); }); describe("fragmentImages", () => { - test("fragments single image without encryption", async () => { + test("fragments single image", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, prefix: "test", @@ -146,28 +136,6 @@ describe("ImageFragmenter", () => { expect(result.fragmentedImages).toBeDefined(); expect(result.manifest.images).toHaveLength(1); expect(result.fragmentedImages).toHaveLength(1); - expect(result.manifest.secure).toBe(false); - expect(result.manifest.algorithm).toBeUndefined(); - }); - - test("fragments single image with encryption", async () => { - const fragmenter = new ImageFragmenter( - { - blockSize: 2, - prefix: "test", - seed: "test-seed", - }, - "test-secret-key", - ); - - const result = await fragmenter.fragmentImages([testImagePath]); - - expect(result.manifest).toBeDefined(); - expect(result.fragmentedImages).toBeDefined(); - expect(result.manifest.images).toHaveLength(1); - expect(result.fragmentedImages).toHaveLength(1); - expect(result.manifest.secure).toBe(true); - expect(result.manifest.algorithm).toBe("aes-256-cbc"); }); test("fragments multiple images", async () => { diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 8caf459..f6f4701 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -1,5 +1,4 @@ import { - CryptoUtils, DEFAULT_FRAGMENTATION_CONFIG, type FragmentationConfig, type FragmentationResult, @@ -8,21 +7,16 @@ import { calcBlocksPerFragment, } from "@image-shield/core"; import { SeededRandom, shuffle } from "@tuki0918/seeded-shuffle"; -import { - blocksToPngImage, - encryptPngImageBuffer, - imageFileToBlocks, -} from "./block"; +import { blocksToPngImage, imageFileToBlocks } from "./block"; import { VERSION } from "./constants"; import { fileNameWithoutExtension, readFileBuffer } from "./file"; +import { generateManifestId } from "./utils"; export class ImageFragmenter { private config: Required; - private secretKey?: string; - constructor(config: FragmentationConfig, secretKey?: string) { + constructor(config: FragmentationConfig) { this.config = this._initializeConfig(config); - this.secretKey = secretKey; } private _initializeConfig( @@ -68,13 +62,10 @@ export class ImageFragmenter { index, ); const imageBlocks = shuffledBlocks.slice(start, end); - const fragmentImage = await this._createFragmentImage( + return await this._createFragmentImage( imageBlocks, - fragmentBlocksCount[index], manifest.config.blockSize, ); - - return fragmentImage; }), ); } @@ -97,17 +88,12 @@ export class ImageFragmenter { ): ManifestData { this._validateFileNames(imageInfos); - const secure = !!this.secretKey; - const algorithm = secure ? "aes-256-cbc" : undefined; - return { id: manifestId, version: VERSION, timestamp: new Date().toISOString(), config: this.config, - images: this._mapImageInfosToShortFormat(imageInfos), - algorithm, - secure, + images: imageInfos, }; } @@ -128,29 +114,15 @@ export class ImageFragmenter { } } - private _mapImageInfosToShortFormat(imageInfos: ImageInfo[]) { - return imageInfos.map((info) => ({ - w: info.width, - h: info.height, - c: 4, // Always use 4 channels (RGBA) for generated PNG - x: info.blockCountX, - y: info.blockCountY, - name: info.name, - })); - } - private async _prepareFragmentData(imagePaths: string[]): Promise<{ manifest: ManifestData; allBlocks: Buffer[]; fragmentBlocksCount: number[]; }> { - // Generate manifest ID first - const manifestId = CryptoUtils.generateUUID(); + const manifestId = generateManifestId(); - const { imageInfos, allBlocks } = await this._processSourceImages( - imagePaths, - { id: manifestId }, - ); + const { imageInfos, allBlocks } = + await this._processSourceImages(imagePaths); const manifest = this._createManifest(manifestId, imageInfos); @@ -162,17 +134,12 @@ export class ImageFragmenter { return { manifest, allBlocks, fragmentBlocksCount }; } - private async _processSourceImages( - imagePaths: string[], - manifestInfo: Pick, - ): Promise<{ + private async _processSourceImages(imagePaths: string[]): Promise<{ imageInfos: ImageInfo[]; allBlocks: Buffer[]; }> { const processedImages = await Promise.all( - imagePaths.map((imagePath) => - this._processSourceImage(imagePath, manifestInfo), - ), + imagePaths.map((imagePath) => this._processSourceImage(imagePath)), ); const imageInfos = processedImages.map((p) => p.imageInfo); @@ -181,27 +148,21 @@ export class ImageFragmenter { return { imageInfos, allBlocks }; } - private async _processSourceImage( - imagePath: string, - manifestInfo: Pick, - ): Promise<{ + private async _processSourceImage(imagePath: string): Promise<{ imageInfo: ImageInfo; blocks: Buffer[]; }> { - const processedImageBuffer = await this._processSourceImageBuffer( - imagePath, - manifestInfo, - ); + const imageBuffer = await readFileBuffer(imagePath); const { blocks, width, height, channels, blockCountX, blockCountY } = - await imageFileToBlocks(processedImageBuffer, this.config.blockSize); + await imageFileToBlocks(imageBuffer, this.config.blockSize); const imageInfo: ImageInfo = { - width, - height, - channels, - blockCountX, - blockCountY, + w: width, + h: height, + c: 4, // Always use 4 channels (RGBA) for generated PNG + x: blockCountX, + y: blockCountY, name: this.config.restoreFileName ? fileNameWithoutExtension(imagePath) : undefined, @@ -210,44 +171,15 @@ export class ImageFragmenter { return { imageInfo, blocks }; } - private async _processSourceImageBuffer( - imagePath: string, - manifestInfo: Pick, - ): Promise { - const originalBuffer = await readFileBuffer(imagePath); - - if (this.secretKey && manifestInfo.id) { - return await encryptPngImageBuffer( - originalBuffer, - this.secretKey, - manifestInfo.id, - ); - } - - return originalBuffer; - } - private async _createFragmentImage( blocks: Buffer[], - blockCount: number, blockSize: number, ): Promise { - const { imageWidth, imageHeight } = this._calculateFragmentImageSize( - blockCount, - blockSize, - ); - - return await blocksToPngImage(blocks, imageWidth, imageHeight, blockSize); - } - - private _calculateFragmentImageSize( - blockCount: number, - blockSize: number, - ): { imageWidth: number; imageHeight: number } { + const blockCount = blocks.length; const blocksPerRow = Math.ceil(Math.sqrt(blockCount)); const imageWidth = blocksPerRow * blockSize; const imageHeight = Math.ceil(blockCount / blocksPerRow) * blockSize; - return { imageWidth, imageHeight }; + return await blocksToPngImage(blocks, imageWidth, imageHeight, blockSize); } } diff --git a/packages/node/src/index.test.ts b/packages/node/src/index.test.ts index 988d44e..03db360 100644 --- a/packages/node/src/index.test.ts +++ b/packages/node/src/index.test.ts @@ -25,7 +25,6 @@ describe("ImageShield (integration)", () => { const width = 2; const height = 2; const blockSize = 1; - const secretKey = "index-test-key"; const prefix = "indextestimg"; let imagePaths: string[] = []; let manifestPath = ""; @@ -51,7 +50,6 @@ describe("ImageShield (integration)", () => { imagePaths, config: { blockSize, prefix }, outputDir: tmpDir, - secretKey, }); // Find manifest and fragment files manifestPath = path.join(tmpDir, "manifest.json"); @@ -72,7 +70,6 @@ describe("ImageShield (integration)", () => { imagePaths: fragmentPaths, manifestPath, outputDir: tmpDir, - secretKey, }); // Find restored images (use the same logic as index.ts, based on fragmentPaths) restoredPaths = []; @@ -114,23 +111,11 @@ describe("ImageShield (integration)", () => { // Check that fragment images exist and are valid PNGs for (const fragmentPath of fragmentPaths) { expect(fs.existsSync(fragmentPath)).toBe(true); - if (secretKey) { - // If encrypted, it should still be a valid PNG but with encrypted content - const jimpImage = await Jimp.read(fragmentPath); - expect(jimpImage.mime).toBe("image/png"); - expect(jimpImage.bitmap.width).toBeGreaterThan(0); - expect(jimpImage.bitmap.height).toBeGreaterThan(0); - // The encrypted image should have different dimensions or content from originals - expect( - jimpImage.bitmap.width * jimpImage.bitmap.height, - ).toBeGreaterThanOrEqual(4); // At least encrypted data size - } else { - // If not encrypted, it should be openable as PNG - const jimpImage = await Jimp.read(fragmentPath); - expect(jimpImage.mime).toBe("image/png"); - expect(jimpImage.bitmap.width).toBeGreaterThan(0); - expect(jimpImage.bitmap.height).toBeGreaterThan(0); - } + // Should be openable as PNG + const jimpImage = await Jimp.read(fragmentPath); + expect(jimpImage.mime).toBe("image/png"); + expect(jimpImage.bitmap.width).toBeGreaterThan(0); + expect(jimpImage.bitmap.height).toBeGreaterThan(0); } // Check that restored images exist before comparing content @@ -147,7 +132,7 @@ describe("ImageShield (integration)", () => { }); }); -describe("ImageShield (restoreFileName + encrypt integration)", () => { +describe("ImageShield (restoreFileName integration)", () => { const tmpDir = path.join(tmpdir(), "index_test_tmp_restoreFileName"); const originalImages = [ Buffer.from([ @@ -162,7 +147,6 @@ describe("ImageShield (restoreFileName + encrypt integration)", () => { const width = 2; const height = 2; const blockSize = 1; - const secretKey = "index-test-key"; const prefix = "indextestimgorig"; let imagePaths: string[] = []; let manifestPath = ""; @@ -189,7 +173,6 @@ describe("ImageShield (restoreFileName + encrypt integration)", () => { imagePaths, config: { blockSize, prefix, restoreFileName: true }, outputDir: tmpDir, - secretKey, }); // Find manifest and fragment files manifestPath = path.join(tmpDir, "manifest.json"); @@ -210,7 +193,6 @@ describe("ImageShield (restoreFileName + encrypt integration)", () => { imagePaths: fragmentPaths, manifestPath, outputDir: tmpDir, - secretKey, }); // Find restored images (should be named as original file name) restoredPaths = []; @@ -232,7 +214,7 @@ describe("ImageShield (restoreFileName + encrypt integration)", () => { if (fs.existsSync(tmpDir)) fs.rmdirSync(tmpDir); }); - test("manifest images[].name contains original file name when restoreFileName=true (encrypt mode)", () => { + test("manifest images[].name contains original file name when restoreFileName=true", () => { expect(manifest).toBeDefined(); expect(manifest?.config.restoreFileName).toBe(true); expect(Array.isArray(manifest?.images)).toBe(true); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 0f715ff..7a10d81 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,5 +1,4 @@ import { - CryptoUtils, type DecryptOptions, type EncryptOptions, type FragmentationConfig, @@ -8,16 +7,11 @@ import { generateFragmentFileName, generateRestoredFileName, generateRestoredOriginalFileName, - verifySecretKey, } from "@image-shield/core"; -import { NodeCryptoProvider } from "./crypto"; import { createDir, readJsonFile, writeFile } from "./file"; import { ImageFragmenter } from "./fragmenter"; import { ImageRestorer } from "./restorer"; -// Initialize the crypto provider -CryptoUtils.setProvider(new NodeCryptoProvider()); - export { ImageFragmenter, ImageRestorer, @@ -27,13 +21,9 @@ export { export default class ImageShield { static async encrypt(options: EncryptOptions): Promise { - const { imagePaths, config, outputDir, secretKey } = - validateEncryptOptions(options); + const { imagePaths, config, outputDir } = validateEncryptOptions(options); - const fragmenter = new ImageFragmenter( - config ?? {}, - verifySecretKey(secretKey), - ); + const fragmenter = new ImageFragmenter(config ?? {}); const { manifest, fragmentedImages } = await fragmenter.fragmentImages(imagePaths); @@ -53,12 +43,12 @@ export default class ImageShield { } static async decrypt(options: DecryptOptions): Promise { - const { imagePaths, manifestPath, outputDir, secretKey } = + const { imagePaths, manifestPath, outputDir } = validateDecryptOptions(options); const manifest = await readJsonFile(manifestPath); - const restorer = new ImageRestorer(verifySecretKey(secretKey)); + const restorer = new ImageRestorer(); const restoredImages = await restorer.restoreImages(imagePaths, manifest); await createDir(outputDir, true); diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index 549287f..6165037 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -1,15 +1,11 @@ import fs from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { CryptoUtils, type ManifestData } from "@image-shield/core"; +import type { ManifestData } from "@image-shield/core"; import { Jimp, JimpMime } from "jimp"; -import { NodeCryptoProvider } from "./crypto"; import { ImageFragmenter } from "./fragmenter"; import { ImageRestorer } from "./restorer"; -// Initialize crypto provider -CryptoUtils.setProvider(new NodeCryptoProvider()); - describe("ImageRestorer", () => { const tmpDir = path.join(tmpdir(), "restorer_test_tmp"); let testImagePath: string; @@ -107,19 +103,14 @@ describe("ImageRestorer", () => { }); describe("constructor", () => { - test("initializes without secret key", () => { + test("initializes successfully", () => { const restorer = new ImageRestorer(); expect(restorer).toBeInstanceOf(ImageRestorer); }); - - test("initializes with secret key", () => { - const restorer = new ImageRestorer("test-secret"); - expect(restorer).toBeInstanceOf(ImageRestorer); - }); }); describe("restoreImages", () => { - test("restores single image without encryption", async () => { + test("restores single image", async () => { // First fragment the image const fragmenter = new ImageFragmenter({ blockSize: 2, @@ -146,38 +137,6 @@ describe("ImageRestorer", () => { expect(jimpImage.bitmap.height).toBe(4); }); - test("restores single image with encryption", async () => { - const secretKey = "test-secret-key"; - - // First fragment the image with encryption - const fragmenter = new ImageFragmenter( - { - blockSize: 2, - seed: "test-seed", - }, - secretKey, - ); - const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ - testImagePath, - ]); - - // Then restore it with the same secret key - const restorer = new ImageRestorer(secretKey); - const restoredImages = await restorer.restoreImages( - fragmentedImages, - manifest, - ); - - expect(restoredImages).toHaveLength(1); - expect(Buffer.isBuffer(restoredImages[0])).toBe(true); - - // Should be able to read as PNG - const jimpImage = await Jimp.read(restoredImages[0]); - expect(jimpImage.mime).toBe("image/png"); - expect(jimpImage.bitmap.width).toBe(4); - expect(jimpImage.bitmap.height).toBe(4); - }); - test("restores multiple images", async () => { // First fragment multiple images const fragmenter = new ImageFragmenter({ @@ -345,74 +304,6 @@ describe("ImageRestorer", () => { ).rejects.toThrow("Fragment image count mismatch"); }); - test("returns encrypted data when secret key is missing for encrypted data", async () => { - const secretKey = "test-secret-key"; - - // Fragment with encryption - const fragmenter = new ImageFragmenter( - { - blockSize: 2, - seed: "test-seed", - }, - secretKey, - ); - const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ - testImagePath, - ]); - - // Try to restore without secret key - const restorer = new ImageRestorer(); // No secret key - - // Should return encrypted data (not throw error) - const restoredImages = await restorer.restoreImages( - fragmentedImages, - manifest, - ); - expect(restoredImages).toHaveLength(1); - expect(Buffer.isBuffer(restoredImages[0])).toBe(true); - - // The restored image should be a valid PNG but contain encrypted data - const restoredJimp = await Jimp.read(restoredImages[0]); - expect(restoredJimp.mime).toBe("image/png"); - - // Now verify that using the correct key produces the original image - const restorerWithKey = new ImageRestorer(secretKey); - const decryptedImages = await restorerWithKey.restoreImages( - fragmentedImages, - manifest, - ); - const decryptedJimp = await Jimp.read(decryptedImages[0]); - const originalJimp = await Jimp.read(testImagePath); - - // Decrypted image should match original dimensions - expect(decryptedJimp.bitmap.width).toBe(originalJimp.bitmap.width); - expect(decryptedJimp.bitmap.height).toBe(originalJimp.bitmap.height); - }); - - test("throws error when wrong secret key is used", async () => { - const correctKey = "correct-secret-key"; - const wrongKey = "wrong-secret-key"; - - // Fragment with correct key - const fragmenter = new ImageFragmenter( - { - blockSize: 2, - seed: "test-seed", - }, - correctKey, - ); - const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ - testImagePath, - ]); - - // Try to restore with wrong key - const restorer = new ImageRestorer(wrongKey); - - await expect( - restorer.restoreImages(fragmentedImages, manifest), - ).rejects.toThrow(); - }); - test("handles non-existent fragment file", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index 80fefa8..f02da7e 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -1,23 +1,13 @@ import { + type ImageInfo, type ManifestData, - type ShortImageInfo, calcBlocksPerFragment, } from "@image-shield/core"; import { unshuffle } from "@tuki0918/seeded-shuffle"; -import { - blocksToPngImage, - decryptPngImageBuffer, - imageFileToBlocks, -} from "./block"; +import { blocksToPngImage, imageFileToBlocks } from "./block"; import { readFileBuffer } from "./file"; export class ImageRestorer { - private secretKey?: string; - - constructor(secretKey?: string) { - this.secretKey = secretKey; - } - async restoreImages( fragmentImages: (string | Buffer)[], manifest: ManifestData, @@ -34,16 +24,6 @@ export class ImageRestorer { manifest, ); - // If encryption was used, decrypt the reconstructed images - const secretKey = this.secretKey; - if (manifest.secure && secretKey) { - return await Promise.all( - reconstructedImages.map((img) => - decryptPngImageBuffer(img, secretKey, manifest.id), - ), - ); - } - return reconstructedImages; } @@ -68,7 +48,7 @@ export class ImageRestorer { } private _calculateBlockRange( - images: ShortImageInfo[], + images: ImageInfo[], targetIndex: number, ): { start: number; end: number } { const blockCount = images[targetIndex].x * images[targetIndex].y; @@ -115,7 +95,7 @@ export class ImageRestorer { } } - private _calculateTotalBlocks(images: ShortImageInfo[]): number { + private _calculateTotalBlocks(images: ImageInfo[]): number { return images.reduce((total, image) => total + image.x * image.y, 0); } @@ -125,17 +105,15 @@ export class ImageRestorer { manifest: ManifestData, expectedBlockCount: number, ): Promise { - const buf = await this._readImageBuffer(fragmentImage); - const { blocks } = await imageFileToBlocks(buf, manifest.config.blockSize); - return blocks.slice(0, expectedBlockCount); - } - - private async _readImageBuffer( - fragmentImage: string | Buffer, - ): Promise { - return Buffer.isBuffer(fragmentImage) + const imageBuffer = Buffer.isBuffer(fragmentImage) ? fragmentImage : await readFileBuffer(fragmentImage); + + const { blocks } = await imageFileToBlocks( + imageBuffer, + manifest.config.blockSize, + ); + return blocks.slice(0, expectedBlockCount); } private async _extractBlocksFromFragments( @@ -158,7 +136,7 @@ export class ImageRestorer { private async _reconstructImage( blocks: Buffer[], blockSize: number, - imageInfo: ShortImageInfo, + imageInfo: ImageInfo, ): Promise { const { w, h } = imageInfo; return await blocksToPngImage(blocks, w, h, blockSize); diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts new file mode 100644 index 0000000..e954731 --- /dev/null +++ b/packages/node/src/utils.ts @@ -0,0 +1,9 @@ +import crypto from "node:crypto"; + +/** + * Generate a unique manifest ID + * @returns A UUID string + */ +export function generateManifestId(): string { + return crypto.randomUUID(); +}