I'm trying to implement AES cryptography between an iOS app and a java servlet. Java servlet uses BouncyCastle library while iOS app uses OpenSSL. Although I've used same public/private key pair and domain parameters for both side, shared secret generated by OpenSSL sometimes differs from what generates by BouncyCastle on server - side.
The procedure is as follows;
server_public_key
, server_private_key
)server_public_key
is embedded in iOS app in form of an EC_POINT
X
and Yclient_key_curve
which is an EC_KEY
), andserver_public_key
and calculates shared secret
(key_agreement
) based on server_public_key and client_key_curve, andclient_public_key
(extracted from client_key_curve
) as well as
a ciphered message which is encrypted symmetrically using the
derived shared secret (key_agreement
) are sent to serverclient_public_key
and server ECDH parameters which are the same as
client side, andkey_agreement
BUT the decrypted messages are not always the same as the client sent messages.
Since I've also developed an Android app which uses the same procedure but employs BouncyCastle for cryptography therefore I suspect the correctness of implemented code using OpenSSL, so the code is revealed here for others to help resolving the issue. What I've implemented to calculate the shared secret is as follows
- (void)calculateSharedSecret
{
BN_CTX* bn_ctx;
EC_KEY* client_key_curve = NULL;
EC_KEY* server_key_curve = NULL;
EC_GROUP* client_key_group = NULL;
EC_GROUP* server_key_group = NULL;
EC_POINT* client_publicKey = NULL;
EC_POINT* server_publicKey = NULL;
BIGNUM* client_privatKey = NULL;
BIGNUM* client_publicK_x = NULL;
BIGNUM* client_publicK_y = NULL;
BIGNUM* server_publicK_x = NULL;
BIGNUM* server_publicK_y = NULL;
NSException *p = [NSException exceptionWithName:@"" reason:@"" userInfo:nil];
bn_ctx = BN_CTX_new();
BN_CTX_start(bn_ctx);
client_publicK_x = BN_CTX_get(bn_ctx);
client_publicK_y = BN_CTX_get(bn_ctx);
client_privatKey = BN_CTX_get(bn_ctx);
server_publicK_x = BN_CTX_get(bn_ctx);
server_publicK_y = BN_CTX_get(bn_ctx);
// client
if ((client_key_curve = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)) == NULL)
@throw p;
if ((client_key_group = (EC_GROUP *)EC_KEY_get0_group(client_key_curve)) == NULL)
@throw p;
if (EC_KEY_generate_key(client_key_curve) != 1)
@throw p;
if ((client_publicKey = (EC_POINT *)EC_KEY_get0_public_key(client_key_curve)) == NULL)
@throw p;
if (EC_KEY_check_key(client_key_curve) != 1)
@throw p;
client_privatKey = (BIGNUM *)EC_KEY_get0_private_key(client_key_curve);
char *client_public_key = EC_POINT_point2hex(client_key_group, client_publicKey, POINT_CONVERSION_COMPRESSED, bn_ctx);
char *client_privat_key = BN_bn2hex(client_privatKey);
_clientPublicKey = [NSString stringWithCString:client_public_key encoding:NSUTF8StringEncoding];
// server
NSArray* lines = [self loadServerPublicKeyXY];
NSString *public_str_x = [lines objectAtIndex:0];
NSString *public_str_y = [lines objectAtIndex:1];
BN_dec2bn(&server_publicK_x, [public_str_x UTF8String]);
BN_dec2bn(&server_publicK_y, [public_str_y UTF8String]);
if ((server_key_curve = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)) == NULL)
@throw p;
if ((server_key_group = (EC_GROUP *)EC_KEY_get0_group(server_key_curve)) == NULL)
@throw p;
if (EC_KEY_generate_key(server_key_curve) != 1)
@throw p;
if ((server_publicKey = EC_POINT_new(server_key_group)) == NULL)
@throw p;
if (EC_POINT_set_affine_coordinates_GFp(server_key_group, server_publicKey, server_publicK_x, server_publicK_y, bn_ctx) != 1)
@throw p;
if (EC_KEY_check_key(server_key_curve) != 1)
@throw p;
unsigned char *key_agreement = NULL;
key_agreement = (unsigned char *)OPENSSL_malloc(SHA_DIGEST_LENGTH);
if (ECDH_compute_key(key_agreement, SHA_DIGEST_LENGTH, server_publicKey, client_key_curve, KDF1_SHA1) == 0)
@throw p;
_symmetricKey = [NSData dataWithBytes:key_agreement length:16];
}
and
void *KDF1_SHA1(const void *input, size_t inlen, void *output, size_t *outlen)
{
if (*outlen < SHA_DIGEST_LENGTH)
return NULL;
else
*outlen = SHA_DIGEST_LENGTH;
return SHA1(input, inlen, output);
}
_clientPublicKey
and _symmetricKey
are declared at class level
The same curve (named prime256v1 or secp256r1) is used on both side but the results are not always the same.
EDIT 1: In response to @PeterDettman, I’ve published server – side code for more clarification
public byte[] generateAESSymmetricKey(byte[] client_public_key_hex) throws InvalidRequest{
try {
// ECDH Private Key as well as other prime256v1 params was generated by Java "keytool" and stored in a JKS file
KeyStore keyStore = ...;
PrivateKey privateKey = (PrivateKey) keyStore.getKey("keyAlias", "keyStorePassword".toCharArray());
ECPrivateKeyParameters ecdhPrivateKeyParameters = (ECPrivateKeyParameters) (PrivateKeyFactory.createKey(privateKey.getEncoded()));
ECCurve ecCurve = ecdhPrivateKeyParameters.getParameters().getCurve();
ECDomainParameters ecDomainParameters = ecdhPrivateKeyParameters.getParameters();
ECPublicKeyParameters client_public_key = new ECPublicKeyParameters(ecCurve.decodePoint(client_public_key_hex), ecDomainParameters);
BasicAgreement agree = new ECDHBasicAgreement();
agree.init(ecdhPrivateKeyParameters);
byte[] keyAgreement = agree.calculateAgreement(client_public_key).toByteArray();
SHA1Digest sha1Digest = new SHA1Digest();
sha1Digest.update(keyAgreement, 0, keyAgreement.length);
byte hashKeyAgreement[] = new byte[sha1Digest.getDigestSize()];
sha1Digest.doFinal(hashKeyAgreement, 0);
byte[] server_calculatd_symmetric_key = new byte[16];
System.arraycopy(hashKeyAgreement, 0, server_calculatd_symmetric_key, 0, server_calculatd_symmetric_key.length);
return server_calculatd_symmetric_key;
} catch (Throwable ignored) {
return null;
}
}
where client_public_key_hex
is client_public_key
that is converted to an array of byte. The expected result is that server_calculatd_symmetric_key
equals symmetricKey
for all the time. BUT they are not always the same.
EDIT 2: As a feedback to @PeterDettman answer, I made some changes to reflect his suggestion and although rate of inequality reduces, generated key agreements (shared secret) on either side are not still equal in all cases.
It is possible to reproduce one of inequality case with following data
So is there any mistake in the implemented code or procedure?
Thanks
There's a problem in the server code, in the way that the ECDH agreement value is converted to bytes:
byte[] keyAgreement = agree.calculateAgreement(client_public_key).toByteArray();
Try this instead:
BigInteger agreementValue = agree.calculateAgreement(client_public_key);
byte[] keyAgreement = BigIntegers.asUnsignedByteArray(agree.getFieldSize(), agreementValue);
This will ensure a fixed-size byte array as output, which is a requirement for converting EC field elements to octet strings (search "Field Element to Octet String Conversion Primitive" for more details).
I recommend you ignore the SHA1 key derivation part until you can get that Java keyAgreement byte array to exactly match the input to your KDF1_SHA1 function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With