Edited at

OpenSSL互換の暗号化をJava/PHPで実現する


OpenSSL cli互換の暗号化・復号処理をいろいろな言語でやってみた

とあるプロジェクトでOpenSSLのcliを使って暗号化したテキストを各種スクリプト言語で復号する必要に迫られてJavaとPHPの場合にはちょっと面倒だったので情報をまとめてみました。


OpenSSL cliの暗号化・復号処理

OpenSSLのcliで例えば以下の様にencオプションを使うと簡単にパスワードペースでのテキストの暗号化を行なえますが、これをJavaを使って復号しようとすると、OpenSSL互換のヘッダー情報の付加と解析に対応した機能がないためにそのままでは復号することができませんでした。

OpenSSLのcliでテキストファイルの内容を暗号化する場合には、以下の例の様にオプションとして暗号化の方法とパスワードを与えるだけで簡単に暗号化された文字列を得ることができます。

cat plain.txt | openssl enc -e -aes-128-cbc -base64 -k <パスワード>

本来であれば暗号化の方式にAESを指定した場合には暗号化、復号の際にパスワードに加えてSaltとIV(Initialization Vector=初期化ベクトル)を与える必要がありますが、OpenSSLではこのSaltとIVは自動的に生成されて暗号化されたデータのヘッダーへと埋め込まれるため、ユーザーはパスワード以外を意識する必要はありません。

この記事のサンプルスクリプトにある様にPerlやRubyのOpenSSLライブラリではOpenSSLのcliでの暗号化互換のSaltとIVの生成メカニズムを持っている為に比較的簡単にOpenSSLのcliで暗号化されたテキストを復号することができるのですが、JavaとPHP向けのOpenSSLライブラリの場合にはこのOpenSSLのcli互換の仕組みを持っていなかったために自前でSaltとIVの生成メカニズムを組み込む必要がありました。

ちなみに、OpenSSLのcliでencオプションを使ってAES方式で暗号化を行なった場合にはSaltにはランダムな8バイト(64ビット)のビット列が、IVには以下の方法で生成された16バイト(128ビット)のビット列が使われます。

IVの生成式

キー = パスワード + Salt のMD5

IV = キー + パスワード + Salt のMD5

これらが暗号化されたデータの先頭にヘッダー情報として付加されて暗号化データとして返されます。

文字列"Salted__"(8バイト) + Salt(8バイト) + IV(16バイト) + [暗号化されたデータ]

復号する際には暗号化されたデータの先頭が"Salted__"であることを確認したならば、それに続く8バイトのデータをSaltとして利用して、暗号化の時と同じ様にパスワード、SaltからIVを生成し、Saltに続く暗号化されたデータの本体を復号します。


Javaの場合

これを組み込んで書いたのが以下のサンプルコードです。

このコード中の getKeyAndGenerateIv でOpenSSL Cli互換の方法でSaltとIVの生成を行なっています。


EncryptDecryptText.java

import javax.crypto.Cipher;

import javax.crypto.spec.IvParameterSpec;
import java.security.spec.KeySpec;
import java.security.SecureRandom;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Arrays;
import java.io.BufferedReader;
import java.io.InputStreamReader;

