Featured image of post Perlでの暗号化 — Crypt::* モジュール群

Perlでの暗号化 — Crypt::* モジュール群

Perlの暗号化関連CPANモジュールの使い方とベストプラクティスを解説。

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を使用
  • タイミング攻撃: 秘密情報の比較には定数時間比較を使用

暗号化は正しく使わないと逆に危険です。標準的なモジュールとベストプラクティスに従い、独自の暗号化方式は作らないようにしましょう。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。