-
Notifications
You must be signed in to change notification settings - Fork 0
Home
AI-generated
Welcome to the image-shield wiki!
このドキュメントでは、Image Shield の Node.js パッケージにおける画像の暗号化(encrypt)と復号化(decrypt)の処理フローを詳細に説明します。
Image Shield は secretKey の有無により、2つの動作モードを持ちます。
secretKey が設定されていない場合、シャッフルのみが実行されます。
元画像 → 読み込み → RGBA変換 → ブロック分割 → シャッフル → 断片化PNG出力
特徴:
- 暗号化は行わない(
manifest.secure = false) - ブロックの配置をランダムに入れ替えるのみ
- 高速な処理が可能
- セキュリティレベルは低い(シャッフルのシードが分かれば復元可能)
secretKey が設定されている場合、暗号化とシャッフルの両方が実行されます。
元画像 → 読み込み → RGBA変換 → 暗号化 → ブロック分割 → シャッフル → 断片化PNG出力
特徴:
- AES-256-CBC による暗号化を実施(
manifest.secure = true) - ブロックシャッフルと暗号化の二重保護
- セキュアな画像保護が可能
- 処理時間がやや長くなる
graph TB
A[開始: ImageShield.encrypt] --> B[入力検証]
B --> C[ImageFragmenter インスタンス生成]
C --> D[fragmentImages 実行]
D --> E[各画像の処理]
E --> F{secretKey あり?}
F -->|Yes| G[画像暗号化]
F -->|No| H[スキップ]
G --> I[RGBA変換 & ブロック分割]
H --> I
I --> J[全ブロックを統合]
J --> K[シャッフル実行]
K --> L[断片画像生成]
L --> M[マニフェスト作成]
M --> N[ファイル出力]
N --> O[完了]
ファイル: packages/node/src/index.ts
static async encrypt(options: EncryptOptions): Promise<void>-
imagePaths: 元画像のパス配列 -
config: 断片化設定(ブロックサイズ、prefix、seed等) -
outputDir: 出力先ディレクトリ -
secretKey: (オプション)暗号化キー
検証項目:
-
imagePathsが空でない配列であること -
outputDirが有効な文字列であること
ファイル: packages/node/src/fragmenter.ts
constructor(config: FragmentationConfig, secretKey?: string)設定の初期化:
-
blockSize: ブロックサイズ(デフォルト: 32px) -
prefix: 出力ファイルのプレフィックス -
seed: シャッフル用のシード値(未指定の場合は自動生成) -
restoreFileName: 元のファイル名を保持するか
ファイル: packages/node/src/block.ts
async function encryptPngImageBuffer(
pngBuffer: Buffer,
secretKey: string,
manifestId: string,
): Promise<Buffer>処理手順:
-
RGBA画像バッファの抽出
- PNG画像をJimpで読み込み
- RGBA形式(4チャネル)に変換
- 生のピクセルデータを取得
-
メタデータの作成
// 12バイトのメタデータ [width: 4 bytes][height: 4 bytes][imageBufferLength: 4 bytes]
-
データの結合と暗号化
- メタデータ + 画像バッファを結合
- AES-256-CBC で暗号化
- IV(初期化ベクトル)はマニフェストIDから生成
// Manifest ID (UUID) → 16バイトIVに変換 const iv = CryptoUtils.uuidToIV(manifestId); const encryptedData = CryptoUtils.encryptBuffer(dataToEncrypt, secretKey, iv);
-
暗号化データのPNG化
- 暗号化データを格納する最適な画像サイズを計算
- パディングを追加(必要に応じて)
- RGBA画像として再構成
- PNG形式で出力
ファイル: packages/node/src/block.ts
async function imageFileToBlocks(
input: string | Buffer,
blockSize: number,
): Promise<ImageFileToBlocksResult>処理手順:
-
画像の読み込みとRGBA変換
- Jimpライブラリを使用
- すべての画像形式をRGBAに統一
-
ブロック数の計算
blockCountX = Math.ceil(width / blockSize) blockCountY = Math.ceil(height / blockSize)
-
ブロックの抽出
- 左上から右下へ、行ごとにブロックを抽出
- 各ブロックはRGBAピクセルデータのBuffer
- エッジブロック(端のブロック)は実際のサイズに調整
ブロックの構造:
元画像 (例: 100x100px、blockSize=32)
┌────────────────────┐
│ [0][1][2][3] │ → blockCountX = 4
│ [4][5][6][7] │ → blockCountY = 4
│ [8][9][10][11] │ → 合計16ブロック
│ [12][13][14][15] │
└────────────────────┘
ファイル: packages/node/src/fragmenter.ts
private async _processSourceImages(
imagePaths: string[],
manifestInfo: Pick<ManifestData, "id">,
)- 複数の画像のブロックを1つの配列に統合
- 各画像のメタデータ(サイズ、ブロック数等)を記録
ライブラリ: @tuki0918/seeded-shuffle
const shuffledBlocks = shuffle(allBlocks, manifest.config.seed);- Fisher-Yates シャッフルアルゴリズム
- シード値により再現可能なランダム配置
- 同じシード値で元の順序に戻すことが可能(復号化時)
ファイル: packages/node/src/fragmenter.ts
private async _createFragmentedImages(
shuffledBlocks: Buffer[],
fragmentBlocksCount: number[],
manifest: ManifestData,
): Promise<Buffer[]>処理手順:
-
各断片へのブロック配分
- 全ブロックを画像数で均等に分配
- 各断片が含むブロック数を計算
-
断片画像のサイズ計算
// ブロックを正方形に近い配置で並べる blocksPerRow = Math.ceil(Math.sqrt(blockCount)) imageWidth = blocksPerRow × blockSize imageHeight = Math.ceil(blockCount / blocksPerRow) × blockSize
-
PNG画像として出力
- ブロックを新しい画像バッファに配置
- PNG形式で保存
断片化の例:
シャッフル済みブロック: [7,2,14,5,9,...]
↓ 3つの画像に分割
断片1: [7,2,14,5] → fragment_0.png
断片2: [9,13,1,8] → fragment_1.png
断片3: [3,11,6,15] → fragment_2.png
ファイル: packages/node/src/fragmenter.ts
private _createManifest(
manifestId: string,
imageInfos: ImageInfo[],
): ManifestDataマニフェストの構造:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"version": "1.0.0",
"timestamp": "2025-10-08T12:34:56.789Z",
"config": {
"blockSize": 32,
"prefix": "fragment",
"seed": 123456,
"restoreFileName": true
},
"images": [
{
"w": 800, // 元の画像幅
"h": 600, // 元の画像高さ
"c": 4, // チャネル数(RGBA)
"x": 25, // 横方向ブロック数
"y": 19, // 縦方向ブロック数
"name": "photo" // 元のファイル名(拡張子なし)
}
],
"algorithm": "aes-256-cbc", // 暗号化アルゴリズム(secretKeyありの場合のみ)
"secure": true // 暗号化の有無
}ファイル: packages/node/src/index.ts
-
manifest.json: マニフェストファイル -
fragment_0.png,fragment_1.png, ...: 断片化された画像ファイル
graph TB
A[開始: ImageShield.decrypt] --> B[入力検証]
B --> C[マニフェスト読み込み]
C --> D[ImageRestorer インスタンス生成]
D --> E[restoreImages 実行]
E --> F[断片画像からブロック抽出]
F --> G[全ブロックを統合]
G --> H[アンシャッフル実行]
H --> I[元の画像ごとにブロック分割]
I --> J[画像再構成]
J --> K{secretKey あり?}
K -->|Yes| L[画像復号化]
K -->|No| M[スキップ]
L --> N[ファイル出力]
M --> N
N --> O[完了]
ファイル: packages/node/src/index.ts
static async decrypt(options: DecryptOptions): Promise<void>-
imagePaths: 断片画像のパス配列 -
manifestPath: マニフェストファイルのパス -
outputDir: 出力先ディレクトリ -
secretKey: (オプション)復号化キー
検証項目:
-
imagePathsが空でない配列であること -
manifestPathが有効な文字列であること -
outputDirが有効な文字列であること
ファイル: packages/node/src/index.ts
const manifest = await readJsonFile<ManifestData>(manifestPath);- マニフェストファイルから元の画像情報を取得
- ブロックサイズ、シード値、画像のメタデータを読み取る
ファイル: packages/node/src/restorer.ts
constructor(secretKey?: string)-
secretKeyを保持(暗号化されている場合に使用)
ファイル: packages/node/src/restorer.ts
private async _extractBlocksFromFragments(
fragmentImages: (string | Buffer)[],
manifest: ManifestData,
fragmentBlocksCount: number[],
): Promise<Buffer[]>処理手順:
-
各断片画像の読み込み
- ファイルパスまたはBufferから画像データを取得
-
ブロックの抽出
const { blocks } = await imageFileToBlocks(buf, manifest.config.blockSize);
- 断片画像をブロックサイズで分割
- 必要な数のブロックのみを取得(余分なパディング領域を除外)
-
全ブロックの統合
- すべての断片からのブロックを1つの配列に結合
ライブラリ: @tuki0918/seeded-shuffle
const restoredBlocks = unshuffle(allBlocks, manifest.config.seed);- シャッフルの逆操作を実行
- 同じシード値を使用して元の順序に復元
- Fisher-Yates アルゴリズムの逆適用
アンシャッフルの例:
シャッフル済み: [7,2,14,5,9,13,1,8,3,11,6,15]
↓ unshuffle (seed: 123456)
元の順序: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
ファイル: packages/node/src/restorer.ts
private _calculateBlockRange(
images: ShortImageInfo[],
targetIndex: number,
): { start: number; end: number }- マニフェストの画像情報を基に、各画像のブロック範囲を計算
- 画像ごとにブロックを分離
ブロック範囲の計算例:
画像0: 16ブロック → [0-15]
画像1: 12ブロック → [16-27]
画像2: 20ブロック → [28-47]
ファイル: packages/node/src/block.ts
export async function blocksToPngImage(
blocks: Buffer[],
width: number,
height: number,
blockSize: number,
): Promise<Buffer>処理手順:
-
画像バッファの作成
const imageBuffer = Buffer.alloc(width * height * RGBA_CHANNELS);
-
ブロックの配置
- 左上から右下へ、行ごとにブロックを配置
- エッジブロックは実際のサイズで配置
-
PNG形式に変換
- RGBAバッファからJimp画像を生成
- PNG形式でエンコード
再構成の例:
ブロック配列: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
↓
┌────────────────────┐
│ [0][1][2][3] │
│ [4][5][6][7] │
│ [8][9][10][11] │
│ [12][13][14][15] │
└────────────────────┘
→ 元画像に復元
ファイル: packages/node/src/block.ts
async function decryptPngImageBuffer(
encryptedPngBuffer: Buffer,
secretKey: string,
manifestId: string,
): Promise<Buffer>処理手順:
-
暗号化された画像バッファの抽出
- PNG画像からRGBAバッファを取得
-
パディングの除去
const encryptedData = removePadding(encryptedImageData);
- 末尾のゼロバイトを削除
-
データの復号化
const iv = CryptoUtils.uuidToIV(manifestId); const decryptedData = CryptoUtils.decryptBuffer(encryptedData, secretKey, iv);
- AES-256-CBC で復号化
- IV(初期化ベクトル)はマニフェストIDから生成
-
メタデータの解析
const { width, height, imageBufferLength } = parseImageBufferMetadata( decryptedData.subarray(0, 12) );
- 先頭12バイトから元の画像サイズとデータ長を取得
-
元の画像バッファの抽出
const originalImageBuffer = decryptedData.subarray(12, 12 + imageBufferLength);
-
PNG画像として再構成
- RGBAバッファからPNG形式に変換
ファイル: packages/node/src/index.ts
- 元のファイル名で復元(
restoreFileNameがtrueの場合) - または
restored_0.png,restored_1.png, ... として出力
AES-256-CBC (Advanced Encryption Standard, 256-bit, Cipher Block Chaining)
- ブロック暗号: 128ビット(16バイト)単位で暗号化
- 鍵長: 256ビット(32バイト)
- モード: CBC(Cipher Block Chaining)
- IV(初期化ベクトル): 128ビット(16バイト)
実装:
// Node.js の crypto モジュールを使用
const cipher = crypto.createCipheriv('aes-256-cbc', key32bytes, iv);
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);鍵の生成:
// シークレットキーをSHA-256でハッシュ化して32バイトに変換
const key = crypto.createHash('sha256').update(secretKey).digest();IVの生成:
// マニフェストID(UUID)から16バイトのIVを生成
// 例: "550e8400-e29b-41d4-a716-446655440000"
// → ハイフンを除去して16進数として解釈
const iv = Buffer.from(uuid.replace(/-/g, ''), 'hex');Fisher-Yates Shuffle (シード付き)
-
ライブラリ:
@tuki0918/seeded-shuffle -
特徴:
- 疑似乱数生成器にシード値を使用
- 同じシード値で常に同じシャッフル結果を得る
- O(n) の時間計算量
アルゴリズム:
for i from n-1 down to 1 do:
j = seeded_random(0, i)
swap array[i] and array[j]
アンシャッフル:
シャッフル過程を記録し、逆順で適用することで元に戻す
ブロックサイズ:
- デフォルト: 32×32 ピクセル
- カスタマイズ可能(config.blockSize)
ブロック分割の詳細例:
画像サイズ 100×100px をブロックサイズ 32×32px で分割する場合:
1. ブロック数の計算
blockCountX = Math.ceil(100 / 32) = Math.ceil(3.125) = 4
blockCountY = Math.ceil(100 / 32) = Math.ceil(3.125) = 4
総ブロック数 = 4 × 4 = 16ブロック2. ブロックの配置とサイズ
画像: 100px × 100px
ブロックサイズ: 32px × 32px
0 32 64 96 100
┌───────┬───────┬───────┬─┐
0 │ 0 │ 1 │ 2 │3│ ← 行0: y = 0-31
│ 32×32 │ 32×32 │ 32×32 │ │
32 ├───────┼───────┼───────┼─┤
│ 4 │ 5 │ 6 │7│ ← 行1: y = 32-63
│ 32×32 │ 32×32 │ 32×32 │ │
64 ├───────┼───────┼───────┼─┤
│ 8 │ 9 │ 10 │11│ ← 行2: y = 64-95
│ 32×32 │ 32×32 │ 32×32 │ │
96 ├───────┼───────┼───────┼─┤
│ 12 │ 13 │ 14 │15│ ← 行3: y = 96-99 (高さ4px)
100 └───────┴───────┴───────┴─┘
4px幅 4px幅 4px幅 4×4
3. 各ブロックの実際のサイズ
| ブロックID | 位置 (x, y) | サイズ (width × height) | 説明 |
|---|---|---|---|
| 0 | (0, 0) | 32 × 32 | 通常ブロック |
| 1 | (32, 0) | 32 × 32 | 通常ブロック |
| 2 | (64, 0) | 32 × 32 | 通常ブロック |
| 3 | (96, 0) | 4 × 32 | 右端(幅が4px) |
| 4 | (0, 32) | 32 × 32 | 通常ブロック |
| 5 | (32, 32) | 32 × 32 | 通常ブロック |
| 6 | (64, 32) | 32 × 32 | 通常ブロック |
| 7 | (96, 32) | 4 × 32 | 右端(幅が4px) |
| 8 | (0, 64) | 32 × 32 | 通常ブロック |
| 9 | (32, 64) | 32 × 32 | 通常ブロック |
| 10 | (64, 64) | 32 × 32 | 通常ブロック |
| 11 | (96, 64) | 4 × 32 | 右端(幅が4px) |
| 12 | (0, 96) | 32 × 4 | 下端(高さが4px) |
| 13 | (32, 96) | 32 × 4 | 下端(高さが4px) |
| 14 | (64, 96) | 32 × 4 | 下端(高さが4px) |
| 15 | (96, 96) | 4 × 4 | 右下角(4×4px) |
4. ブロックのデータサイズ
各ブロックはRGBA形式(4チャネル)で保存されます:
// 通常ブロック (32×32px)
dataSize = 32 × 32 × 4 = 4,096 bytes
// 右端ブロック (4×32px)
dataSize = 4 × 32 × 4 = 512 bytes
// 下端ブロック (32×4px)
dataSize = 32 × 4 × 4 = 512 bytes
// 右下角ブロック (4×4px)
dataSize = 4 × 4 × 4 = 64 bytes5. 抽出処理の実装
// ブロック3(右端、96-99px, 0-31px)の抽出例
function extractBlock(buffer, imageWidth, imageHeight, startX, startY, blockSize) {
// 実際のブロックサイズを計算(エッジ処理)
const blockWidth = Math.min(blockSize, imageWidth - startX); // min(32, 100-96) = 4
const blockHeight = Math.min(blockSize, imageHeight - startY); // min(32, 100-0) = 32
const blockData = [];
// ピクセルを行ごとに抽出
for (let y = 0; y < blockHeight; y++) { // y = 0 から 31 まで
for (let x = 0; x < blockWidth; x++) { // x = 0 から 3 まで(4ピクセル)
const pixelX = startX + x; // 96, 97, 98, 99
const pixelY = startY + y; // 0-31
const pixelIndex = (pixelY * imageWidth + pixelX) * 4; // RGBAなので×4
// RGBAの4チャネルをコピー
blockData.push(buffer[pixelIndex]); // R
blockData.push(buffer[pixelIndex + 1]); // G
blockData.push(buffer[pixelIndex + 2]); // B
blockData.push(buffer[pixelIndex + 3]); // A
}
}
return Buffer.from(blockData); // 4×32×4 = 512 bytes
}6. 復元時の配置処理
// ブロック15(右下角、4×4px)を配置する例
function placeBlock(targetBuffer, blockData, targetWidth, destX, destY, blockWidth, blockHeight) {
// destX=96, destY=96, blockWidth=4, blockHeight=4
for (let y = 0; y < blockHeight; y++) { // y = 0 から 3 まで
for (let x = 0; x < blockWidth; x++) { // x = 0 から 3 まで
const sourceIndex = (y * blockWidth + x) * 4; // ブロック内の位置
const targetIndex = ((destY + y) * targetWidth + (destX + x)) * 4; // 画像内の位置
// RGBAの4チャネルをコピー
targetBuffer[targetIndex] = blockData[sourceIndex]; // R
targetBuffer[targetIndex + 1] = blockData[sourceIndex + 1]; // G
targetBuffer[targetIndex + 2] = blockData[sourceIndex + 2]; // B
targetBuffer[targetIndex + 3] = blockData[sourceIndex + 3]; // A
}
}
}7. シャッフル後の断片化
16個のブロックを3つの断片画像に分配する例:
// ブロック配分の計算
totalBlocks = 16
fragmentCount = 3
fragmentBlocksCount = [6, 5, 5] // できるだけ均等に分配
// シャッフル前: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
// シャッフル後: [7,2,14,5,9,1,13,8,3,11,6,15,0,10,4,12]
// 断片1 (6ブロック): [7,2,14,5,9,1]
// 断片2 (5ブロック): [13,8,3,11,6]
// 断片3 (5ブロック): [15,0,10,4,12]各断片画像の構造:
断片1 (6ブロック):
┌────┬────┬────┐
│ 7 │ 2 │ 14 │ 3列 × 2行の配置
├────┼────┼────┤ blocksPerRow = ceil(√6) = 3
│ 5 │ 9 │ 1 │ 画像サイズ = 3×32 × 2×32 = 96×64px
└────┴────┴────┘
断片2 (5ブロック):
┌────┬────┬────┐
│ 13 │ 8 │ 3 │ 3列 × 2行の配置
├────┼────┼────┤ blocksPerRow = ceil(√5) = 3
│ 11 │ 6 │ │ 画像サイズ = 3×32 × 2×32 = 96×64px
└────┴────┴────┘ (空白部分は透明または黒)
断片3 (5ブロック):
┌────┬────┬────┐
│ 15 │ 0 │ 10 │ 3列 × 2行の配置
├────┼────┼────┤ blocksPerRow = ceil(√5) = 3
│ 4 │ 12 │ │ 画像サイズ = 3×32 × 2×32 = 96×64px
└────┴────┴────┘
重要なポイント:
- エッジブロックは元の画像サイズに合わせて自動的に調整される
- すべてのブロックはRGBA形式で保存され、チャネル数は常に4
- ブロックの抽出と配置は行ごと(左→右、上→下)に処理される
- シャッフル後も各ブロックは元のサイズ情報を保持
- 断片画像は正方形に近い配置で生成される
ストリーミング処理の不使用:
現在の実装では、画像全体をメモリに読み込んで処理します。 非常に大きな画像(数GB)を扱う場合は、メモリ制約に注意が必要です。
Buffer の使用:
- Node.js の Buffer を使用してバイナリデータを効率的に扱う
- コピーを最小限に抑える設計
検証項目:
-
入力検証
- ファイルパスの存在確認
- マニフェストの整合性チェック
-
断片数の検証
if (manifestImageCount !== fragmentImageCount) { throw new Error('Fragment image count mismatch'); }
-
ブロック数の検証
- マニフェストで期待されるブロック数と実際のブロック数を比較
-
暗号化キーの検証
-
manifest.secureがtrueの場合、secretKeyが必須
-
-
並列処理
await Promise.all(images.map(async (image) => { // 各画像を並列処理 }));
-
効率的なバッファ操作
- Buffer.concat を使用した効率的な結合
- メモリの事前割り当て
-
Jimp による画像処理
- Pure JavaScript実装
- プラットフォーム非依存
Image Shield の Node.js パッケージは、以下の特徴を持つ画像保護システムです:
-
処理:
元画像 → 読み込み → RGBA変換 → ブロック分割 → シャッフル → 断片化PNG出力 - 用途: 簡易的な画像の分散保存
- セキュリティ: 低(シード値が分かれば復元可能)
- パフォーマンス: 高速
-
処理:
元画像 → 読み込み → RGBA変換 → 暗号化 → ブロック分割 → シャッフル → 断片化PNG出力 - 用途: セキュアな画像の分散保存
- セキュリティ: 高(AES-256-CBC + ブロックシャッフル)
- パフォーマンス: やや低速(暗号化処理のため)
- 暗号化: AES-256-CBC
- シャッフル: Fisher-Yates(シード付き)
- 画像処理: Jimp(RGBA変換、PNG生成)
- ブロック分割: カスタマイズ可能なサイズ
- 二重保護: 暗号化とブロックシャッフルの組み合わせ
- 決定論的復元: シード値とマニフェストで完全に復元可能
- メタデータ保護: 画像サイズ情報も暗号化データに含める