class EncryptDecryptText {

public static boolean getKeyAndGenerateIv(String password, byte[] salt, byte[] key_bytes, byte[] iv_bytes) {
try {
byte[] password_bytes = password.getBytes(StandardCharsets.UTF_8);
int length = password_bytes.length + salt.length;
ByteBuffer byte_buffer = ByteBuffer.allocate(length);
byte_buffer.put(password_bytes);
byte_buffer.put(salt);
byte_buffer.rewind();
byte[] byte_array = new byte[length];
byte_buffer.get(byte_array);
System.arraycopy(MessageDigest.getInstance("MD5").digest(byte_array), 0, key_bytes, 0, key_bytes.length);
length = password_bytes.length + salt.length + key_bytes.length;
byte_buffer = ByteBuffer.allocate(length);
byte_buffer.put(key_bytes);
byte_buffer.put(password_bytes);
byte_buffer.put(salt);
byte_buffer.rewind();
byte_array = new byte[length];
byte_buffer.get(byte_array);
System.arraycopy(MessageDigest.getInstance("MD5").digest(byte_array), 0, iv_bytes, 0, iv_bytes.length);
}
catch ( NoSuchAlgorithmException e ) {
return false;
}
return true;
}

public static String encrypt(String plaintext, String password) throws Exception {
// Generate random salt.
byte[] random_bytes = new byte[8];
new SecureRandom().nextBytes(random_bytes);

byte[] key_bytes = new byte[16];
byte[] iv_bytes = new byte[16];
getKeyAndGenerateIv(password, random_bytes, key_bytes, iv_bytes);

SecretKey secret = new SecretKeySpec(key_bytes, "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv_bytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encrypted_bytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

final String header_string = "Salted__";
byte[] header_bytes = header_string.getBytes(StandardCharsets.UTF_8);
int length = header_bytes.length + random_bytes.length + encrypted_bytes.length;
ByteBuffer byte_buffer = ByteBuffer.allocate(length);
byte_buffer.put(header_bytes);
byte_buffer.put(random_bytes);
byte_buffer.put(encrypted_bytes);
byte_buffer.rewind();
byte[] byte_array = new byte[length];
byte_buffer.get(byte_array);

return new String(Base64.getEncoder().encodeToString(byte_array));
}

public static String decrypt(String payload, String password) throws Exception {
byte[] payload_bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8));
byte[] header_bytes = new byte[8];
byte[] salt_bytes = new byte[8];
int length = payload_bytes.length;
ByteBuffer byte_buffer = ByteBuffer.allocate(length);
byte_buffer.put(payload_bytes);
byte_buffer.rewind();
byte_buffer.get(header_bytes);
byte_buffer.get(salt_bytes);
length = payload_bytes.length - header_bytes.length - salt_bytes.length;
byte[] data_bytes = new byte[length];
byte_buffer.get(data_bytes);

byte[] key_byte = new byte[16];
byte[] iv_bytes = new byte[16];
getKeyAndGenerateIv(password, salt_bytes, key_byte, iv_bytes);

SecretKey secret = new SecretKeySpec(key_byte, "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv_bytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] decrypted = cipher.doFinal(data_bytes);

return new String(decrypted);
}

public static void main(String[] args) throws Exception {
// 暗号化対象データ・パスワードを読み込み
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Plain text: ");
System.out.flush();
String plain_text = br.readLine();

System.out.print("Password: ");
System.out.flush();
String password = br.readLine();

// 暗号化処理
String encrypted = EncryptorDecryptor.encrypt(plain_text, password);
System.out.print("encrypted:" + encrypted);
System.out.println();

// 復号処理
String decrypted = EncryptorDecryptor.decrypt(encrypted, password);
System.out.print("decrypted:" + decrypted);
System.out.println();
}
}



PHPの場合

<?php

function encrypt($plain_text, $password) {
// ランダムな8バイトのSaltを生成
$random_salt = openssl_random_pseudo_bytes(8);

// パスワードとSaltからKeyとIVを生成
$key_data = $password.$random_salt;
$raw_key = md5($key_data, true);

$iv_data = $raw_key.$password.$random_salt;
$iv = md5($iv_data, true);

// 暗号化
$encrypted = openssl_encrypt($plain_text, 'aes-128-cbc', $raw_key, OPENSSL_RAW_DATA, $iv);
return ( base64_encode("Salted__".$random_salt.$encrypted) );
}

function decrypt($encrypted_text, $password) {
// 復号
$payload_text = base64_decode($encrypted_text);
$header = substr($payload_text, 0, 7);
$salt = substr($payload_text, 8, 8);
$data = substr($payload_text, 16);
// パスワードとSaltからKeyとIVを生成
$key_data = $password.$salt;
$raw_key = md5($key_data, true);
$iv_data = $raw_key.$password.$salt;
$iv = md5($iv_data, true);
$decrypted_text = openssl_decrypt($data, 'aes-128-cbc', $raw_key, OPENSSL_RAW_DATA, $iv);
return ( $decrypted_text );
}

