Perlでの暗号化 - Crypt::* モジュール群
セキュリティは現代のアプリケーション開発において最重要課題の一つです。Perlには暗号化関連の強力なCPANモジュール群が揃っており、パスワード保護からデータ暗号化まで幅広く対応できます。
パスワードハッシュ化
パスワードは絶対に平文で保存してはいけません。必ずハッシュ化して保存します。
bcrypt を使ったパスワードハッシュ化
bcryptは、計算コストを調整できる強力なパスワードハッシュアルゴリズムです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
use Crypt::Bcrypt qw(bcrypt bcrypt_check);
# パスワードのハッシュ化
my $password = 'my_secure_password';
my $hash = bcrypt($password, '2b', 12); # cost=12
print "Hash: $hash\n";
# パスワードの検証
if (bcrypt_check($password, $hash)) {
print "パスワードが一致しました\n";
} else {
print "パスワードが一致しません\n";
}
# 間違ったパスワードで検証
if (bcrypt_check('wrong_password', $hash)) {
print "一致\n";
} else {
print "不一致\n"; # これが表示される
}
|
bcryptのcostパラメータは、計算時間を決定します。値が大きいほど安全ですが、処理に時間がかかります。一般的には12〜14が推奨されます。
Argon2 を使ったパスワードハッシュ化
Argon2は、パスワードハッシュコンテストの優勝アルゴリズムで、最新の推奨方式です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
use Crypt::Argon2 qw(argon2id_pass argon2id_verify);
use Crypt::Random qw(makerandom_octet);
my $password = 'my_secure_password';
# 16バイトのランダムソルトを生成(セキュアに)
my $salt = makerandom_octet(Length => 16);
# Argon2id(推奨)でハッシュ化
my $hash = argon2id_pass(
$password,
$salt, # ランダムに生成されたソルト
3, # time cost(繰り返し回数)
'32M', # memory cost(メモリ使用量)
1, # parallelism(並列度)
16 # tag length(出力長)
);
print "Salt (hex): " . unpack('H*', $salt) . "\n";
print "Hash: $hash\n";
# 検証
if (argon2id_verify($hash, $password)) {
print "パスワードが一致しました\n";
} else {
print "パスワードが一致しません\n";
}
|
パスワードハッシュのベストプラクティス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
use Crypt::Bcrypt qw(bcrypt bcrypt_check);
# ユーザー登録時
sub register_user {
my ($username, $password) = @_;
# パスワードの強度チェック
die "パスワードは8文字以上必要です" if length($password) < 8;
die "パスワードに英数字を含めてください"
unless $password =~ /[A-Za-z]/ && $password =~ /[0-9]/;
# bcryptでハッシュ化(cost=12)
my $hash = bcrypt($password, '2b', 12);
# データベースに保存
save_to_database($username, $hash);
}
# ログイン時
sub login_user {
my ($username, $password) = @_;
# データベースからハッシュを取得
my $stored_hash = get_from_database($username);
return unless $stored_hash;
# タイミング攻撃を防ぐため、常に検証を実行
if (bcrypt_check($password, $stored_hash)) {
return { success => 1, username => $username };
}
return { success => 0 };
}
|
データの暗号化
AES による対称鍵暗号化
AESは、現在最も広く使われている対称鍵暗号アルゴリズムです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
use Crypt::Mode::CBC;
use Crypt::PRNG;
# 暗号化キー(256ビット = 32バイト)安全なランダム値を生成
# 本番環境では環境変数やKMSから取得することを推奨
my $key = Crypt::PRNG::random_bytes(32);
# 初期化ベクトル(16バイト)毎回ランダムに生成
my $iv = Crypt::PRNG::random_bytes(16);
my $cbc = Crypt::Mode::CBC->new('AES');
# 暗号化
my $plaintext = 'This is a secret message';
my $ciphertext = $cbc->encrypt($plaintext, $key, $iv);
# IVを先頭に付加して保存/送信する(IVは秘密にする必要はない)
my $output = $iv . $ciphertext;
print "Encrypted (IV+ciphertext): ", unpack('H*', $output), "\n";
# 復号化
# 受信時はIVと暗号文を分離する
my $received_iv = substr($output, 0, 16);
my $received_ciphertext = substr($output, 16);
my $decrypted = $cbc->decrypt($received_ciphertext, $key, $received_iv);
print "Decrypted: $decrypted\n";
|
セキュリティ上の重要なポイント:
- キーは絶対にコードに埋め込まず、環境変数やKMSから取得する
- IVは毎回ランダムに生成し、暗号文と一緒に保存/送信する
- より高度なセキュリティが必要な場合はAEAD(AES-GCM等)を使用
実用的な暗号化クラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package SecureStorage;
use Moo;
use Crypt::Mode::CBC;
use Crypt::Random qw(makerandom_octet);
use MIME::Base64 qw(encode_base64 decode_base64);
has key => (is => 'ro', required => 1);
sub encrypt {
my ($self, $plaintext) = @_;
# ランダムなIVを生成(16バイト)
my $iv = makerandom_octet(Length => 16);
my $cbc = Crypt::Mode::CBC->new('AES');
my $ciphertext = $cbc->encrypt($plaintext, $self->key, $iv);
# IV + 暗号文をBase64エンコードして返す
return encode_base64($iv . $ciphertext, '');
}
sub decrypt {
my ($self, $encrypted) = @_;
my $data = decode_base64($encrypted);
# 最初の16バイトがIV
my $iv = substr($data, 0, 16);
my $ciphertext = substr($data, 16);
my $cbc = Crypt::Mode::CBC->new('AES');
return $cbc->decrypt($ciphertext, $self->key, $iv);
}
# 使用例
package main;
# 256ビットキーを生成(実際には安全に保管)
my $key = Crypt::Random::makerandom_octet(Length => 32);
my $storage = SecureStorage->new(key => $key);
my $secret = "My credit card number is 1234-5678-9012-3456";
my $encrypted = $storage->encrypt($secret);
print "Encrypted: $encrypted\n";
my $decrypted = $storage->decrypt($encrypted);
print "Decrypted: $decrypted\n";
|
公開鍵暗号
RSAを使った公開鍵暗号化の例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
use Crypt::PK::RSA;
# RSA鍵ペアの生成
my $pk = Crypt::PK::RSA->new();
$pk->generate_key(2048); # 2048ビット鍵
# 公開鍵と秘密鍵をPEM形式で保存
my $public_pem = $pk->export_key_pem('public');
my $private_pem = $pk->export_key_pem('private');
# 公開鍵で暗号化
my $message = "Secret message";
my $encrypted = $pk->encrypt($message, 'oaep');
print "Encrypted: ", unpack('H*', $encrypted), "\n";
# 秘密鍵で復号化
my $decrypted = $pk->decrypt($encrypted, 'oaep');
print "Decrypted: $decrypted\n";
|
メッセージ認証コード (HMAC)
データの改ざん検出にHMACを使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
use Crypt::Mac::HMAC qw(hmac_hex);
my $secret_key = 'my_secret_key';
my $message = 'Important message';
# HMAC-SHA256でメッセージ認証コードを生成
my $mac = hmac_hex('SHA256', $secret_key, $message);
print "MAC: $mac\n";
# メッセージの検証
sub verify_message {
my ($message, $mac, $key) = @_;
my $expected_mac = hmac_hex('SHA256', $key, $message);
return $mac eq $expected_mac;
}
if (verify_message($message, $mac, $secret_key)) {
print "メッセージは正当です\n";
} else {
print "メッセージが改ざんされています\n";
}
# 改ざんされたメッセージ
my $tampered = 'Important message!';
if (verify_message($tampered, $mac, $secret_key)) {
print "正当\n";
} else {
print "改ざん検出!\n"; # これが表示される
}
|
ハッシュ関数
1
2
3
4
5
6
7
8
9
10
11
|
use Crypt::Digest::SHA256 qw(sha256_hex sha256_base64);
use Crypt::Digest::SHA3_256 qw(sha3_256_hex);
my $data = "Hello, World!";
# SHA-256
print "SHA-256: ", sha256_hex($data), "\n";
print "SHA-256 (Base64): ", sha256_base64($data), "\n";
# SHA3-256(最新のSHA-3)
print "SHA3-256: ", sha3_256_hex($data), "\n";
|
セキュリティベストプラクティス
1. 鍵の安全な管理
1
2
3
4
5
6
7
8
9
10
11
12
13
|
use Crypt::Random qw(makerandom_octet);
use Path::Tiny;
# 安全なランダムキーの生成
my $key = makerandom_octet(Length => 32);
# 環境変数から読み込む(推奨)
my $encryption_key = pack('H*', $ENV{ENCRYPTION_KEY})
or die "ENCRYPTION_KEY environment variable not set\n";
# ファイルから読み込む場合(パーミッションに注意)
# chmod 600 /path/to/keyfile
my $key_from_file = path('/path/to/keyfile')->slurp_raw;
|
2. ソルトの使用
1
2
3
4
5
6
7
8
9
|
use Crypt::Bcrypt qw(bcrypt);
use Crypt::Random qw(makerandom_octet);
# bcryptは自動的にソルトを含む
my $hash = bcrypt($password, '2b', 12);
# 手動でソルトを扱う場合
my $salt = makerandom_octet(Length => 16);
my $salted_password = $salt . $password;
|
3. タイミング攻撃の防止
1
2
3
4
5
6
7
8
|
# 文字列比較は eq ではなく、タイミングセーフな比較を使用
use Crypt::Util qw(constant_time_equal);
if (constant_time_equal($provided_mac, $expected_mac)) {
# OK
}
# bcrypt_check は内部でタイミングセーフな比較を使用
|
実用例: セキュアなAPIトークン生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
use Moo;
use Crypt::Random qw(makerandom_octet);
use Crypt::Digest::SHA256 qw(sha256_hex);
use MIME::Base64 qw(encode_base64url);
package APIToken;
sub generate {
my ($class) = @_;
# 32バイトのランダムデータを生成
my $random = makerandom_octet(Length => 32);
# Base64url エンコード(URL-safe)
my $token = encode_base64url($random, '');
return $token;
}
sub hash_token {
my ($class, $token) = @_;
# トークンのハッシュを保存(トークン自体は保存しない)
return sha256_hex($token);
}
package main;
# トークン生成
my $token = APIToken->generate();
print "Token: $token\n";
# ハッシュ化して保存
my $token_hash = APIToken->hash_token($token);
print "Token hash (store this): $token_hash\n";
# 検証時
my $provided_token = $token;
my $provided_hash = APIToken->hash_token($provided_token);
if ($provided_hash eq $token_hash) {
print "トークンが有効です\n";
}
|
まとめ
- パスワード: bcryptまたはArgon2でハッシュ化、平文保存は絶対NG
- データ暗号化: AESなどの対称鍵暗号を使用
- 鍵管理: 鍵はコードに含めず、環境変数や専用のキー管理システムを使用
- ランダム性: 暗号学的に安全な乱数生成器を使用(Crypt::Random)
- HMAC: データの完全性検証にはHMACを使用
- タイミング攻撃: 秘密情報の比較には定数時間比較を使用
暗号化は正しく使わないと逆に危険です。標準的なモジュールとベストプラクティスに従い、独自の暗号化方式は作らないようにしましょう。