Skip to content
Y.Yamamoto edited this page Oct 9, 2025 · 4 revisions

AI-generated

Welcome to the image-shield wiki!

このドキュメントでは、Image Shield の Node.js パッケージにおける画像の暗号化(encrypt)と復号化(decrypt)の処理フローを詳細に説明します。

目次

  1. 処理モード
  2. 暗号化処理(Encrypt)
  3. 復号化処理(Decrypt)
  4. 技術詳細

処理モード

Image Shield は secretKey の有無により、2つの動作モードを持ちます。

🔀 Shuffle Only Mode(シャッフルのみモード)

secretKey が設定されていない場合、シャッフルのみが実行されます。

元画像 → 読み込み → RGBA変換 → ブロック分割 → シャッフル → 断片化PNG出力

特徴:

  • 暗号化は行わない(manifest.secure = false
  • ブロックの配置をランダムに入れ替えるのみ
  • 高速な処理が可能
  • セキュリティレベルは低い(シャッフルのシードが分かれば復元可能)

🔐 Shuffle + Encrypt Mode(シャッフル + 暗号化モード)

secretKey が設定されている場合、暗号化とシャッフルの両方が実行されます。

元画像 → 読み込み → RGBA変換 → 暗号化 → ブロック分割 → シャッフル → 断片化PNG出力

特徴:

  • AES-256-CBC による暗号化を実施(manifest.secure = true
  • ブロックシャッフルと暗号化の二重保護
  • セキュアな画像保護が可能
  • 処理時間がやや長くなる

暗号化処理(Encrypt)

全体フロー

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[完了]
Loading

ステップ詳細

1. 初期化と入力検証

ファイル: packages/node/src/index.ts

static async encrypt(options: EncryptOptions): Promise<void>
  • imagePaths: 元画像のパス配列
  • config: 断片化設定(ブロックサイズ、prefix、seed等)
  • outputDir: 出力先ディレクトリ
  • secretKey: (オプション)暗号化キー

検証項目:

  • imagePaths が空でない配列であること
  • outputDir が有効な文字列であること

2. ImageFragmenter の生成

ファイル: packages/node/src/fragmenter.ts

constructor(config: FragmentationConfig, secretKey?: string)

設定の初期化:

  • blockSize: ブロックサイズ(デフォルト: 32px)
  • prefix: 出力ファイルのプレフィックス
  • seed: シャッフル用のシード値(未指定の場合は自動生成)
  • restoreFileName: 元のファイル名を保持するか

3. 画像の暗号化(secretKey ありの場合)

ファイル: packages/node/src/block.ts

async function encryptPngImageBuffer(
  pngBuffer: Buffer,
  secretKey: string,
  manifestId: string,
): Promise<Buffer>

処理手順:

  1. RGBA画像バッファの抽出

    • PNG画像をJimpで読み込み
    • RGBA形式(4チャネル)に変換
    • 生のピクセルデータを取得
  2. メタデータの作成

    // 12バイトのメタデータ
    [width: 4 bytes][height: 4 bytes][imageBufferLength: 4 bytes]
  3. データの結合と暗号化

    • メタデータ + 画像バッファを結合
    • AES-256-CBC で暗号化
    • IV(初期化ベクトル)はマニフェストIDから生成
    // Manifest ID (UUID) → 16バイトIVに変換
    const iv = CryptoUtils.uuidToIV(manifestId);
    const encryptedData = CryptoUtils.encryptBuffer(dataToEncrypt, secretKey, iv);
  4. 暗号化データのPNG化

    • 暗号化データを格納する最適な画像サイズを計算
    • パディングを追加(必要に応じて)
    • RGBA画像として再構成
    • PNG形式で出力

4. ブロック分割

ファイル: packages/node/src/block.ts

async function imageFileToBlocks(
  input: string | Buffer,
  blockSize: number,
): Promise<ImageFileToBlocksResult>

処理手順:

  1. 画像の読み込みとRGBA変換

    • Jimpライブラリを使用
    • すべての画像形式をRGBAに統一
  2. ブロック数の計算

    blockCountX = Math.ceil(width / blockSize)
    blockCountY = Math.ceil(height / blockSize)
  3. ブロックの抽出

    • 左上から右下へ、行ごとにブロックを抽出
    • 各ブロックは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]   │
└────────────────────┘

5. 全ブロックの統合

ファイル: packages/node/src/fragmenter.ts

private async _processSourceImages(
  imagePaths: string[],
  manifestInfo: Pick<ManifestData, "id">,
)
  • 複数の画像のブロックを1つの配列に統合
  • 各画像のメタデータ(サイズ、ブロック数等)を記録

6. ブロックのシャッフル

ライブラリ: @tuki0918/seeded-shuffle

const shuffledBlocks = shuffle(allBlocks, manifest.config.seed);
  • Fisher-Yates シャッフルアルゴリズム
  • シード値により再現可能なランダム配置
  • 同じシード値で元の順序に戻すことが可能(復号化時)

7. 断片画像の生成

ファイル: packages/node/src/fragmenter.ts

private async _createFragmentedImages(
  shuffledBlocks: Buffer[],
  fragmentBlocksCount: number[],
  manifest: ManifestData,
): Promise<Buffer[]>

処理手順:

  1. 各断片へのブロック配分

    • 全ブロックを画像数で均等に分配
    • 各断片が含むブロック数を計算
  2. 断片画像のサイズ計算

    // ブロックを正方形に近い配置で並べる
    blocksPerRow = Math.ceil(Math.sqrt(blockCount))
    imageWidth = blocksPerRow × blockSize
    imageHeight = Math.ceil(blockCount / blocksPerRow) × blockSize
  3. 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

8. マニフェストファイルの生成

ファイル: 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                // 暗号化の有無
}