// Usage:
$str = file_get_contents('php://stdin');
print "Plain text: " . $str . "\n";
$password = $argv[1];
print "Password: " . $password . "\n";

// 暗号化処理
$encrypted = encrypt($str, $password);
print "encrypted:" . $encrypted . "\n";

// 復号処理
$decrypted = decrypt($encrypted, $password);
print "decrypted:" . $decrypted . "\n";


Perlの場合

#!/usr/bin/perl

use MIME::Base64;
use strict;
use Crypt::CBC;
use Config;

sub encrypt {
my ($plain_text, $passphrase) = @_;
my $pbe = Crypt::CBC->new(
-key => $passphrase,
-cipher => 'Crypt::Rijndael',
-keysize => 128/8,
);
my $cipher_text = $pbe->encrypt($plain_text);
my $encrypted_text = encode_base64($cipher_text);
return $encrypted_text;
}

sub decrypt {
my ($encrypted_text, $passphrase) = @_;
my $cipher_text = decode_base64($encrypted_text);
my $pbe = Crypt::CBC->new(
-key => $passphrase,
-cipher => 'Crypt::Rijndael',
-keysize => 128/8,
);
my $plain_text = $pbe->decrypt($cipher_text);
return $plain_text;
}

# Usage:
# 暗号化処理
my $str = (scalar <>);
print "Plain text: " . $str . "\n";
my $password = $ARGV[0];
print "Password: " . $password . "\n";

# 暗号化処理
my $encrypted = encrypt($str, $password);
print "encrypted:" . $encrypted . "\n";

# 復号処理
my $decrypted = decrypt($encrypted, $password);
print "decrypted:" . $decrypted . "\n";


Rubyの場合

#!/usr/bin/ruby

require "openssl"
require "base64"

def encrypt(plain_text, password)
salt = OpenSSL::Random.random_bytes(8)
cipher = OpenSSL::Cipher.new("aes-128-cbc")
cipher.encrypt()
cipher.pkcs5_keyivgen(
password,
salt,
1
)
# Rubyの場合、ヘッダー情報の文字列への付加は自分でやらないといけない
encrypted_text = "Salted__" + salt + cipher.update(plain_text) + cipher.final
return Base64.encode64(encrypted_text)
end

def decrypt(encrypted_text, password)
decoded_str = Base64.decode64(encrypted_text)
# Rubyの場合、ヘッダー情報を分解するのも自分でやらないといけない
@cipher_text = decoded_str.unpack("a8a8a*")
cipher = OpenSSL::Cipher.new("aes-128-cbc")
cipher.pkcs5_keyivgen(
password,
@cipher_text[1],
1
)
cipher.decrypt()
decrypted_text = cipher.update(@cipher_text[2]) + cipher.final
return decrypted_text
end

# Usage:
str = gets
print "Plain text: " + str + "\n";
password = ARGV[0]
print "Password: " + password + "\n";

# 暗号化処理
encrypted = encrypt(str, password);
print "encrypted:" + encrypted + "\n";

# 復号処理
decrypted = decrypt(encrypted, password);
print "decrypted:" + decrypted + "\n";


シェルスクリプト(bash)の場合

#!/bin/bash

function encrypt() {
plain_text=$1
password=$2
encrypted_text=`echo -n "$plain_text" | openssl enc -e -aes-128-cbc -base64 -k "$password"`
echo $encrypted_text
}

function decrypt() {
encrypted_text=$1
password=$2
plain_text=`echo "$encrypted_text" | openssl enc -d -aes-128-cbc -base64 -k "$password"`
echo $plain_text
}

# Useage:
str="$(cat -)"
echo "Plain text: $str"
password=$1
echo "Password: $password"

# 暗号化処理
encrypted=`encrypt "$str" "$password"`
echo "encrypted:$encrypted"

# 復号処理
decrypted=`decrypt "$encrypted" "$password"`
echo "decrypted:$decrypted"