Предисловие.
Всем привет. Было бы классно если бы у блогопостов отображалась дата написания.
То вы бы смогли обнаружить, что от текущего поста до предыдущего, несколько лет).
Ну ничего нет же страшного, как говорится "лучше поздно чем никогда",
да и сайт надо пополнять контентом, а то что-то на нем тишина "стоит".
Начало.
Был холодный 2009 javaMe гуляла по миру и только у некоторых групп людей на слуху были слова android, iphone, wp7.
А паблик народ знал в мире мобильного develop такие понятия как:
- java мидлет - мировая виртуальная машина - просто время шло и она дала путь новым платформам, но сама тянуть все аппаратные тяжбы не могла в одиночку с девайсом,
и далее она стала просто как надстройка для мобильных ОС.
- симбиан - просто потом с годами купила msf и подмяла под себя, а в той ОС много что было так хорошо продуманно.
- windows mobile - у данной фирмы много денег поэтому всякую дребедень можно выпускать для галочки, что типа мы там есть на рынке мобильных девайсов.
- blackberry - хорошая самобытная ос система, почему потом её разваливать начали непонятно,
наверно суженный круг пользования был ИЛИ мало разработчиков ИЛИ просто хватались за все что лежало рядом,
а разработчиков то в той команде мало было ну ИЛИ просто не поспели за прогрессом в виду последнего упомянутого факта.
Ну ладно не будем как девочки сентименталить и продолжим повествование. Тогда я работал в фирме, связанной с финансовым процессингом.
Первая серьезная задача, после окончания вуза и уже 1 года работы, именно здесь была мне поставлена.
Ну как сказать поставлена, просто поставили перед фактом) и надо это сделать.
Смысл был в следующем:
- Уже существовал механизм обмена по шифрованному каналу между сервером и клиентом.
- Клиентом - были платежные терминалы на версиях windows.
- Сервер - был написан на c++.
- Обмен - по протоколу ssl и введена ЭЦП по формату MSDN.
- И никто не хотел какому-то новенькому программисту писать никаких новых механизмов по обмену с мобильным терминалом, тк это же не актуально было тогда!))
Дано:
Работник, который:
- 0,5 года знает мобильную java.
- Получил специальность "Информационные системы в управлении", хотя раньше он думал,
что получил специальность "Инженер-системотехник", спасибо одному человеку), что поправил меня)).
- Знания о шифровании были получены только при создании диплома, который он сам лично делал и который реально работал в организации.
Исходя из своего маленького опыта быстро только решилась проблема с протоколом ssl.
Надо было просто на сервер поставить сертификат SSL от организации:
Thawte или
VeriSign, а на мобильном - при разработке использовать интерфейс
HttpsConnection:
HttpsConnection hConnect = (HttpsConnection) Connector.open("https://WWW.DOMAIN.ORG:PORT_SSL/", Connector.READ_WRITE, true);
Зачем только ограниченный круг фирм-создателей сертификатов можно было ставить на сервер?
Кто раньше писал под javaMe помнят, что при использовании ssl протокола на некоторых телефонах спрашивалось мол соединение недоверенное продолжить да/нет, а на некоторых - просто соединение закрывалось.
Проблема решалась 2 путями:
- Сертификацией ssl сервера от организации Thawte или VeriSign, тк в мобильных устройства часто (практически 100%) корневыми сертификатами безопасности являлись именно сертификаты от этих контор.
- Портирование в качестве корневого сертификата в устройство, такого же какое стоит на сервере. Но это как вы понимаете не на широкую публику.
Проблема с шифрованием канала передачи была решана быстро.
Далее проверка аутентичности сторон при обмене или проще говоря ЭЦП. В детальном рассмотрении это должно было так выглядеть.
- Мобильный терминал генерирует пару RSA ключей.
- Далее терминал инициирует обмен публичными ключами с сервером. Подготавливает свой публичный ключ для сервера (чтобы сервер смог распознать).
- Сервер на запрос шлет свой публичный ключ. Тут мы его парсим и приводим в удобно читаемый вид в нашем яве коде.
- Мобильный терминал сохраняет серверный ключ.
- При последующих обменах с сервером. Идет подпись сообщения приватным ключом клиента. При ответе от сервера идет верификация сообщения публичным ключом сервера.
Первый пункт быстро решился, благодаря библиотеке, о которой идет повествование, если вы забыли) это
Bouncy Castle.
/********......*********/
private RSAPrivateCrtKeyParameters RSAprivKey = null;
private RSAKeyParameters RSApubKey = null;
private AsymmetricCipherKeyPair keyPair = null;
public void generateRSAKeyPair() throws Exception {
//генерация ключей для ЭЦП
SecureRandom sr = new SecureRandom();
BigInteger pubExp = new BigInteger("10001", 16);
RSAKeyPairGenerator RSAKeyPairGen = new RSAKeyPairGenerator();
RSAKeyGenerationParameters RSAKeyGenPara = new RSAKeyGenerationParameters(pubExp, sr, 1024, 80);//80
RSAKeyPairGen.init(RSAKeyGenPara);
keyPair = RSAKeyPairGen.generateKeyPair();
RSAprivKey = (RSAPrivateCrtKeyParameters) keyPair.getPrivate();
RSApubKey = (RSAKeyParameters) keyPair.getPublic();
if (RSApubKey.getModulus().bitLength() < 1018) {
System.out.println("RSA: failed key generation (1018) length test");
throw new Exception("RSA: failed key generation (1018) length test");
}
}
/********......*********/
Любопытный факт, что тогда были такие слабенькие телефоны. К примеру на Sony Ericsson w660, процесс генерации пары RSA ключей занимал ~105 секунд. То есть стандартно эту операцию надо было помещать в поток...
Полученный приватный ключ надо сохранить, чтобы потом его использовать при следущих обменах. Но чтобы его полноценно потом восстановить из сохранения, необходимо сохранить несколько параметров:
...RSAprivKey.getModulus().toByteArray() - модуль
...RSAprivKey.getDP().toByteArray() - параметр DP RSA алгоритма
...RSAprivKey.getP().toByteArray() - случайное простое число p
...RSAprivKey.getQ().toByteArray() - случайное простое число q
...RSAprivKey.getExponent().toByteArray() - экспонента
...RSAprivKey.getDQ().toByteArray() - параметр DQ RSA алгоритма
...RSAprivKey.getPublicExponent().toByteArray() - открытая экспонента
...RSAprivKey.getQInv().toByteArray() - параметр InverseQ RSA алгоритма
то есть далее при восстановлении приватного ключа используется конструктор:
RSAprivKey = new RSAPrivateCrtKeyParameters (RSAmod, RSApubExp, RSAprivExp, RSAp, RSAq, RSAdp, RSAdq, RSAqInv);
На
втором пункте сетевая составляющая как думаю всем понятна.
Нас больше интересует конвертирование НАШЕГО публичного ключа в формат Public Key BLOBs, чтобы сервер понял нас и начал с "нами работать".
Формат Public Key BLOBs:
Название поля | Размер | Описание |
Тип | 1 байт | PUBLICKEYBLOB (0x6) |
Версия | 1 байт | CUR_BLOB_VERSION (0x2) |
Зарезервировано | 2 байта | PUBLICKEYBLOB (0x0000) |
Идентификатор алгоритма | 4 байта | CALG_RSA_SIGN (0x00002400) |
Магическое слово | 4 байта | "RSA1" (0x31415352) |
Длина публичной экспоненты | 4 байта | (0x00000400) |
Публичная экспонента | 4 байта | RAW bytes public exponents |
Modulus | RSAPUBKEY.bitlen/8 | Сам открытый ключ |
Не буду вдаваться в подробности названия, тк в приведенной таблице каждая группа байт полей еще называется своим именем.
Перед подготовкой конвертирования не забываем, что byte-order java (big-endian) и си (little-endian) разный.
public static byte[] GetPublicRSAKeyForCryptoAPI() throws IOException, ArrayIndexOutOfBoundsException, Exception {
if (PubKeyClient == null) { throw new IOException("Ключи ЭЦП не найдены!"); } // локальная переменная класса, отвечающая за публичный ключ устройства.
byte PUBLICKEYBLOB = 0x06;
byte CUR_BLOB_VERSION = 0x02;
short RESERVED = 0x0000;
//int CALG_RSA_SIGN = 0x00002400;
//String MAGIC = "RSA1"; // 0x31415352
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream daos = new DataOutputStream(baos);
try {
//LITTLE ENDIAN!!!!!!!!!!!!!
daos.writeByte(PUBLICKEYBLOB);
daos.writeByte(CUR_BLOB_VERSION);
daos.writeShort(RESERVED);
//CALG_RSA_SIGN
daos.writeByte(0x00);
daos.writeByte(0x24);
daos.writeByte(0x00);
daos.writeByte(0x00);
//RSA1
daos.writeByte(0x52);
daos.writeByte(0x53);
daos.writeByte(0x41);
daos.writeByte(0x31);
//Len bit public exponent
daos.writeByte(0x00);
daos.writeByte(0x04);//04
daos.writeByte(0x00);
daos.writeByte(0x00);
//Берем наш открытый ключ
RSAKeyParameters kea = (RSAKeyParameters) PubKeyClient;
//не забываем про байт ордер
byte[] bbb = BigIntegers.asUnsignedByteArray(kea.getExponent());
daos.write(1 > bbb.length ? 0x00 : bbb[0]);
daos.write(2 > bbb.length ? 0x00 : bbb[1]);
daos.write(3 > bbb.length ? 0x00 : bbb[2]);
daos.write(4 > bbb.length ? 0x00 : bbb[3]);
//Формируем беззнаковый массив байтов
byte[] binp = BigIntegers.asUnsignedByteArray(kea.getModulus());
//Содержимое открытого ключа переворачиваем
daos.write(XDSF.ToReserveEndianByteArray(binp));
byte rez[] = baos.toByteArray();
return rez;
} catch (IOException ioe) {
throw new IOException("Error write binary data");
} catch (ArrayIndexOutOfBoundsException ai) {
throw new ArrayIndexOutOfBoundsException("Index out range");
} catch (Exception e) {
throw new Exception("Error sys...");
} finally {
daos.close();
baos.close();
}
}
Далее уже на
третьем пункте нам проще.
Тк надо просто напросто было сделать обратную операцию с ключом, который мы получаем от сервера.
private void ReadRSAPublicKeyFromCryptoAPI(byte[] CryptoApiRSA) throws Exception {
byte PUBLICKEYBLOB = 0x06;
byte CUR_BLOB_VERSION = 0x02;
short RESERVED = 0x0000;
int CALG_RSA_KEYX = 0x0000a400;
int CALG_RSA_SIGN = 0x00002400;
String MAGIC = "RSA1"; // 0x31415352
DataInputStream dis = null;
int jint = 0; // int to build Java int from little−endian ordered byte data
int bitlen = 0;
int pubexp = 0;
ByteArrayInputStream bis = new ByteArrayInputStream(CryptoApiRSA);
dis = new DataInputStream(bis);
try {
if (dis.readByte() != PUBLICKEYBLOB || dis.readByte() !=
CUR_BLOB_VERSION || dis.readShort() != RESERVED) {
throw new Exception("Server key is not public structure!");
}
jint = 0;
for (int i = 0; i < 4; i++) {
jint += dis.readUnsignedByte() * (int) pow(256, i);
}
if (jint != CALG_RSA_KEYX && jint != CALG_RSA_SIGN) {
throw new Exception("Format key is not for sign or swaping data!");
}
//−−−−−− Read the RSAPUBKEY struct members −−−−−−−−−//
StringBuffer magic = new StringBuffer(4);
for (int i = 1; i <= 4; i++) {
magic.append((char) dis.readByte());
}
if (!magic.toString().equals(MAGIC)) {
throw new Exception("Format key is not RSA1!");
}
for (int i = 0; i < 4; i++) {
bitlen += dis.readUnsignedByte() * (int) pow(256, i);
}
for (int i = 0; i < 4; i++) {
pubexp += dis.readUnsignedByte() * (int) pow(256, i);
}
int keysize = bitlen;
//−−−−− Finally, get the modulus data, and reverse bytes to get big−endian value −−−−−−−−//
byte[] modulus = new byte[bitlen / 8]; //should be this many bytes left
int modbytes = dis.read(modulus);
if (modbytes != (bitlen / 8)) {
throw new Exception("CRC public server key is not == :)) !");
}
modulus = XDSF.ToReserveEndianByteArray(modulus); //reverse bytes to put in big−endian order
PubKeyServ = new RSAKeyParameters(false, new BigInteger(1, modulus), BigInteger.valueOf(pubexp));//заполняем нашу локальную переменную класс, отвечающую за публичный ключ сервера.
} catch (IOException o) {
throw new Exception("Error perform getServerKey: " + o.getMessage());
}
}
Далее приведу код вспомогательных функций, которые использовались в прямом и обратном преобразованиях.
Возведение в степень
private int pow(int x, int y) {
int r = 1;
for (int i = 0; i < y; i++) {
r *= x;
}
return r;
}
Изменение порядка байтов
public static byte[] ToReserveEndianByteArray(byte[] mas) {
byte m[] = new byte[mas.length];
for (int i = 0; i < mas.length; i++) {
m[i] = mas[mas.length - i - 1];
}
return m;
}
Остальные функции Вы можете найти в самой рассматриваемой библиотеке Bouncy Castle.
Ну и на конец
четвертый шаг. Это подпись и верификация сообщений.
Подпись сообщения приватным ключом устройства
public static byte[] SignRSA(byte[] DataSign) throws Exception {
if (PrivKeyClient == null) {//локальная переменная "Закрытый ключ устройства"
throw new Exception("Ключи ЭЦП не найдены!");
}
SHA1Digest dig = new SHA1Digest();
RSADigestSigner signer = new RSADigestSigner(dig);
CipherParameters cipa = (CipherParameters) PrivKeyClient;
signer.init(true, cipa);
signer.update(DataSign, 0, DataSign.length);
byte[] sign = signer.generateSignature();
BigInteger bi_sign = new BigInteger(XDSF.ToReserveEndianByteArray(sign));
signer.reset();
return BigIntegers.asUnsignedByteArray(bi_sign);
}
Верификация принятого сообщения серверным публичным ключом
public static boolean RSAVerifyServ(byte[] mesg, byte[] sig) throws Exception {
if (PubKeyServ == null) {//локальная переменная "Публичный ключ сервера"
throw new Exception("Ключи ЭЦП не найдены!");
}
SHA1Digest dig = new SHA1Digest();
RSADigestSigner signer = new RSADigestSigner(dig);
CipherParameters cipa = (CipherParameters) PubKeyServ;
signer.init(false, cipa);
signer.update(mesg, 0, mesg.length);
BigInteger bi_sign = new BigInteger(XDSF.ToReserveEndianByteArray(sig));
return signer.verifySignature(BigIntegers.asUnsignedByteArray(bi_sign));
}
Заключение.
С высоты прожитых лет с того момента, сейчас кажется, что это ерунда и пара пустяков.
Но тогда на эту разработку ушло 19 дней! и приходилось для себя "разжевывать" каждый закоулок кода.
А смысл последних двух предложений в том, что помогайте и поощаряйте, тех кто
реально тянется к знаниям и совершествованию,
а для кого очередной копипаст из интернета это всего лишь следующий "распил" - просто "по шапке, по шапке" их ))