ファイルにパスワードを付加する安全な方法
パスワードのハッシュ化:
パスワードを直接使うと長さや内容に依存するため、
SHA-256でハッシュ化し、
固定長のハッシュ値をもとに挿入位置や間隔を決定する。
ランダムバイトの挿入:
挿入するバイトをランダムに生成し、
パスワードに依存したシードを使うことで予測を難しくする。
一貫性と柔軟性:
ハッシュ値から開始位置、間隔、バイト数を動的に決定し、
ファイル全体に適用する。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class FileProtector
{
/// <summary>
/// パスワードをSHA-256でハッシュ化し、バイト配列として返す。
/// </summary>
/// <param name="password_">ユーザーが設定したパスワード</param>
/// <returns>ハッシュ化されたバイト配列(256ビット=32バイト)</returns>
private static byte[] hash_password(string password_)
{
using (var sha256 = SHA256.Create())
{
// パスワードをUTF-8でバイト配列に変換し、ハッシュ化
return sha256.ComputeHash(Encoding.UTF8.GetBytes(password_));
}
}
/// <summary>
/// 元のファイルデータにパスワードに基づくランダムバイトを挿入し、新しいデータを作成する。
/// </summary>
/// <param name="file_data_">元のファイルデータ</param>
/// <param name="hash_">パスワードのハッシュ値</param>
/// <param name="new_file_data_">バイトが挿入された新しいファイルデータ</param>
private static void insert_bytes(byte[] file_data_, byte[] hash_, out byte[] new_file_data_)
{
// ハッシュ値から挿入の開始位置、間隔、バイト数を決定
uint start_pos = BitConverter.ToUInt32(hash_, 0) % (uint)file_data_.Length; // 開始位置(ファイルサイズで割った余り)
ushort interval = (ushort)(BitConverter.ToUInt16(hash_, 4) % 10 + 1); // 挿入間隔(1~10バイト)
ushort byte_count = (ushort)(BitConverter.ToUInt16(hash_, 6) % 5 + 1); // 挿入バイト数(1~5バイト)
// ランダムバイト生成のためのシード(ハッシュ値から取得)
int seed = BitConverter.ToInt32(hash_, 8);
var random = new Random(seed);
// 新しいファイルデータをメモリストリームに構築
using (var ms = new MemoryStream())
{
int pos = 0; // 現在の処理位置
while (pos < file_data_.Length)
{
// 開始位置まで元のデータをそのままコピー
if (pos < start_pos)
{
int copy_length = (int)Math.Min(start_pos - pos, file_data_.Length - pos);
ms.Write(file_data_, pos, copy_length);
pos += copy_length;
}
else
{
// 挿入位置にランダムバイトを追加
byte[] random_bytes = new byte[byte_count];
random.NextBytes(random_bytes);
ms.Write(random_bytes, 0, byte_count);
// 次の挿入位置まで元のデータをコピー
int next_pos = pos + interval;
if (next_pos < file_data_.Length)
{
ms.Write(file_data_, pos, interval);
pos = next_pos;
}
else
{
ms.Write(file_data_, pos, file_data_.Length - pos);
pos = file_data_.Length;
}
}
}
new_file_data_ = ms.ToArray();
}
}
/// <summary>
/// バイトが挿入されたファイルデータから無関係なバイトを除去し、元のデータに戻す。
/// </summary>
/// <param name="file_data_">バイトが挿入されたファイルデータ</param>
/// <param name="hash_">パスワードのハッシュ値</param>
/// <param name="original_file_data_">復元された元のファイルデータ</param>
private static void remove_bytes(byte[] file_data_, byte[] hash_, out byte[] original_file_data_)
{
// ハッシュ値から挿入時の開始位置、間隔、バイト数を再計算
uint start_pos = BitConverter.ToUInt32(hash_, 0) % (uint)file_data_.Length;
ushort interval = (ushort)(BitConverter.ToUInt16(hash_, 4) % 10 + 1);
ushort byte_count = (ushort)(BitConverter.ToUInt16(hash_, 6) % 5 + 1);
// 元のデータをメモリストリームに構築
using (var ms = new MemoryStream())
{
int pos = 0; // 現在の処理位置
while (pos < file_data_.Length)
{
// 開始位置までデータをそのままコピー
if (pos < start_pos)
{
int copy_length = (int)Math.Min(start_pos - pos, file_data_.Length - pos);
ms.Write(file_data_, pos, copy_length);
pos += copy_length;
}
else
{
// 挿入されたバイトをスキップ
pos += byte_count;
// 次の挿入位置までデータをコピー
int next_pos = pos + interval;
if (next_pos < file_data_.Length)
{
ms.Write(file_data_, pos, interval);
pos = next_pos;
}
else
{
ms.Write(file_data_, pos, file_data_.Length - pos);
pos = file_data_.Length;
}
}
}
original_file_data_ = ms.ToArray();
}
}
/// <summary>
/// パスワードを使ってファイルを保存する。
/// </summary>
/// <param name="file_path_">保存先のファイルパス</param>
/// <param name="password_">設定するパスワード</param>
/// <param name="file_data_">保存する元のファイルデータ</param>
public static void save_file(string file_path_, string password_, byte[] file_data_)
{
byte[] hash = hash_password(password_); // パスワードをハッシュ化
insert_bytes(file_data_, hash, out byte[] new_file_data); // バイトを挿入
File.WriteAllBytes(file_path_, new_file_data); // ファイルに書き込み
}
/// <summary>
/// パスワードを使ってファイルを開く。
/// </summary>
/// <param name="file_path_">読み込むファイルパス</param>
/// <param name="password_">設定されたパスワード</param>
/// <returns>復元された元のファイルデータ</returns>
public static byte[] load_file(string file_path_, string password_)
{
byte[] hash = hash_password(password_); // パスワードをハッシュ化
byte[] file_data = File.ReadAllBytes(file_path_); // ファイルを読み込み
remove_bytes(file_data, hash, out byte[] original_file_data); // バイトを除去
return original_file_data;
}
// 使用例
static void Main()
{
string file_path = "test.bin";
string password = "MySecretPass123";
byte[] original_data = Encoding.UTF8.GetBytes("これはテストデータです。");
// ファイル保存
save_file(file_path, password, original_data);
Console.WriteLine("ファイルが保存されました。");
// ファイル読み込み
byte[] loaded_data = load_file(file_path, password);
Console.WriteLine("読み込んだデータ: " + Encoding.UTF8.GetString(loaded_data));
}
}
hash_password 関数
パスワードをSHA-256でハッシュ化し、
32バイトの固定長データに変換する。
これにより、パスワードの長さや内容に依存せず一貫した値を生成する。
insert_bytes 関数
元のファイルデータにランダムバイトを挿入する。
開始位置(start_pos)、間隔(interval)、バイト数(byte_count)を
ハッシュ値から決定する。
ランダムバイトはパスワード依存のシードで生成し、予測を困難にする。
remove_bytes 関数
挿入されたバイトを除去し、元のデータを復元する。
正しいパスワード(ハッシュ値)でないと位置や数が一致せず、
データが壊れる。
save_file および load_file 関数
ファイルの保存と読み込みを簡潔に実装。
ユーザーが直接呼び出せるインターフェース。
《 複数のファイルへの対応 》
パスワードをSHA-256でハッシュ化し、
そのハッシュ値から挿入位置や値を決定している場合、
同じパスワードでは常に同じハッシュ値が生成され、
結果として同じ位置と値が使用されてしまう。
これを回避するため、以下のようにソルト(salt)を導入する。
ソルトの導入:
ファイルごとにランダムなソルトを生成し、ファイルの先頭に保存する。
ハッシュ化:
パスワードとソルトを組み合わせてSHA-256でハッシュ化し、
そのハッシュ値から挿入位置、間隔、バイト数を決定する。
利点:
同じパスワードでも、ソルトが異なるため
各ファイルごとに異なるハッシュ値が生成され、挿入位置や値が異なる。
これにより、バイナリ配列の比較によるパスワードの割り出しを防ぐ。
実装の詳細ソルトの生成と保存:
ファイル保存時に、16バイトのランダムなソルトを生成する。
このソルトをファイルの先頭に保存し、その後に元のファイルデータを続ける。
ハッシュ化:
パスワードとソルトを組み合わせてSHA-256でハッシュ化する。
生成されたハッシュ値から、挿入の開始位置、間隔、バイト数を決定する。
バイトの挿入と除去:保存時:
ソルトを先頭に追加し、ハッシュ値に基づいて
ランダムバイトを挿入したデータを作成する。
読み込み時:
ファイルの先頭からソルトを読み取り、パスワードと組み合わせてハッシュ化し、
挿入されたバイトを除去して元のデータを復元する。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class FileProtector
{
private const int SALT_SIZE = 16; // ソルトのサイズ(バイト)
// パスワードとソルトを組み合わせてハッシュ化
private static byte[] HashPasswordWithSalt(string password, byte[] salt)
{
using (var sha256 = SHA256.Create())
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] combined = new byte[passwordBytes.Length + salt.Length];
Buffer.BlockCopy(passwordBytes, 0, combined, 0, passwordBytes.Length);
Buffer.BlockCopy(salt, 0, combined, passwordBytes.Length, salt.Length);
return sha256.ComputeHash(combined);
}
}
// ソルトとバイトを挿入
private static void InsertBytesWithSalt(byte[] fileData, byte[] hash, byte[] salt, out byte[] newFileData)
{
uint startPos = BitConverter.ToUInt32(hash, 0) % (uint)fileData.Length;
ushort interval = (ushort)(BitConverter.ToUInt16(hash, 4) % 10 + 1);
ushort byteCount = (ushort)(BitConverter.ToUInt16(hash, 6) % 5 + 1);
int seed = BitConverter.ToInt32(hash, 8);
var random = new Random(seed);
using (var ms = new MemoryStream())
{
ms.Write(salt, 0, salt.Length); // ソルトを先頭に追加
int pos = 0;
while (pos < fileData.Length)
{
if (pos < startPos)
{
int copyLength = (int)Math.Min(startPos - pos, fileData.Length - pos);
ms.Write(fileData, pos, copyLength);
pos += copyLength;
}
else
{
byte[] randomBytes = new byte[byteCount];
random.NextBytes(randomBytes);
ms.Write(randomBytes, 0, byteCount);
int nextPos = pos + interval;
if (nextPos < fileData.Length)
{
ms.Write(fileData, pos, interval);
pos = nextPos;
}
else
{
ms.Write(fileData, pos, fileData.Length - pos);
pos = fileData.Length;
}
}
}
newFileData = ms.ToArray();
}
}
// バイトを除去
private static void RemoveBytesWithSalt(byte[] fileData, byte[] hash, out byte[] originalFileData)
{
uint startPos = BitConverter.ToUInt32(hash, 0) % (uint)(fileData.Length - SALT_SIZE);
ushort interval = (ushort)(BitConverter.ToUInt16(hash, 4) % 10 + 1);
ushort byteCount = (ushort)(BitConverter.ToUInt16(hash, 6) % 5 + 1);
using (var ms = new MemoryStream())
{
int pos = SALT_SIZE; // ソルトの後から開始
while (pos < fileData.Length)
{
if (pos < startPos + SALT_SIZE)
{
int copyLength = (int)Math.Min(startPos + SALT_SIZE - pos, fileData.Length - pos);
ms.Write(fileData, pos, copyLength);
pos += copyLength;
}
else
{
pos += byteCount; // 挿入バイトをスキップ
int nextPos = pos + interval;
if (nextPos < fileData.Length)
{
ms.Write(fileData, pos, interval);
pos = nextPos;
}
else
{
ms.Write(fileData, pos, fileData.Length - pos);
pos = fileData.Length;
}
}
}
originalFileData = ms.ToArray();
}
}
// ファイル保存
public static void SaveFile(string filePath, string password, byte[] fileData)
{
byte[] salt = new byte[SALT_SIZE];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(salt); // ランダムなソルト生成
}
byte[] hash = HashPasswordWithSalt(password, salt);
InsertBytesWithSalt(fileData, hash, salt, out byte[] newFileData);
File.WriteAllBytes(filePath, newFileData);
}
// ファイル読み込み
public static byte[] LoadFile(string filePathasc = File.ReadAllBytes(filePath);
byte[] salt = new byte[SALT_SIZE];
Buffer.BlockCopy(fileData, 0, salt, 0, SALT_SIZE);
byte[] hash = HashPasswordWithSalt(password, salt);
RemoveBytesWithSalt(fileData, hash, out byte[] originalData);
return originalData;
}
// 使用例
static void Main()
{
string filePath = "test.bin";
string password = "MySecretPass123";
byte[] originalData = Encoding.UTF8.GetBytes("テストデータ");
SaveFile(filePath, password, originalData);
byte[] loadedData = LoadFile(filePath, password);
Console.WriteLine("復元データ: " + Encoding.UTF8.GetString(loadedData));
}
hash_password 関数
パスワードをSHA-256でハッシュ化し、
32バイトの固定長データに変換する。
これにより、パスワードの長さや内容に依存せず一貫した値を生成する。
insert_bytes 関数
元のファイルデータにランダムバイトを挿入する。
開始位置(start_pos)、間隔(interval)、バイト数(byte_count)を
ハッシュ値から決定する。
ランダムバイトはパスワード依存のシードで生成し、予測を困難にする。
remove_bytes 関数
挿入されたバイトを除去し、元のデータを復元する。
正しいパスワード(ハッシュ値)でないと位置や数が一致せず、データが壊れる。
save_file および load_file 関数
ファイルの保存と読み込みを簡潔に実装している。
ユーザーが直接呼び出せるインターフェース。
注意点と改善案ファイルサイズの増加: 挿入バイトが多いと
ファイルサイズが膨らむ。
intervalやbyte_countを調整することで抑えられる。
セキュリティ向上:
ソルトにより、同じパスワードでも
ファイルごとに異なるハッシュ値が生成され、
挿入位置と値が異なる。
バイナリ比較の防止:
ファイルごとに異なるソルトを使用することで、
バイナリ配列の比較によるパスワードの割り出しが困難になる。
実用性:
ソルトはファイルの先頭に固定長(16バイト)で保存されるため、
処理がシンプルで実装しやすい。
《 暗号化 》
暗号化
AES(Advanced Encryption Standard)のCBCモードを使用して、
ファイル全体をパスワードに基づいて暗号化する。
パスワードから鍵を生成し、ランダムな初期化ベクトル(IV)を付加する。
バイト挿入
暗号化されたデータに対して、ランダムなソルトを生成し、
パスワードとソルトを組み合わせたハッシュ値に基づいて
ランダムバイトを挿入する。
ソルトとIVはファイルの先頭に保存され、復号時に利用される。
復号化
保存されたソルトとIVを使用して、挿入されたバイトを除去し、
AESでデータを復号化する。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class FileProtector
{
private const int SALT_SIZE = 16; // ソルトのサイズ(バイト)
private const int IV_SIZE = 16; // AESのIVのサイズ(バイト)
// パスワードからAESの鍵を生成
private static byte[] GenerateKeyFromPassword(string password)
{
using (var sha256 = SHA256.Create())
{
return sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
}
}
// パスワードとソルトを組み合わせてハッシュ化
private static byte[] HashPasswordWithSalt(string password, byte[] salt)
{
using (var sha256 = SHA256.Create())
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] combined = new byte[passwordBytes.Length + salt.Length];
Buffer.BlockCopy(passwordBytes, 0, combined, 0, passwordBytes.Length);
Buffer.BlockCopy(salt, 0, combined, passwordBytes.Length, salt.Length);
return sha256.ComputeHash(combined);
}
}
// AESでファイルを暗号化
private static byte[] EncryptFile(byte[] fileData, byte[] key, byte[] iv)
{
using (var aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(fileData, 0, fileData.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
}
}
// AESでファイルを復号化
private static byte[] DecryptFile(byte[] encryptedData, byte[] key, byte[] iv)
{
using (var aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(encryptedData, 0, encryptedData.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
}
}
// ソルトとバイトを挿入
private static void InsertBytesWithSalt(byte[] encryptedData, byte[] hash, byte[] salt, byte[] iv, out byte[] newFileData)
{
uint startPos = BitConverter.ToUInt32(hash, 0) % (uint)encryptedData.Length;
ushort interval = (ushort)(BitConverter.ToUInt16(hash, 4) % 10 + 1);
ushort byteCount = (ushort)(BitConverter.ToUInt16(hash, 6) % 5 + 1);
int seed = BitConverter.ToInt32(hash, 8);
var random = new Random(seed);
using (var ms = new MemoryStream())
{
ms.Write(salt, 0, salt.Length); // ソルトを先頭に追加
ms.Write(iv, 0, iv.Length); // IVを追加
int pos = 0;
while (pos < encryptedData.Length)
{
if (pos < startPos)
{
int copyLength = (int)Math.Min(startPos - pos, encryptedData.Length - pos);
ms.Write(encryptedData, pos, copyLength);
pos += copyLength;
}
else
{
byte[] randomBytes = new byte[byteCount];
random.NextBytes(randomBytes);
好像 ms.Write(randomBytes, 0, byteCount);
int nextPos = pos + interval;
if (nextPos < encryptedData.Length)
{
ms.Write(encryptedData, pos, interval);
pos = nextPos;
}
else
{
ms.Write(encryptedData, pos, encryptedData.Length - pos);
pos = encryptedData.Length;
}
}
}
newFileData = ms.ToArray();
}
}
// バイトを除去
private static void RemoveBytesWithSalt(byte[] fileData, byte[] hash, out byte[] encryptedData)
{
uint startPos = BitConverter.ToUInt32(hash, 0) % (uint)(fileData.Length - SALT_SIZE - IV_SIZE);
ushort interval = (ushort)(BitConverter.ToUInt16(hash, 4) % 10 + 1);
ushort byteCount = (ushort)(BitConverter.ToUInt16(hash, 6) % 5 + 1);
using (var ms = new MemoryStream())
{
int pos = SALT_SIZE + IV_SIZE; // ソルトとIVの後から開始
while (pos < fileData.Length)
{
if (pos < startPos + SALT_SIZE + IV_SIZE)
{
int copyLength = (int)Math.Min(startPos + SALT_SIZE + IV_SIZE - pos, fileData.Length - pos);
ms.Write(fileData, pos, copyLength);
pos += copyLength;
}
else
{
pos += byteCount; // 挿入バイトをスキップ
int nextPos = pos + interval;
if (nextPos < fileData.Length)
{
ms.Write(fileData, pos, interval);
pos = nextPos;
}
else
{
ms.Write(fileData, pos, fileData.Length - pos);
pos = fileData.Length;
}
}
}
encryptedData = ms.ToArray();
}
}
// ファイル保存
public static void SaveFile(string filePath, string password, byte[] fileData)
{
byte[] salt = new byte[SALT_SIZE];
byte[] iv = new byte[IV_SIZE];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(salt); // ランダムなソルト生成
rng.GetBytes(iv); // ランダムなIV生成
}
byte[] key = GenerateKeyFromPassword(password);
byte[] encryptedData = EncryptFile(fileData, key, iv);
byte[] hash = HashPasswordWithSalt(password, salt);
InsertBytesWithSalt(encryptedData, hash, salt, iv, out byte[] newFileData);
File.WriteAllBytes(filePath, newFileData);
}
// ファイル読み込み
public static byte[] LoadFile(string filePath, string password)
{
byte[] fileData = File.ReadAllBytes(filePath);
byte[] salt = new byte[SALT_SIZE];
byte[] iv = new byte[IV_SIZE];
Buffer.BlockCopy(fileData, 0, salt, 0, SALT_SIZE);
Buffer.BlockCopy(fileData, SALT_SIZE, iv, 0, IV_SIZE);
byte[] hash = HashPasswordWithSalt(password, salt);
byte[] encryptedData;
RemoveBytesWithSalt(fileData, hash, out encryptedData);
byte[] key = GenerateKeyFromPassword(password);
return DecryptFile(encryptedData, key, iv);
}
// 使用例
static void Main()
{
string filePath = "test.bin";
string password = "MySecretPass123";
byte[] originalData = Encoding.UTF8.GetBytes("テストデータ");
SaveFile(filePath, password, originalData);
byte[] loadedData = LoadFile(filePath, password);
Console.WriteLine("復元データ: " + Encoding.UTF8.GetString(loadedData));
}
}
(1)暗号化の実装
GenerateKeyFromPassword
パスワードをSHA-256でハッシュ化し、
AESの鍵(32バイト)を生成する。
EncryptFile
AES-CBCモードでデータを暗号化する。
IV(初期化ベクトル)はランダムに生成され、
暗号化されたデータに影響を与える。
DecryptFile
保存されたIVと鍵を使用してデータを復号化する。
(2)バイト挿入の実装
HashPasswordWithSalt
パスワードとランダムなソルトを組み合わせてSHA-256でハッシュ化し、
バイト挿入の位置や数を決定する。
InsertBytesWithSalt
ソルトとIVをファイルの先頭に保存する。
ハッシュ値から開始位置(startPos)、挿入間隔(interval)、
挿入バイト数(byteCount)を計算する。
指定された位置にランダムなバイトを挿入し、
元のバイナリとの類似性を排除する。
RemoveBytesWithSalt
挿入されたバイトをハッシュ値に基づいて除去し、
暗号化されたデータを復元する。
(3)ファイル保存と読み込み
SaveFile
ソルトとIVを生成。
データを暗号化し、バイト挿入を適用してファイルに保存する。
LoadFile
ファイルからソルトとIVを読み取り、バイトを除去し、
復号化して元のデータを返す。
特徴と利点セキュリティ
AES-CBCによる暗号化でデータが保護され、
バイト挿入により元のバイナリとの一致部分が残らない。
再現性
同じパスワードとファイルでも、
ソルトがランダムであるため毎回異なる出力が生成される。
完全性
ソルトとIVをファイルに保存することで、
復号時に必要な情報が保持される。
コメント