9. ファイル出力

ファイル: packages/node/src/index.ts

  • manifest.json: マニフェストファイル
  • fragment_0.png, fragment_1.png, ...: 断片化された画像ファイル

復号化処理(Decrypt)

全体フロー

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[完了]
Loading

ステップ詳細

1. 初期化と入力検証

ファイル: packages/node/src/index.ts

static async decrypt(options: DecryptOptions): Promise<void>
  • imagePaths: 断片画像のパス配列
  • manifestPath: マニフェストファイルのパス
  • outputDir: 出力先ディレクトリ
  • secretKey: (オプション)復号化キー

検証項目:

  • imagePaths が空でない配列であること
  • manifestPath が有効な文字列であること
  • outputDir が有効な文字列であること

2. マニフェストの読み込み

ファイル: packages/node/src/index.ts

const manifest = await readJsonFile<ManifestData>(manifestPath);
  • マニフェストファイルから元の画像情報を取得
  • ブロックサイズ、シード値、画像のメタデータを読み取る

3. ImageRestorer の生成

ファイル: packages/node/src/restorer.ts

constructor(secretKey?: string)
  • secretKey を保持(暗号化されている場合に使用)

4. 断片画像からブロックの抽出

ファイル: packages/node/src/restorer.ts

private async _extractBlocksFromFragments(
  fragmentImages: (string | Buffer)[],
  manifest: ManifestData,
  fragmentBlocksCount: number[],
): Promise<Buffer[]>

処理手順:

  1. 各断片画像の読み込み

    • ファイルパスまたはBufferから画像データを取得
  2. ブロックの抽出

    const { blocks } = await imageFileToBlocks(buf, manifest.config.blockSize);
    • 断片画像をブロックサイズで分割
    • 必要な数のブロックのみを取得(余分なパディング領域を除外)
  3. 全ブロックの統合

    • すべての断片からのブロックを1つの配列に結合

5. ブロックのアンシャッフル

ライブラリ: @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]

6. 元の画像ごとにブロックを分割

ファイル: 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]

7. 画像の再構成

ファイル: packages/node/src/block.ts

export async function blocksToPngImage(
  blocks: Buffer[],
  width: number,
  height: number,
  blockSize: number,
): Promise<Buffer>

処理手順:

  1. 画像バッファの作成

    const imageBuffer = Buffer.alloc(width * height * RGBA_CHANNELS);
  2. ブロックの配置

    • 左上から右下へ、行ごとにブロックを配置
    • エッジブロックは実際のサイズで配置
  3. 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]   │
└────────────────────┘
→ 元画像に復元

8. 画像の復号化(secretKey ありの場合)

