From c447ca268774367a0ebf7fdd95dc1242da9a84df Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Thu, 6 Nov 2025 02:16:00 +0900 Subject: [PATCH 1/7] fix: update FragmentationConfig to allow seed as string or number Modified the seed property in FragmentationConfig interface to accept both number and string types, enhancing flexibility in configuration options. --- packages/core/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6968b3b..ed9eae5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -26,7 +26,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; } From a21e38136e325c332e9eaf1dc475dba2a6ba5b5f Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Thu, 6 Nov 2025 02:18:49 +0900 Subject: [PATCH 2/7] refactor: remove encryption functionality from image fragmentation and restoration - Updated README to reflect the removal of encryption options and modes. - Refactored CLI commands to focus solely on image fragmentation without encryption. - Removed secret key handling from relevant interfaces and classes. - Adjusted tests to align with the new functionality, ensuring they focus on fragmentation only. --- README.md | 110 ++---------------- packages/cli/README.md | 53 +++------ packages/cli/src/commands/decrypt.ts | 6 +- packages/cli/src/commands/encrypt.ts | 10 +- packages/cli/src/types.ts | 2 - packages/core/src/helpers.test.ts | 15 --- packages/core/src/helpers.ts | 14 --- packages/core/src/types.ts | 16 --- packages/node/src/block.ts | 161 --------------------------- packages/node/src/fragmenter.test.ts | 29 +---- packages/node/src/fragmenter.ts | 27 +---- packages/node/src/index.test.ts | 32 ++---- packages/node/src/index.ts | 13 +-- packages/node/src/restorer.test.ts | 109 +----------------- packages/node/src/restorer.ts | 22 +--- 15 files changed, 49 insertions(+), 570 deletions(-) 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/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/types.ts b/packages/core/src/types.ts index ed9eae5..840ffbc 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 { @@ -34,12 +30,6 @@ 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 */ @@ -74,8 +64,6 @@ export interface ShortImageInfo { name?: string; } -export type EncryptionAlgorithm = "aes-256-cbc"; - export interface ManifestData { /** UUID */ id: string; @@ -87,10 +75,6 @@ export interface ManifestData { config: Required; /** Image information */ images: ShortImageInfo[]; - /** Algorithm (only set if secure is true) */ - algorithm?: EncryptionAlgorithm; - /** Secure (true if encrypted) */ - secure: boolean; } export interface FragmentationResult { diff --git a/packages/node/src/block.ts b/packages/node/src/block.ts index f3ff3ef..3c7f0d5 100644 --- a/packages/node/src/block.ts +++ b/packages/node/src/block.ts @@ -1,9 +1,6 @@ -import { CryptoUtils } 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; @@ -426,161 +423,3 @@ export async function createPngFromImageBuffer( ); } } - -/** - * 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/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index 9dfb257..5f55215 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -125,15 +125,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 +141,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..01aabd0 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -8,21 +8,15 @@ 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"; 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( @@ -97,17 +91,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, }; } @@ -214,17 +203,7 @@ export class ImageFragmenter { 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; + return await readFileBuffer(imagePath); } private async _createFragmentImage( 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..c54a7db 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -8,7 +8,6 @@ import { generateFragmentFileName, generateRestoredFileName, generateRestoredOriginalFileName, - verifySecretKey, } from "@image-shield/core"; import { NodeCryptoProvider } from "./crypto"; import { createDir, readJsonFile, writeFile } from "./file"; @@ -27,13 +26,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 +48,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..d7523a6 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -107,19 +107,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 +141,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 +308,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..d805bda 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -4,20 +4,10 @@ import { 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; } From 20750c5287843d8bb371bac07f212d6f20b23b0f Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Thu, 6 Nov 2025 02:20:27 +0900 Subject: [PATCH 3/7] refactor: remove crypto functionality from core and node packages - Deleted CryptoUtils and NodeCryptoProvider classes to streamline the codebase. - Updated imports in index and test files to remove references to the removed crypto functionality. - Adjusted image fragmenter to use native crypto methods directly, enhancing performance and reducing dependencies. --- packages/core/src/crypto.ts | 51 ---------------------------- packages/core/src/index.ts | 1 - packages/node/src/crypto.ts | 36 -------------------- packages/node/src/fragmenter.test.ts | 5 --- packages/node/src/fragmenter.ts | 4 +-- packages/node/src/index.ts | 5 --- packages/node/src/restorer.test.ts | 6 +--- 7 files changed, 3 insertions(+), 105 deletions(-) delete mode 100644 packages/core/src/crypto.ts delete mode 100644 packages/node/src/crypto.ts 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/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/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 5f55215..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; diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 01aabd0..77289c5 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -1,5 +1,5 @@ +import crypto from "node:crypto"; import { - CryptoUtils, DEFAULT_FRAGMENTATION_CONFIG, type FragmentationConfig, type FragmentationResult, @@ -134,7 +134,7 @@ export class ImageFragmenter { fragmentBlocksCount: number[]; }> { // Generate manifest ID first - const manifestId = CryptoUtils.generateUUID(); + const manifestId = crypto.randomUUID(); const { imageInfos, allBlocks } = await this._processSourceImages( imagePaths, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index c54a7db..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, @@ -9,14 +8,10 @@ import { generateRestoredFileName, generateRestoredOriginalFileName, } 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, diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index d7523a6..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; From 1ba920306f673bc500a190227e155a6f8f2dbfee Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Fri, 7 Nov 2025 00:35:03 +0900 Subject: [PATCH 4/7] refactor: streamline image processing in block and fragmenter modules - Removed the imageBufferToPng function and integrated its logic directly into createPngFromImageBuffer for clarity. - Simplified the _processSourceImages and _processSourceImage methods in ImageFragmenter by eliminating unnecessary parameters and improving readability. - Updated the _createFragmentImage method to calculate block count directly from the provided blocks, enhancing efficiency. - Refactored image restoration logic to improve buffer handling and streamline block extraction from fragment images. --- packages/node/src/block.ts | 19 ++--------- packages/node/src/fragmenter.ts | 56 ++++++--------------------------- packages/node/src/restorer.ts | 16 +++++----- 3 files changed, 19 insertions(+), 72 deletions(-) diff --git a/packages/node/src/block.ts b/packages/node/src/block.ts index 3c7f0d5..df8310a 100644 --- a/packages/node/src/block.ts +++ b/packages/node/src/block.ts @@ -56,22 +56,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 @@ -416,7 +400,8 @@ 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), diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 77289c5..6aeaf26 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -62,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; }), ); } @@ -133,13 +130,10 @@ export class ImageFragmenter { allBlocks: Buffer[]; fragmentBlocksCount: number[]; }> { - // Generate manifest ID first const manifestId = crypto.randomUUID(); - const { imageInfos, allBlocks } = await this._processSourceImages( - imagePaths, - { id: manifestId }, - ); + const { imageInfos, allBlocks } = + await this._processSourceImages(imagePaths); const manifest = this._createManifest(manifestId, imageInfos); @@ -151,17 +145,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); @@ -170,20 +159,14 @@ 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, @@ -199,34 +182,15 @@ export class ImageFragmenter { return { imageInfo, blocks }; } - private async _processSourceImageBuffer( - imagePath: string, - manifestInfo: Pick, - ): Promise { - return await readFileBuffer(imagePath); - } - 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/restorer.ts b/packages/node/src/restorer.ts index d805bda..5c20e80 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -105,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( From a6db0caa90c9f41515dcf1ed16b25ee9973f96d7 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Fri, 7 Nov 2025 01:31:36 +0900 Subject: [PATCH 5/7] refactor: replace ShortImageInfo with ImageInfo in fragmenter and restorer - Updated the ImageFragmenter and ImageRestorer classes to use the full ImageInfo interface instead of the shortened version. - Removed the _mapImageInfosToShortFormat method to streamline image information handling. - Adjusted related methods to accommodate the new structure of ImageInfo, enhancing clarity and consistency across the codebase. --- packages/core/src/types.ts | 20 +------------------- packages/node/src/fragmenter.ts | 23 ++++++----------------- packages/node/src/restorer.ts | 8 ++++---- 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 840ffbc..00e859c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -32,24 +32,6 @@ export interface FragmentationConfig { * This includes dimensions, channels, and block counts. */ 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,7 +56,7 @@ export interface ManifestData { /** Config */ config: Required; /** Image information */ - images: ShortImageInfo[]; + images: ImageInfo[]; } export interface FragmentationResult { diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 6aeaf26..86bde43 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -93,7 +93,7 @@ export class ImageFragmenter { version: VERSION, timestamp: new Date().toISOString(), config: this.config, - images: this._mapImageInfosToShortFormat(imageInfos), + images: imageInfos, }; } @@ -114,17 +114,6 @@ 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[]; @@ -169,11 +158,11 @@ export class ImageFragmenter { 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, diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index 5c20e80..f02da7e 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -1,6 +1,6 @@ import { + type ImageInfo, type ManifestData, - type ShortImageInfo, calcBlocksPerFragment, } from "@image-shield/core"; import { unshuffle } from "@tuki0918/seeded-shuffle"; @@ -48,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; @@ -95,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); } @@ -136,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); From 34731549e879fee27393ffd759c65b693e070398 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Fri, 7 Nov 2025 01:48:53 +0900 Subject: [PATCH 6/7] refactor: introduce generateManifestId utility for unique ID generation - Added a new utility function, generateManifestId, to encapsulate the logic for generating unique manifest IDs using crypto. - Updated the ImageFragmenter class to utilize the new utility function instead of directly calling crypto.randomUUID, enhancing code clarity and maintainability. --- packages/node/src/fragmenter.ts | 4 ++-- packages/node/src/utils.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/node/src/utils.ts diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 86bde43..f6f4701 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -1,4 +1,3 @@ -import crypto from "node:crypto"; import { DEFAULT_FRAGMENTATION_CONFIG, type FragmentationConfig, @@ -11,6 +10,7 @@ import { SeededRandom, shuffle } from "@tuki0918/seeded-shuffle"; import { blocksToPngImage, imageFileToBlocks } from "./block"; import { VERSION } from "./constants"; import { fileNameWithoutExtension, readFileBuffer } from "./file"; +import { generateManifestId } from "./utils"; export class ImageFragmenter { private config: Required; @@ -119,7 +119,7 @@ export class ImageFragmenter { allBlocks: Buffer[]; fragmentBlocksCount: number[]; }> { - const manifestId = crypto.randomUUID(); + const manifestId = generateManifestId(); const { imageInfos, allBlocks } = await this._processSourceImages(imagePaths); 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(); +} From 55a062f17fab85fd04ba92aede867c40c159108e Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Fri, 7 Nov 2025 02:24:37 +0900 Subject: [PATCH 7/7] refactor: enhance block utility functions and tests - Introduced RGBA_CHANNELS constant and BlockCounts interface to improve clarity in block calculations. - Added calculateBlockCounts function for determining block counts based on image dimensions and block size. - Refactored tests for calcBlocksPerFragment to ensure comprehensive coverage of various scenarios, including edge cases. - Removed redundant test cases to streamline the test suite. --- packages/core/src/block-utils.ts | 28 +++++++++++ packages/node/src/block.test.ts | 79 +++----------------------------- packages/node/src/block.ts | 37 ++++----------- 3 files changed, 43 insertions(+), 101 deletions(-) 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/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 df8310a..f2e2939 100644 --- a/packages/node/src/block.ts +++ b/packages/node/src/block.ts @@ -1,12 +1,10 @@ +import { + type BlockCounts, + RGBA_CHANNELS, + calculateBlockCounts, +} from "@image-shield/core"; import { Jimp, JimpMime } from "jimp"; -const RGBA_CHANNELS = 4; - -interface BlockCounts { - blockCountX: number; - blockCountY: number; -} - interface ImageFileToBlocksResult { blocks: Buffer[]; width: number; @@ -56,24 +54,6 @@ function formatErrorMessage(operation: string, error: unknown): string { return `${operation}: ${errorMessage}`; } -/** - * 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) @@ -335,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.`, + ); } }