TripleDES в Perl/PHP/ColdFusion
В последнее время возникла проблема с подключением API с процессором платежей, который запрашивал строку, которая должна быть зашифрована для использования в качестве токена, используя стандарт TripleDES. Наши приложения запускаются с использованием ColdFusion, который имеет тег Encrypt, который поддерживает TripleDES, однако результат, который мы возвращали, не был тем, что ожидал процессор платежей.
Прежде всего, вот результат, который ожидал процессор платежей.
AYOF+kRtg239Mnyc8QIarw==
И ниже приведен фрагмент ColdFusion, который мы использовали, и результирующая строка.
<!--- Coldfusion Crypt (here be monsters) --->
<cfset theKey="123412341234123412341234">
<cfset theString = "username=test123">
<cfset strEncodedEnc = Encrypt(theString, theKey, "DESEDE", "Base64")>
<!---
resulting string(strEncodedEnc): tc/Jb7E9w+HpU2Yvn5dA7ILGmyNTQM0h
--->
Как вы можете видеть, это не возвращало строку, на которую мы надеялись. В поисках решения мы бросили ColdFusion для этого процесса и попытались воспроизвести токен в PHP.
Теперь я знаю, что различные языки используют шифрование по-разному - например, в прошлом, управляя шифрованием между приложением С# и внутренним интерфейсом PHP, мне приходилось играть с дополнением, чтобы получить эти два но мой опыт в том, что PHP обычно ведет себя, когда дело доходит до стандартов шифрования.
В любом случае, на источник PHP, который мы попытались, и результирующую строку.
/* PHP Circus (here be Elephants) */
$theKey="123412341234123412341234";
$theString="username=test123";
$strEncodedEnc=base64_encode(mcrypt_ecb (MCRYPT_3DES, $theKey, $theString, MCRYPT_ENCRYPT));
/*
resulting string(strEncodedEnc): sfiSu4mVggia8Ysw98x0uw==
*/
Как вы можете ясно видеть, у нас есть еще одна строка, которая отличается от строки, ожидаемой процессором оплаты, и той, которая была создана ColdFusion. Методы интеграции с использованием технологии "против часовой стрелки".
После многих контактов с платежным процессором (много и много повторений) мы не можем помочь с проблемами с кодированием, вы должны делать это неправильно, прочитайте руководство "), мы, наконец, перешли к кому-то с более чем двумя мозговыми ячейками, чтобы тереться вместе, кто смог отступить и на самом деле посмотреть и диагностировать проблему.
Он согласился, наши попытки CF и PHP не привели к правильной строке. После быстрого поиска он также согласился с тем, что это не обязательно наш источник, а скорее то, как эти два языка реализовали свое видение стандарта TripleDES.
Сегодня утром в офис мы встретили электронное письмо с фрагментом исходного кода в Perl. Это был код, который они использовали непосредственно на своем конце для получения ожидаемого токена.
#!/usr/bin/perl
# Perl Crypt Calamity (here be...something)
use strict;
use CGI;
use MIME::Base64;
use Crypt::TripleDES;
my $cgi = CGI->new();
my $param = $cgi->Vars();
$param->{key} = "123412341234123412341234";
$param->{string} = "username=test123";
my $des = Crypt::TripleDES->new();
my $enc = $des->encrypt3($param->{string}, $param->{key});
$enc = encode_base64($enc);
$enc =~ s/\n//gs;
# resulting string (enc): AYOF+kRtg239Mnyc8QIarw==
Итак, у нас это есть. Три языка, три реализации того, что они цитируют в документации, как стандартное шифрование TripleDES, и три совершенно разные результирующие строки.
Мой вопрос состоит в том, что, исходя из вашего опыта этих трех языков и их реализации алгоритма TripleDES, вы могли заставить всех двух из них дать один и тот же ответ, и если да, то какие настройки кода вам нужно сделать, чтобы прийти к результату?
Я понимаю, что это очень сложный вопрос, но я хотел дать четкую и точную настройку для каждого этапа тестирования, который мы должны были выполнить.
Я также буду выполнять еще несколько исследовательских работ по этому вопросу позже и опубликую любые выводы, которые я придумал для этого вопроса, чтобы другие могли избежать этой головной боли.
Ответы
Ответ 1
Нельзя использовать Perl TripleDES. Он делает так много странных вещей, и вам будет весело.
Ваша первая проблема заключается в том, что ключи в Perl являются шестнадцатеричными, и вам нужно преобразовать их в двоичные. Попробуйте это в PHP,
$theKey="123412341234123412341234";
$key = pack('H*', str_pad($theKey, 16*3, '0'));
$strEncodedEnc=base64_encode(mcrypt_ecb (MCRYPT_3DES, $key, $theString, MCRYPT_ENCRYPT));
echo $strEncodedEnc, "\n";
В результате получается
AYOF+kRtg239Mnyc8QIarw==
Затем вам придется пометить его странным образом. Я забыл детали. Вам повезло с этим образцом (это 16 символов).
Ответ 2
Ответ Coldfusion:
Первая проблема заключается в том, что длина ключа неверна для Triple DES. ZZ Coder правильно вывел, что он должен быть дополнен до нужной длины с 0.
Следующий шаг заключается в том, что ключ должен быть преобразован в шестнадцатеричный. Для этого в CF мы имеем:
<cfset theKey="123412341234123412341234000000000000000000000000">
<cfset encodedKey = ToBase64(BinaryDecode(theKey, "HEX"))>
Последний шаг заключается в том, что результат не дополняется, поэтому нам нужно указать это в алгоритме шифрования в CF:
<cfset strEncodedEnc = Encrypt(theString, encodedKey, "DESEDE/ECB/NoPadding", "Base64")>
Полученный полный код:
<cfset theKey="123412341234123412341234000000000000000000000000">
<cfset encodedKey = ToBase64(BinaryDecode(theKey, "HEX"))>
<cfset theString = "username=test123">
<cfset strEncodedEnc = Encrypt(theString, encodedKey, "DESEDE/ECB/NoPadding", "Base64")>
<cfdump var="#strEncodedEnc#"><br>
приводит к:
AYOF+kRtg239Mnyc8QIarw==
Ответ 3
Я включу код ниже для тех, кто, возможно, работает над обновлением CCBill (что звучит как компания, упомянутая в исходном сообщении). Ниже перечислены функции PHP, соответствующие выходному значению внутреннего шифрования CCBill 3DES/TripleDES, как описано в документации:
http://www.ccbill.com/cs/manuals/CCBill_Subscription_Upgrade_Users_Guide.pdf
//Encrypt String using 3DES Key
function encrypt($str,$key){
$hex_key = hexmod($key);
$bin_hex_key = pack('H*', str_pad($hex_key, 16*3, '0'));
//Pad string length to exact multiple of 8
$str = $str. str_repeat(' ',8-(strlen($str)%8) );
$out = base64_encode( mcrypt_ecb(MCRYPT_3DES, $bin_hex_key, $str, MCRYPT_ENCRYPT) );
//print_r('Key/Hex/Str: '.$key.' -> '.$hex_key.' -> '.$str.' -> '.$out,1);
return $out;
}
//Hex Modulus: Converts G-Z/g-z to 0-f (See @Jinyo Post)
//Necessary to match CCBill Encryption
function hexmod($str){
//Convert G-Z & g-z to 0-f
$ascii_in = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$ascii_out = '0123456789ABCDEF0123456789ABCDEF0123abcdef0123456789abcdef0123';
$hex_out = str_replace(str_split($ascii_in),str_split($ascii_out),$str);
return $hex_out;
}
$triple_des_key = 'ABCDEFGHIJKLMNOPQRSTUVWX'; // <!-- 24char 3DES Key
$username_string = 'username=<username here>'; // Encrypt this string
$encrypted_username = encrypt($username_string,$triple_des_key); // <-- Output
Ответ 4
О, это весело!
> hex clear_text
0000 75 73 65 72 6e 61 6d 65 3d 74 65 73 74 31 32 33 username =test123
> openssl des3 -in clear_text -out crypt_text
enter des-ede3-cbc encryption password: 123412341234123412341234
Verifying - enter des-ede3-cbc encryption password: 123412341234123412341234
> hex crypt_text
0000 53 61 6c 74 65 64 5f 5f d7 1b 37 a6 e0 c4 99 d1 Salted__ ..7.....
0010 ce 39 7f 87 5e 8b e8 8a 27 ca 39 41 58 01 38 16 .9..^... '.9AX.8.
0020 a5 2b c8 14 ed da b7 d5 .+......
> base64 crypt_text
U2FsdGVkX1/XGzem4MSZ0c45f4dei+iKJ8o5QVgBOBalK8gU7dq31Q==
> openssl version
OpenSSL 0.9.8k 25 Mar 2009
> base64 --version | head -n 1
base64 (GNU coreutils) 7.1
Вам следует поговорить с криптовым экспертом, попробуйте, возможно, списки рассылки openssl-users или dev-tech-crypto @mozilla, если здесь не появится полезная информация.
Ответ 5
ZZ Coder был почти там. Еще несколько предостережений о том, почему коды Perl и PHP возвращают разные шифровки.
Во-первых, всякий раз, когда есть недопустимые шестнадцатеричные буквы (буквы после F), замените их в соответствии со следующим правилом:
- G- > 0
- H- > 1
- I- > 2
- J- > 3
- ...
- P- > 9
- Q- > А
- R- > B
- ...
- V- > F
- W- > 0
- ...
- Z- > 3
Используя этот метод, ключом для AZ98AZ98AZ98AZ98AZ98AZ98 является A398A398A398A398A398A398000000000000000000000000 (после заполнения нулями).
Во-вторых, текст, который должен быть зашифрован, должен быть дополнен пробелами, чтобы количество символов делилось на 8. В этом примере имя пользователя = test123 делится на 8, поэтому его не нужно заполнять. Но, если это было username = test12, то в конце ему нужно одно пробелы.
Следующий PHP-код возвращает шифрование, соответствующее perl-шифрованию
$theKey="A398A398A398A398A398A398000000000000000000000000";
$key = pack("H*", $theKey);
$input = "username=test123";
$strEncodedEnc=mcrypt_ecb (MCRYPT_3DES, $key, $input, MCRYPT_ENCRYPT);
$strEncodedEnc64=base64_encode($strEncodedEnc);
echo $strEncodedEnc . "<br />";
echo $strEncodedEnc64 . "<br />";
Ответ 6
Принял меня большую часть вечера, но вот как выглядит решение @Eric Kigathi в рубине
def encoding(key, val)
require "openssl"
des = OpenSSL::Cipher::Cipher.new('des-ede3')
des.encrypt
des.key = convert_key_to_hex_bin key
#ENCRYPTION
des.padding = 0 #Tell Openssl not to pad
val += " " until val.bytesize % 8 == 0 #Pad with zeros
edata = des.update(val) + des.final
b64data = Base64.encode64(edata).gsub(/\n/,'')
end
def convert_key_to_hex_bin(str)
decoder_ring = Hash['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/'.split(//).zip('0123456789ABCDEF0123456789ABCDEF0123ABCDEF0123456789ABCDEF012345'.split(//))]
str.gsub!(/./, decoder_ring)
[str.ljust(16*3, '0')].pack("H*")
end
Будьте осторожны. Я не совсем уверен, что делать + и/конвертировать в конец. Я догадался в 4 и 5, но я не могу сказать, правда ли это.
Совет о шляпе http://opensourcetester.co.uk/2012/11/29/zeros-padding-3des-ruby-openssl/ код шифрования и комментарий.
Ответ 7
В ColdFusion Answer отсутствует модификация ключа ccbill для работы (например, в Eric Answer)...
Я изменил Eric Ответ на Lucee Code. Не нужно много работать, чтобы вернуть его в ACF-совместимый код (изменив структуру в ReplaceNoCase на отдельные).
public function ccbillupgrade(string key = "XXXXXXXXXXXXXXXXXXXXXXXX", string username){
var remote_user = padUserName("username=#arguments.username#");
var padded_key =
Ucase(
Replace(
LJustify(
hexmod(arguments.key)
, 48), // Pad key to 48 bytes (hex)
" ", '0', 'all'
)
);
var encodedKey = ToBase64(BinaryDecode(padded_key, "HEX"));
return Encrypt(remote_user, encodedKey, "DESEDE/ECB/NoPadding", "Base64");
}
private string function hexmod(string input) {
return ReplaceNoCase( arguments.input,
{
'G' = '0', 'H' = '1',
'I' = '2', 'J' = '3',
'K' = '4', 'L' = '5',
'M' = '6', 'N' = '7',
'O' = '8', 'P' = '9',
'Q' = 'A', 'R' = 'B',
'S' = 'C', 'T' = 'D',
'U' = 'E', 'V' = 'F',
'W' = '0', 'X' = '1',
'Y' = '2', 'Z' = '3'
}
);
}
private string function padUserName(string username) {
var neededLength = Len(arguments.username) + ( 8 - Len(username) % 8 );
return LJustify(arguments.username, neededLength);
}
Ответ 8
Есть две проблемы (или нет) с Crypt:: TripleDES:
-
Тот факт, что ключи для Crypt:: TripleDES являются HEX (объясняется ранее ZZ Coder). Вы можете использовать свой ключ, используя распаковку или используя ord/sprintf или кучу других методов:
-
$pass = unpack ( "H *", "YOUR PASSPHRASE" ); # pack/unpack version
-
$pass = join ('', map {sprintf ( "% x", $)} map {ord ($)} split (//, "YOUR PASS" ));
Crypt:: TripleDES накладывает простую фразу с пробелами (это было нормально для меня)
-
Crypt:: TripleDES делает пробелы только для обычного текста. Существует множество методов заполнения, которые используются на Java или PHP mcrypt_encrypt:
- (т.е. PKCS5, PKCS7, CMS) - панель с байтами того же значения, указывающая количество заполненных байтов, например: "andrei" → hex: 61 6e 64 72 65 69 → padded: 61 6e 64 72 65 69 02 02
- pad с нулевыми символами, например: 61 6e 64 72 65 69 00 00
- pad с пробелами (Crypt:: TripleDES уже делает это)
- pad с нулями (нулевые символы), за исключением последнего байта, который будет числом заполненных байтов, например: 61 6e 64 72 65 69 00 02
- pad с 0x80, за которым следуют нулевые символы, например: 61 6e 64 72 65 69 80 00
Обратите внимание на свой шифротекст, если он соответствует до некоторой точки, но окончание отличается, тогда у вас есть проблема с заполнением текстового текста. В противном случае у вас может возникнуть проблема с парольной фразой, проблема режима блока шифрования (EBC, CBC,..) http://www.tools4noobs.com/online_tools/encrypt/help_modes.php или проблема с алгоритмом.
Итак, что я сделал в Perl, чтобы иметь возможность сопоставить шифрованный текст с Java (который использовал заполнение нулевых символов):
my $pass = unpack("H*", "MY PASS");
my $text = "bla bla bla";
my $pad = 8 - (length $text % 8);
$pad = 0 if ( $pad > 7 );
$text .= chr(00) x $pad;
my $des = new Crypt::TripleDES;
my $cipher = $des->encrypt3( $text, $pass );
Надеюсь, что это поможет