雑誌のテーマからプロジェクト実践へ
該当記事に関連するサービス・技術ページ
実務では、AES 暗号化 Delphi が失敗するのは稀で、問題はむしろ周辺条件にあります:データはストリームとして処理する必要がある(ファイル、BLOB、バックアップ)、旧フォーマットは引き続き読み取れる必要がある、運用ではデバッグ性(ヘッダ、バージョン管理)と安全なデフォルト(Salt/IVはランダムで再利用しない)が求められます。このソーススニペットは単に「Encrypt/Decrypt」を示すだけでなく、ヘッダ、バージョン、Salt、IV を備えた小さく信頼性の高いフォーマットと、鍵導出のための PBKDF2、および整合性を追加すべき箇所を示しています。
なぜ「AESで文字列を暗号化する」だけではほとんどの場合不十分か
個別の企業向けソフトウェアでは、暗号化は通常、次の三つの箇所で現れます:(1)構成/シークレット(例:認証情報)、(2)交換/エクスポートファイル、(3)静的データ(例:アーカイブ、ドキュメントコンテナ)。単純なアプローチ「パスワード → AESキー → 文字列入出力」はすぐに破綻します:
- IV の再利用:CBC や GCM のようなモードでは、初期化ベクトル(IV)は各暗号化ごとに一意でなければなりません。定数の IV は、パスワードが強力でも情報漏洩になります。
- パスワードからのキー(KDF なし):パスワードを直接キーとして使う(あるいは一度だけハッシュする)と、オフライン攻撃を招きます。PBKDF2 のような KDF(鍵導出関数)は攻撃者の作業を遅らせます。
- フォーマットのバージョンがない:ヘッダやバージョンがないと、後でイテレーション数やアルゴリズム、パラメータを変更した場合に既存データが使えなくなります。
- 整合性がない:AES-CBC は暗号化はしますが、改ざんを防ぎません。認証(例:HMAC や GCM のような AEAD)がないと、ビット改ざんやパディングの問題が発生し、診断が難しいエラーになります。
この投稿の要点:ストリーミングをサポートし、バージョン管理可能で、一般的なミスを避ける小さなコンテナフォーマットです。
AES 暗号化 Delphi — ヘッダ、Salt、IV、PBKDF2 を用いた設計
ここでは、データベースの BLOB やメッセージペイロードでも使えるシンプルなコンテナフォーマットを定義します:
- Magic:4 バイト、例
NBAE(「これが当フォーマットか?」の簡易チェック) - Version:1 バイト(マイグレーションを可能にする)
- KDF-Parameter:イテレーション数(4 バイト)
- Salt:16 バイト(ファイルごとにランダム)
- IV:16 バイト(AES-CBC 用、ファイルごとにランダム)
- Ciphertext:暗号化されたペイロード(ストリーミング可能)
重要:Salt と IV は秘密にする必要はありません。各暗号化で新しくすることが重要です。パスワード自体は秘密として保持し、そこから導出されたキーは保存しません。
AES 暗号化 Delphi をストリームで扱う:コンテナの書き込み/読み取り
コードはあえて「設計図」として書かれています:明確に分離された関数、検証可能なヘッダ、隠れたグローバルは無し。AES と PBKDF2 には多くのチームが実績のある暗号ライブラリ(例:DEC)を利用します。このスニペットはフォーマットとストリーミングパターンを示しており、AES/PBKDF2 の呼び出しはライブラリに応じて差し替え可能なようにカプセル化されています。
unit Nb.AesContainer;
interface
uses
System.SysUtils, System.Classes, System.NetEncoding;
type
ENbCryptoError = class(Exception);
TNbAesContainer = class
public
class procedure EncryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string; const AIterations: Cardinal = 200000);
class procedure DecryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string);
class function EncryptBytesToBase64(const APlain: TBytes; const APassword: string): string;
class function DecryptBase64ToBytes(const ACipherB64: string; const APassword: string): TBytes;
end;
implementation
const
CMagic: array[0..3] of AnsiChar = ('N','B','A','E');
CVersion: Byte = 1;
CSaltLen = 16;
CIvLen = 16;
type
TNbHeaderV1 = packed record
Magic: array[0..3] of AnsiChar;
Version: Byte;
Iterations: Cardinal; // little endian
Salt: array[0..CSaltLen-1] of Byte;
IV: array[0..CIvLen-1] of Byte;
end;
// --- 暗号スタックに応じて実装すべき依存関係 ---
procedure FillRandomBytes(var B: TBytes);
begin
// 暗号用乱数: OS の CSPRNG を使用してください(Windows BCryptGenRandom,
// Linux getrandom/urandom)。ここでは意図的にプレースホルダです。
raise ENbCryptoError.Create('FillRandomBytes: CSPRNG がバインドされていません');
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// 例えば DEC (PBKDF2) または他のライブラリで実装する。
// 結果は AKeyLen バイト。
raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: バインドされていません');
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// ライブラリによる実装:
// - KeyLen = 32 Bytes
// - IVLen = 16 Bytes
// - PKCS#7 Padding
// 重要: ストリーム指向で処理し、全てをメモリに読み込まないこと。
raise ENbCryptoError.Create('AES256_CBC_EncryptStream: バインドされていません');
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create('AES256_CBC_DecryptStream: バインドされていません');
end;
// --- ヘルパー ---
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('ヘッダーを書き込めませんでした');
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('ヘッダーが不完全です');
if (H.Magic[0] <> CMagic[0]) or (H.Magic[1] <> CMagic[1]) or
(H.Magic[2] <> CMagic[2]) or (H.Magic[3] <> CMagic[3]) then
raise ENbCryptoError.Create('有効なコンテナではありません(Magic が一致しません)');
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt('不明なコンテナバージョン: %d', [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create('反復回数が妥当な範囲外です');
Result := H;
end;
class procedure TNbAesContainer.EncryptStreamToStream(const AIn, AOut: TStream;
const APassword: string; const AIterations: Cardinal);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = '' then
raise ENbCryptoError.Create('パスワードは空にできません');
// Salt/IV を生成
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// ヘッダーを設定
Move(CMagic[0], H.Magic[0], Length(CMagic));
H.Version := CVersion;
H.Iterations := AIterations;
Move(Salt[0], H.Salt[0], CSaltLen);
Move(IV[0], H.IV[0], CIvLen);
WriteHeaderV1(AOut, H);
// 鍵を導出(AES-256 のための 32 バイト)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// データを暗号化(暗号文はヘッダーの直後に続きます)
AES256_CBC_EncryptStream(Key, IV, AIn, AOut);
end;
class procedure TNbAesContainer.DecryptStreamToStream(const AIn, AOut: TStream;
const APassword: string);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = '' then
raise ENbCryptoError.Create('パスワードは空にできません');
H := ReadHeaderV1(AIn);
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
Move(H.Salt[0], Salt[0], CSaltLen);
Move(H.IV[0], IV[0], CIvLen);
Key := PBKDF2_HMAC_SHA256(APassword, Salt, H.Iterations, 32);
// Entschlüsseln ab aktueller Stream-Position (nach Header)
AES256_CBC_DecryptStream(Key, IV, AIn, AOut);
end;
class function TNbAesContainer.EncryptBytesToBase64(const APlain: TBytes;
const APassword: string): string;
var
InS, OutS: TBytesStream;
begin
InS := TBytesStream.Create(APlain);
try
OutS := TBytesStream.Create;
try
EncryptStreamToStream(InS, OutS, APassword);
Result := TNetEncoding.Base64.EncodeBytesToString(OutS.Bytes, 0, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
class function TNbAesContainer.DecryptBase64ToBytes(const ACipherB64,
APassword: string): TBytes;
var
Cipher: TBytes;
InS, OutS: TBytesStream;
begin
Cipher := TNetEncoding.Base64.DecodeStringToBytes(ACipherB64);
InS := TBytesStream.Create(Cipher);
try
OutS := TBytesStream.Create;
try
DecryptStreamToStream(InS, OutS, APassword);
Result := OutS.Bytes;
SetLength(Result, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
end.目的: ファイルやBLOBに適した最小限のコンテナ。バージョン管理とKDFパラメータを含む。 前提条件: 基盤としてOS由来の本物のCSPRNG(暗号学的に安全な乱数)と堅牢なAES/PBKDF2実装を置く必要がある。 落とし穴: 『適当な』乱数を使わないこと(Random()は使用しない)、IVを固定しないこと、復号時にはパスワード誤りとデータ破損を明確に区別してエラー処理を設計すること。 バリエーション: CBCの代わりにAEAD(下記参照)を使う、またはヘッダにアルゴリズムIDとHMACを追加する。
整合性: 運用環境でAES-CBC単体がなぜ危険か
AES-CBCは多くのレガシー環境でまだ残っており、追加で整合性保護を用いれば動作することもある。しかし整合性がなければ攻撃者が暗号文を改ざんできる。能動的な攻撃がなくても、伝送エラーや破損したストレージ層が原因で診断の難しい「Padding」エラーが発生することがある。
実用的な選択肢:
- Encrypt-then-HMAC: 暗号文の後にヘッダ+暗号文に対するHMAC(例: HMAC-SHA-256)を書き込む。読み取り時はまずHMACを検証してから復号する。これには理想的にはPBKDF2から二つの鍵を導出する(例: 64バイト→AES用32バイト、HMAC用32バイト)方がよく、同一鍵を二重に使うべきではない。
- AES-GCM: AEADモード(Authenticated Encryption with Associated Data)。暗号文と認証タグを出力する。Delphi-ライブラリがGCMを安定してサポートしているなら、現在では多くの場合これが最も確実な選択肢である。ヘッダフィールドは暗号化せずにAADとして認証できる。
互換性などの理由でCBCを使い続ける必要がある場合は、Encrypt-then-HMACが堅牢な補強となる。新規フォーマットではGCMが有益で、認証を同時に得られ、エラーの症状がより明確になる。
異常に重要: 「暗号学的乱数」と System.Hash が不十分な理由
Delphiプロジェクトでよくあるレガシー的な反射: 「タイムスタンプ+何かに対してSHA256を取ればランダムになる」といった考え。それは信頼できる基盤ではない。SaltやIVにはOSのCSPRNG(Cryptographically Secure Pseudo Random Number Generator)が必要だ。Windowsでは通常BCrypt API(CNG)がそれに当たり、Linuxではカーネル生成器(getrandom() や /dev/urandom)が相当する。実務上の違いは重要で、CSPRNGは観測された値から将来の値を予測できないよう設計されている。
アーキテクチャ的工夫: これを小さな「RandomProvider」ユニットにカプセル化し、テスト時にモックできるようにする。こうすると再現可能なテスト(モックで固定シードを使う)と本番での実際の安全性(OS-CSPRNG)という二つの要件を両立できる。これにより、ホットフィックスで手早く済ませるために再びRandom()が持ち込まれることを防げる。
デバッグとレガシー移行: バージョニングは贅沢ではない
ヘッダは単なる「暗号的美しさ」だけでなく、保守性のためにある:
- 反復回数の調整: PBKDF2の反復回数は年月とともに変わる。ヘッダフィールドがあれば、古いデータを読み取れなくすることなく後から回数を増やせる。
- フォーマット変更: 例としてバージョン2でAES-GCMに移行したりHMACを追加したりできる。
- 現場での診断: Magic/Version によりデータを復号せずともログやツールで迅速なチェックが可能になる。
実践的なヒント:ヘッダー(Magic/Version/Iterations)のみを読み取りログに書き込む小さな「Inspector」を実装してください。これにより、多くのサポート事例(「ここにはどのバージョンがあるか?」)をパスワード処理なしで解決できます。
クリーンに移行する:Big Bangではなく「Read old, write new」
古いフォーマット(例:固定IV、KDF未使用、Blowfish/3DES、独自実装のXORなど)を置き換える場合、Delphiプロジェクトで有効だったパターンがあります:読み取り時に複数のフォーマット(Magic/Version やフォールバックのヒューリスティック)を識別し、書き込みは新しいフォーマットのみを生成する、というものです。加えて、復号に成功した際にバックグラウンドで再暗号化する(「lazy migration」)ことも、プロセスに合えば有効です。これによりロールアウトのリスクを下げ、「一度に全てを再暗号化する」という保守ウィンドウを回避できます。
スレッディングとストリーミング:Delphiにおける典型的な課題
暗号処理はしばしばワーカースレッドで行われます(例:エクスポート時、Kundenportalへのアップロード時、大きなアーカイブの書き込み時)。Delphiプロジェクトで頻出するポイントは次の二点です:
- ストリーム位置:暗号化/復号の前に明確な契約を定めてください。入力ストリームは現在の位置から読み取り、出力ストリームは現在の位置から書き込みます。ストリームを再利用する場合は必ず
Position := 0を明示的に設定してください。 - メモリの尖峰:“全部をTBytesで扱う“ のは避けてください。大きなファイルにはストリームアプローチが重要です。もしお使いの暗号ライブラリがバイト配列のみを受け入れるなら、ストリーム対応実装に移すか、バッファ付きのアダプタを作る追加作業の価値があります。
Services内(Windowsや Linux-Services)で暗号化を行う場合は、例外ログをきちんと分けて記録することにも注意してください。例えば「パスワード誤り」「ヘッダー破損」「Tag/HMAC 無効」はそれぞれ異なる運用事象であり、区別できるようにするべきです。外部に返すエラーメッセージは詳細すぎないように(APIエラーとして「ブロック7のパディングが間違っている」などは出さない)し、内部ログには詳細を残す、という取り扱いが重要です。
このアプローチが有効な場合 — およびリスクが高まる場面
有効なのは次のような場合です: (a) 暗号化されたエクスポート/インポートデータを長期保存する、(b) 複数バージョンのプログラムを並行運用する、(c) データをストリームとして処理する、あるいは (d) 複数モジュール(Client/Server/Tooling)向けに整った暗号インタフェースが必要な場合。
不向き(破綻しやすい)のは「これで全てを解決しようとする」場合です:TransportはTLSの責任領域であり、自作のAESラッパーで代替すべきではありません。Secrets(パスワード、トークン)は多くの場合OS-Secret-StoreやVaultを使う方が適切です。異なる言語との相互運用が必要な場合は、ヘッダー、エンディアン、エンコーディングを正確に文書化する(あるいは確立されたフォーマットを利用する)必要があります。
まとめ:DelphiにおけるAESはアルゴリズムというよりエンジニアリング
この断片で得られる本当の成果は「AESが動くこと」ではなく、運用に耐えるフォーマットです:ランダムなSaltとIV、バージョン付きヘッダー、ペイロード内のPBKDF2パラメータ、ストリーム対応の処理。新しいフォーマットでは可能な限り整合性確保(AES-GCM や Encrypt-then-HMAC)を加えてください。こうして「なんとなく暗号化している」状態が、年単位で保守・移行可能な企業向けソフトウェアの構成要素になります。
このようなコンテナを既存の Delphi 環境に統合する場合、あるいはレガシーフォーマットからきれいにマイグレーションする必要がある場合は、短いアーキテクチャチェック(鍵管理、フォーマットバージョン、運用/ロギング)を行う価値があります。詳細は必要に応じてお打ち合わせで確認いたします:
実務的な文脈では、統合やデータフロー、将来的な改良が適切に連携する必要がある場合、Delphi Aes および Pbkdf2 Delphi も重要な役割を果たします。
次のステップ
テーマが実際のプロジェクトになる場合、アーキテクチャ、既存資産、運用は早い段階でまとめて検討するべきです。
私たちは単なる個別の問い合わせへの対応にとどまらず、ソースの断片やレガシー課題、ポータルの構想が堅牢な企業向けプロジェクトへと成長する段階まで支援します。
- 既存環境、目標像、技術的リスクを一体として評価します。
- REST、データアクセス、ポータル、ロールアウトは後工程として先送りされることはありません。
- 早期に、どのアプローチが経済的かつ運用面で実行可能かを判断できます。