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 |
-|:-------:|:---------------:|:---------------:|
-|  |  |  |
-| 500 x 500px (109KB) | 400 x 600px (4KB) | 600 x 400px (3KB) |
-
-| output 1 | output 2 | output 3 |
-|:-------:|:---------------:|:---------------:|
-|  |  |  |
-| 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 |
-|:-------:|:---------------:|:---------------:|
-|  |  |  |
-| 494 x 494px (976KB) | 494 x 494px (976KB) | 494 x 494px (976KB) |
-
-| output 1 | output 2 | output 3 |
-|:-------:|:---------------:|:---------------:|
-|  |  |  |
-| 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();
+}