ファイル: packages/node/src/block.ts

async function decryptPngImageBuffer(
  encryptedPngBuffer: Buffer,
  secretKey: string,
  manifestId: string,
): Promise<Buffer>

処理手順:

  1. 暗号化された画像バッファの抽出

    • PNG画像からRGBAバッファを取得
  2. パディングの除去

    const encryptedData = removePadding(encryptedImageData);
    • 末尾のゼロバイトを削除
  3. データの復号化

    const iv = CryptoUtils.uuidToIV(manifestId);
    const decryptedData = CryptoUtils.decryptBuffer(encryptedData, secretKey, iv);
    • AES-256-CBC で復号化
    • IV(初期化ベクトル)はマニフェストIDから生成
  4. メタデータの解析

    const { width, height, imageBufferLength } = parseImageBufferMetadata(
      decryptedData.subarray(0, 12)
    );
    • 先頭12バイトから元の画像サイズとデータ長を取得
  5. 元の画像バッファの抽出

    const originalImageBuffer = decryptedData.subarray(12, 12 + imageBufferLength);
  6. PNG画像として再構成

    • RGBAバッファからPNG形式に変換

9. ファイル出力

ファイル: packages/node/src/index.ts

  • 元のファイル名で復元(restoreFileNametrue の場合)
  • または 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 bytes

5. 抽出処理の実装

// ブロック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
└────┴────┴────┘

重要なポイント:

  1. エッジブロックは元の画像サイズに合わせて自動的に調整される
  2. すべてのブロックはRGBA形式で保存され、チャネル数は常に4
  3. ブロックの抽出と配置は行ごと(左→右、上→下)に処理される
  4. シャッフル後も各ブロックは元のサイズ情報を保持
  5. 断片画像は正方形に近い配置で生成される

メモリ効率

ストリーミング処理の不使用:

現在の実装では、画像全体をメモリに読み込んで処理します。 非常に大きな画像(数GB)を扱う場合は、メモリ制約に注意が必要です。

Buffer の使用:

  • Node.js の Buffer を使用してバイナリデータを効率的に扱う
  • コピーを最小限に抑える設計

エラーハンドリング

検証項目:

  1. 入力検証

    • ファイルパスの存在確認
    • マニフェストの整合性チェック
  2. 断片数の検証

    if (manifestImageCount !== fragmentImageCount) {
      throw new Error('Fragment image count mismatch');
    }
  3. ブロック数の検証

    • マニフェストで期待されるブロック数と実際のブロック数を比較
  4. 暗号化キーの検証

    • manifest.securetrue の場合、secretKey が必須

パフォーマンス最適化

  1. 並列処理

    await Promise.all(images.map(async (image) => {
      // 各画像を並列処理
    }));
  2. 効率的なバッファ操作

    • Buffer.concat を使用した効率的な結合
    • メモリの事前割り当て
  3. Jimp による画像処理

    • Pure JavaScript実装
    • プラットフォーム非依存

まとめ

Image Shield の Node.js パッケージは、以下の特徴を持つ画像保護システムです:

🔀 Shuffle Only Mode

  • 処理: 元画像 → 読み込み → RGBA変換 → ブロック分割 → シャッフル → 断片化PNG出力
  • 用途: 簡易的な画像の分散保存
  • セキュリティ: 低(シード値が分かれば復元可能)
  • パフォーマンス: 高速

🔐 Shuffle + Encrypt Mode

  • 処理: 元画像 → 読み込み → RGBA変換 → 暗号化 → ブロック分割 → シャッフル → 断片化PNG出力
  • 用途: セキュアな画像の分散保存
  • セキュリティ: 高(AES-256-CBC + ブロックシャッフル)
  • パフォーマンス: やや低速(暗号化処理のため)

主要技術

  • 暗号化: AES-256-CBC
  • シャッフル: Fisher-Yates(シード付き)
  • 画像処理: Jimp(RGBA変換、PNG生成)
  • ブロック分割: カスタマイズ可能なサイズ

セキュリティ設計

  1. 二重保護: 暗号化とブロックシャッフルの組み合わせ
  2. 決定論的復元: シード値とマニフェストで完全に復元可能
  3. メタデータ保護: 画像サイズ情報も暗号化データに含める