RSA: шифрование в iOS, расшифровка в Java
у меня есть открытый ключ, который отправляется с сервера Java. Кодированные строки base64 совпадают, прежде чем я декодирую и удаляю ASN.1 заголовки. Я храню открытый ключ в брелке с SecItemAdd
.
так я пытаюсь зашифровать данные с помощью открытого ключа, а расшифровать его с помощью закрытого ключа в Java. Я использую SecKeyEncrypt
на стороне iOS и Cipher
на стороне Java.
то, что я шифрую, - это симметричный ключ AES, который шифрует мои фактические данные, поэтому длина ключа это 16 байт. Когда просто base64 кодирует ключ, все работает, поэтому я знаю, что что-то не так с этим шифрованием RSA.
вот пример моего вызова iOS:
OSStatus sanityCheck = SecKeyEncrypt(publicKey,
kSecPaddingPKCS1,
(const uint8_t *) [incomingData bytes],
keyBufferSize,
cipherBuffer,
&cipherBufferSize
);
вот пример моего вызова Java:
public static byte[] decryptMessage (byte[] message, PrivateKey privateKey, String algorithm) {
if (message == null || privateKey == null) {
return null;
}
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, privateKey, algorithm, false);
if (cipher == null) {
return null;
}
try {
return cipher.doFinal(message);
}
catch (IllegalBlockSizeException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
catch (BadPaddingException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
}
private static Cipher createCipher (int mode, Key encryptionKey, String algorithm, boolean useBouncyCastle) {
Cipher cipher;
try {
if (useBouncyCastle) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
cipher = Cipher.getInstance(algorithm, "BC");
}
else {
cipher = Cipher.getInstance(algorithm);
}
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
catch (NoSuchPaddingException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
catch (NoSuchProviderException e) {
e.printStackTrace();
return null;
}
try {
cipher.init(mode, encryptionKey);
}
catch (InvalidKeyException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
return cipher;
}
я пробовал так много комбинаций, и ничего не работает.
- iOS: PKCS1, Java: RSA/ECB/PKCS1Padding
- iOS: PKCS1, Java: RSA
- iOS: PKCS1, Java: RSA / None/PKCS1Padding (броски
org.bouncycastle.crypto.DataLengthException: input too large for RSA cipher.
) - iOS: OAEP, Java: RSA/ECB/OAEPWithSHA-1AndMGF1Padding
- iOS: OAEP, Java: RSA/ECB/OAEPWithSHA-256AndMGF1Padding
я также попытался использовать внутренний поставщик Java, а также поставщик BouncyCastle. The javax.crypto.BadPaddingException
получает брошен каждый раз, но сообщение отличается для каждой комбинации. Какое-то шоу Data must start with zero
, в то время как другие Message is larger than modulus
.
на iOS: PKCS1, Java: RSA
не вызывает исключения, но полученный расшифрованный byte[]
массив должен быть длиной 16, но это длина 256, что означает, что заполнение не правильно удалено.
может кто-нибудь помочь?
***редактировать***
как я делаю больше тестирования, я наткнулся на эту страницу (http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs1/RSACipher.html), что по существу говорит мне, что RSA == RSA/None/PKCS1Padding
. Расшифровка работает в том смысле, что исключений нет, но я все еще получаю расшифрованный ключ, байт[] которого имеет длину 256 вместо длины 16.
еще один интересный момент. Кажется, что если сервер Java имеет открытый ключ, сгенерированный с устройства iOS и зашифрованный с помощью Cipher.getInstance("RSA")
, телефон может правильно декодировать сообщение с помощью RSA / PKCS1.
*** правка 2***
я просмотрел эти учебники и снова просмотрел свой код на iOS сторона:
- http://blog.flirble.org/2011/01/05/rsa-public-key-openssl-ios/
- http://blog.wingsofhermes.org/?p=42
- http://blog.wingsofhermes.org/?p=75
насколько я могу судить, мой код делает все правильно. Одно существенное различие заключалось в том, как я сохранял ключ, поэтому я попытался сохранить его другим способом:
OSStatus error = noErr;
CFTypeRef persistPeer = NULL;
NSMutableDictionary * keyAttr = [[NSMutableDictionary alloc] init];
keyAttr[(__bridge id) kSecClass] = (__bridge id) kSecClassKey;
keyAttr[(__bridge id) kSecAttrKeyType] = (__bridge id) kSecAttrKeyTypeRSA;
keyAttr[(__bridge id) kSecAttrApplicationTag] = [secKeyWrapper getKeyTag:serverPublicKeyTag];
keyAttr[(__bridge id) kSecValueData] = strippedServerPublicKey;
keyAttr[(__bridge id) kSecReturnPersistentRef] = @YES;
error = SecItemAdd((__bridge CFDictionaryRef) keyAttr, (CFTypeRef *)&persistPeer);
if (persistPeer == nil || ( error != noErr && error != errSecDuplicateItem)) {
NSLog(@"Problem adding public key to keychain");
return;
}
CFRelease(persistPeer);
это спасение было успешно, но конечный результат был таким же: расшифрованный ключ AES по-прежнему был длиной 256 байт вместо 16 байт.
2 ответов
у меня была такая же проблема. Работает с kSecPaddingNone
, а не работы с kSecPaddingPKCS1
С PKCS1
комбинация в коде Java.
но, это не хорошая идея, чтобы использовать его без подклада.
Итак, на iOS замените kSecPaddingNone
с kSecPaddingOAEP
и использовать RSA/NONE/OAEPWithSHA1AndMGF1Padding
в коде Java. Это работает на меня.
решение RSA/None/NoPadding
хорошо, так что я получил его работу, но БЕЗ ПРОКЛАДКИ. Эта часть действительно расстраивает меня, и я оставлю это другим, чтобы попытаться помочь. Возможно, я в конечном итоге выпущу то, что у меня есть как библиотека на github, один для Obj-C, один для Java. Вот что мне удалось выяснить.
TL; DR: сохраните ключ в связке ключей с минимальными атрибутами, Чтобы упростить поиск. Зашифровать с помощью SecKeyEncrypt
но использовать kSecPaddingNone
. Расшифровать на стороне Java с BouncyCastle и алгоритмом RSA/None/NoPadding
.
отправка открытого ключа RSA в iOS с Java
Использование Сертификата X. 509
Я хотел проверить, отправляет ли открытый ключ напрямую, удаляя ASN.1 заголовок и сохранение фактически делали то, что он должен был делать. Поэтому я посмотрел на отправку открытого ключа в качестве сертификата. Я хочу отдать должное Дэвид Бенько для предоставления библиотеки шифрования (https://github.com/DavidBenko/DBTransitEncryption), который помог мне с преобразованием сертификата. На самом деле я не пользовался его библиотекой, потому что 1. Я уже использую RNCryptor
/JNCryptor
для моего шифрования AES и 2. у него нет компонента Java, поэтому мне нужно было бы написать там свою собственную дешифровку AES, и я не хотел этого делать. Для тех, кто заинтересован и хочет принять этот подход, вот мой код для создания сертификата на Java и затем преобразовывая сертификат на открытый ключ в iOS:
* важное примечание: пожалуйста, замените e.printStackTrace()
с реальными регистрационными заявлениями. Я использовал это только для тестирования и не в производстве.
Java:
public static X509Certificate generateCertificate (KeyPair newKeys) {
Security.addProvider(new BouncyCastleProvider());
Date startDate = new Date();
Date expiryDate = new DateTime().plusYears(100).toDate();
BigInteger serialNumber = new BigInteger(10, new Random());
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(newKeys
.getPrivate());
SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(ASN1Sequence.getInstance(newKeys
.getPublic().getEncoded()
));
X500Name dnName = new X500Name("CN=FoodJudge API Certificate");
X509v1CertificateBuilder builder = new X509v1CertificateBuilder(dnName,
serialNumber,
startDate, expiryDate,
dnName,
subjectPublicKeyInfo);
X509CertificateHolder holder = builder.build(sigGen);
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(holder);
}
catch (OperatorCreationException e) {
e.printStackTrace();
}
catch (CertificateException e) {
e.printStackTrace();
}
return null;
}
Obj-C:
- (SecKeyRef)extractPublicKeyFromCertificate:(NSData *)certificateBytes {
if (certificateBytes == nil) {
return nil;
}
SecCertificateRef certificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef) certificateBytes);
if (certificate == nil) {
NSLog(@"Can not read certificate from data");
return false;
}
SecTrustRef trust;
SecPolicyRef policy = SecPolicyCreateBasicX509();
OSStatus returnCode = SecTrustCreateWithCertificates(certificate, policy, &trust);
// release the certificate as we're done using it
CFRelease(certificate);
// release the policy
CFRelease(policy);
if (returnCode != errSecSuccess) {
NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %d", (int)returnCode);
return nil;
}
SecTrustResultType trustResultType;
returnCode = SecTrustEvaluate(trust, &trustResultType);
if (returnCode != errSecSuccess) {
// TODO log
CFRelease(trust);
return nil;
}
SecKeyRef publicKey = SecTrustCopyPublicKey(trust);
CFRelease(trust);
if (publicKey == nil) {
NSLog(@"SecTrustCopyPublicKey fail");
return nil;
}
return publicKey;
}
использование открытого ключа RSA
важно отметить, что вам не нужно отправить открытый ключ в сертификате. Фактически, после обнаружения, что открытый ключ был сохранен неправильно (см. Ниже), я вернул этот код и сохранил открытый ключ на моем устройстве. Вам нужно будет раздеть ASN.1
заголовок, как указано в одном из сообщений блога. Этот код повторно размещен здесь (отформатирован для ясности).
+ (NSData *)stripPublicKeyHeader:(NSData *)keyBits {
// Skip ASN.1 public key header
if (keyBits == nil) {
return nil;
}
unsigned int len = [keyBits length];
if (!len) {
return nil;
}
unsigned char *c_key = (unsigned char *)[keyBits bytes];
unsigned int idx = 0;
if (c_key[idx++] != 0x30) {
return nil;
}
if (c_key[idx] > 0x80) {
idx += c_key[idx] - 0x80 + 1;
}
else {
idx++;
}
if (idx >= len) {
return nil;
}
if (c_key[idx] != 0x30) {
return nil;
}
idx += 15;
if (idx >= len - 2) {
return nil;
}
if (c_key[idx++] != 0x03) {
return nil;
}
if (c_key[idx] > 0x80) {
idx += c_key[idx] - 0x80 + 1;
}
else {
idx++;
}
if (idx >= len) {
return nil;
}
if (c_key[idx++] != 0x00) {
return nil;
}
if (idx >= len) {
return nil;
}
// Now make a new NSData from this buffer
return([NSData dataWithBytes:&c_key[idx] length:len - idx]);
}
поэтому я бы просто сохранил ключ так:
- (void)storeServerPublicKey:(NSString *)serverPublicKey {
if (!serverPublicKey) {
return;
}
SecKeyWrapper *secKeyWrapper = [SecKeyWrapper sharedWrapper];
NSData *decryptedServerPublicKey = [[NSData alloc] initWithBase64EncodedString:serverPublicKey options:0];
NSData *strippedServerPublicKey = [SecKeyWrapper stripPublicKeyHeader:decryptedServerPublicKey];
if (!strippedServerPublicKey) {
return;
}
[secKeyWrapper savePublicKeyToKeychain:strippedServerPublicKey tag:@"com.sampleapp.publickey"];
}
сохранение открытого ключа RSA в Keychain
это было невыносимо. Оказалось, что хотя я и сохранил ключ от брелка, то, что я нашел, не было тем, что я положить в! Я обнаружил это случайно, когда сравнивал ключ base64, который я сохранял, с ключом base64, который я использовал для шифрования моего ключа AES. Поэтому я узнал, что лучше упростить NSDictionary, используемый при сохранении ключа. Вот что у меня получилось:
- (void)savePublicKeyToKeychain:(NSData *)key tag:(NSString *)tagString {
NSData *tag = [self getKeyTag:tagString];
NSDictionary *saveDict = @{
(__bridge id) kSecClass : (__bridge id) kSecClassKey,
(__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id) kSecAttrApplicationTag : tag,
(__bridge id) kSecAttrKeyClass : (__bridge id) kSecAttrKeyClassPublic,
(__bridge id) kSecValueData : key
};
[self saveKeyToKeychain:saveDict tag:tagString];
}
- (void)saveKeyToKeychain:(NSDictionary *)saveDict tag:(NSString *)tagString {
OSStatus sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, NULL);
if (sanityCheck != errSecSuccess) {
if (sanityCheck == errSecDuplicateItem) {
// delete the duplicate and save again
sanityCheck = SecItemDelete((__bridge CFDictionaryRef) saveDict);
sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, NULL);
}
if (sanityCheck != errSecSuccess) {
NSLog(@"Problem saving the key to keychain, OSStatus == %d.", (int) sanityCheck);
}
}
// remove from cache
[keyCache removeObjectForKey:tagString];
}
чтобы получить мой ключ, я использую следующие методы:
- (SecKeyRef)getKeyRef:(NSString *)tagString isPrivate:(BOOL)isPrivate {
NSData *tag = [self getKeyTag:tagString];
id keyClass = (__bridge id) kSecAttrKeyClassPublic;
if (isPrivate) {
keyClass = (__bridge id) kSecAttrKeyClassPrivate;
}
NSDictionary *queryDict = @{
(__bridge id) kSecClass : (__bridge id) kSecClassKey,
(__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id) kSecAttrApplicationTag : tag,
(__bridge id) kSecAttrKeyClass : keyClass,
(__bridge id) kSecReturnRef : (__bridge id) kCFBooleanTrue
};
return [self getKeyRef:queryDict tag:tagString];
}
- (SecKeyRef)getKeyRef:(NSDictionary *)query tag:(NSString *)tagString {
SecKeyRef keyReference = NULL;
OSStatus sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef *) &keyReference);
if (sanityCheck != errSecSuccess) {
NSLog(@"Error trying to retrieve key from keychain. tag: %@. sanityCheck: %li", tagString, sanityCheck);
return nil;
}
return keyReference;
}
в конце концов, я смог заставить его работать только без заполнения. Я не уверен, почему BouncyCastle
не удалось удалить набивка, так что если у кого-то есть понимание, дайте мне знать.
вот мой код для шифрования (с изменениями от Дэвид Бенько):
- (NSData *)encryptData:(NSData *)content usingPublicKey:(NSString *)publicKeyTag {
SecKeyRef publicKey = [self getKeyRef:publicKeyTag isPrivate:NO];
NSData *keyBits = [self getKeyBitsFromKey:publicKey];
NSString *keyString = [keyBits base64EncodedStringWithOptions:0];
NSAssert(publicKey != nil,@"Public key can not be nil");
size_t cipherLen = SecKeyGetBlockSize(publicKey); // convert to byte
void *cipher = malloc(cipherLen);
size_t maxPlainLen = cipherLen - 12;
size_t plainLen = [content length];
if (plainLen > maxPlainLen) {
NSLog(@"content(%ld) is too long, must < %ld", plainLen, maxPlainLen);
return nil;
}
void *plain = malloc(plainLen);
[content getBytes:plain
length:plainLen];
OSStatus returnCode = SecKeyEncrypt(publicKey, kSecPaddingNone, plain,
plainLen, cipher, &cipherLen);
NSData *result = nil;
if (returnCode != errSecSuccess) {
NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)returnCode);
}
else {
result = [NSData dataWithBytes:cipher
length:cipherLen];
}
free(plain);
free(cipher);
return result;
}
вот как я расшифровываю на стороне Java:
private Response authenticate (String encryptedSymmetricString) {
byte[] encryptedSymmetricKey = Base64.decodeBase64(encryptedSymmetricKeyString);
String privateKey = Server.getServerPrivateKey();
byte[] decryptedSymmetricKey = KeyHandler.decryptMessage(encryptedSymmetricKey, privateKey,
KeyHandler.ASYMMETRIC_CIPHER_ALGORITHM);
}
public static byte[] decryptMessage (byte[] message, String privateKeyString, String algorithm) {
if (message == null || privateKeyString == null) {
return null;
}
PrivateKey privateKey = getPrivateKey(privateKeyString);
return decryptMessage(message, privateKey, algorithm);
}
public static byte[] decryptMessage (byte[] message, PrivateKey privateKey, String algorithm) {
if (message == null || privateKey == null) {
return null;
}
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, privateKey, algorithm, true);
if (cipher == null) {
return null;
}
try {
return cipher.doFinal(message);
}
catch (IllegalBlockSizeException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
catch (BadPaddingException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
}