I am a iOS developer and trying to use Secure enclave to generate ECC pair key. I am able to do that successfully using the sample app here: https://github.com/agens-no/EllipticCurveKeyPair. When I use this key along with a Python implementation to do encryption and decryption mentioned here: https://gist.github.com/dschuetz/2ff54d738041fc888613f925a7708a06 it works.
The problem is that i need a Java code to do the same. Can anyone help me to achieve this or point me to a code that does the same job of as Python code is doing.
On iOS side I am doing eciesEncryptionStandardX963SHA256AESGCM encrypt and decrypt logic.
I know i should have tried to solve this myself. But I am a iOS Engineer and trying my hands on Java backend. Would be really helpful if someone can guide me.
Created a sample Java code based on the answer. Link to code: https://gist.github.com/balrajOla/fa2f6030538b20a396c086377a6f7114
Using the sample iOS App provided here: https://github.com/agens-no/EllipticCurveKeyPair. I generated ECC keys.
Then pass the public key to the Java code to create an encrypted message. This encrypted messages is passed back to sample iOS app mentioned above to be decrypted using eciesEncryptionStandardX963SHA256AESGCM algo.
But we get an error mentioned below snapshot.
We had the same problem. We want to bring a EC key exchange working from iOS secure enclave with a Java Back-end.
After three days of trial&error, we finally found a Java implementation which is working.
Java code was taken from https://github.com/O2-Czech-Republic/BC-ECIES-for-iOS
And the iOS code, using eciesEncryptionCofactorVariableIVX963SHA256AESGCM algorithm:
static func getExportableKeyFromECKey() -> String? {
// If exists already a created EC Key, then export the public part
if let privateKey = self.loadECPrivateKey() {
if let publicKey = self.getECPublicKey(privateKey) {
if self.algorithmAcceptedForEC(publicKey) {
var error: Unmanaged<CFError>?
// Get Public key External represenatation
guard let cfdata = SecKeyCopyExternalRepresentation(publicKey, &error) else {
return nil
}
let pubKeyData: Data = cfdata as Data
return pubKeyData.base64EncodedString()
}
}
}
// If no EC Key created, then first create one
else {
var error: Unmanaged<CFError>?
let tag = Config.skName.data(using: .utf8) ?? Data()
let attributes: [String: Any] = [kSecClass as String: kSecClassKey,
kSecAttrKeyType as String: Config.skType,
kSecAttrKeySizeInBits as String: Config.ecKeySize,
kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag]]
do {
// Create Private Key
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
// Get Public Key
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
throw error!.takeRetainedValue() as Error
}
// Get Public key External represenatation
guard let cfdata = SecKeyCopyExternalRepresentation(publicKey, &error) else {
throw error!.takeRetainedValue() as Error
}
let pubKeyData: Data = cfdata as Data
return pubKeyData.base64EncodedString()
} catch {
print(error)
}
}
return nil
}
static func loadECPrivateKey() -> SecKey? {
let tag = Config.skName.data(using: .utf8)!
let query: [String: Any] = [kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: Config.skType,
kSecReturnRef as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
return nil
}
print("LOAD PRIVATE KEY: \n \(item as! SecKey) \n")
return (item as! SecKey)
}
static func getECPublicKey(_ privateKey: SecKey) -> SecKey? {
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
// Can't get public key
return nil
}
return publicKey
}
static func algorithmAcceptedForEC(_ publicKey: SecKey) -> Bool {
guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, Config.ecAlgorithm) else {
// Algorith not supported
print("\nEncrytion Algorithm not supported!!!\n")
return false
}
return true
}
/// if let encryptedData = Data(base64Encoded: "BOqw779hxsGLMEV7X81Mphcx+SMtxSQs388s5CydkvJ4V2XuuWoyp48GCmgDMBnYlEIRqAdHxIc/Ts3ATxa9ENCDGdIZf5CjpWsOIVXYxLvupdap4w==", options:.ignoreUnknownCharacters)
static func decryptStr(_ encData: Data) {
/// 1. Step: Get the Private Key and decrypt the symmetric key
let privateKey = loadECPrivateKey()
guard SecKeyIsAlgorithmSupported(privateKey!, .decrypt, Config.ecAlgorithm) else {
print("Can't decrypt\nAlgorithm not supported")
return
}
DispatchQueue.global().async {
var error: Unmanaged<CFError>?
let clearTextData = SecKeyCreateDecryptedData(privateKey!,
Config.ecAlgorithm,
encData as CFData,
&error) as Data?
DispatchQueue.main.async {
guard clearTextData != nil else {
print("Can't decrypt")
return
}
let clearText = String(decoding: clearTextData!, as: UTF8.self)
print("Decrypted Info: \(clearText)")
// clearText is our decrypted string
}
}
}
In Java you have two interesting classes - ECGenParameterSpec and KeyPairGenerator. ECGenParameterSpec specifies parameters for generating elliptic curve domain parameters, and KeyPairGenerator is used to generate pairs of public and private keys.
In the book Android Security Internals by Nokilay Elenkov there's a good code example of their combination to generate the key pair.
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH");
ECGenParameterSpec ecParamSpec = new ECGenParameterSpec("secp256r1");
kpg.initialize(ecParamSpec);
KeyPair keyPair = kpg.generateKeyPair();
This is the explanation given about the previous code
There are two ways to initialize a KeyPairGenerator: by specifying the
desired key size and by specifying algorithm-specific parameters. In
both cases, you can optionally pass a SecureRandom instance to be used
for key generation. If only a key size is specified, key generation
will use default parameters (if any). To specify additional
parameters, you must instantiate and configure an
AlgorithmParameterSpec instance appropriate for the asymmetric
algorithm you are using and pass it to the initialize() method, as
shown in Example 5-15. In this example, the ECGenParameterSpec
initialized in line 2 is an AlgorithmParameterSpec that allows you to
specify the curve name used when generating Elliptic Curve (EC)
cryptography keys. After it is passed to the initialize() method in line 3,
the subsequent generateKeyPair() call in line 4 will use the specified
curve (secp256r1) to generate the key pair.
Related
I am having some trouble using the JWT debugger at https://jwt.io/ I am using the JWT library from https://github.com/jwtk/jjwt The use case is to send requests to an API that requires that I pass an API key in the token payload and sign it using the client secret that they give me. So far, using the client secret they sent me (one secret appeared to be base64, but the second one they sent was definitely not base 64). So far generating the token using the above library fails on the REST API, and it also fails at jwt.io. If I can get that working, I can probably get it to work on the REST API as well.
Here is a block of code I use to generate my code. I used a very simple secret string for this example but it is 32 bytes long (the client secret they gave me was 43 bytes long):
public class TestHarness {
public static void main(String[] args) {
String apiKey = "39999999-ba25-476a-957e-806f9f726e39";
String secret = "123456789-123456789_123456789_12";
//long timeNow = JWTutil.getUnixTime();
// Using same time so that each time we run we should get the exact same
// token
long timeNow = 1672761664338l;
System.out.println(String.format("Now in UNIX is %d", timeNow));
String token = JWTutil.getToken(apiKey, secret, 300, timeNow);
System.out.println(token);
String[] parts = token.split(token, '.');
System.out.println(String.format("Token has %d parts", parts.length));
for (int i=0;i<parts.length;i++) {
System.out.println(String.format("Part %d : %s", i, parts[i]));
}
if (parts.length > 2) {
System.out.println(parts[2]);
}
}
And here is my JWT utils class with the methods used above to generate the token
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.crypto.SecretKey;
public class JWTutil {
private static SignatureAlgorithm sigHS256 = SignatureAlgorithm.HS256;
public static String getToken(String appID, String secret, int duration ) {
long currTime = getUnixTime();
return getToken(appID, secret, duration, currTime);
}
public static String getToken(String appID, String secret, int duration, long currTime) {
String token = Jwts.builder().setHeader(getHeader()).addClaims(getClaims(appID, currTime, duration)).signWith(getSigningKey(secret), sigHS256).compact();
return token;
}
private static Map<String,Object> getHeader() {
Map<String, Object> map = new HashMap<String,Object>();
map.put("alg", "HS256");
map.put("typ", "JWT");
return map;
}
private static Map<String,Object> getClaims(String appId, long now, int duration) {
Map<String,Object> claims = new HashMap<String,Object>();
claims.put("sub", appId);
claims.put("iat", Long.valueOf(now));
claims.put("exp", Long.valueOf(now + (long)duration));
return claims;
}
private static Key getSigningKey(String secret) {
byte[] theBytes = secret.getBytes(StandardCharsets.UTF_8);
//byte[] encodedBytes = Base64.getEncoder().encode(secret.getBytes(StandardCharsets.UTF_8));
//Key key = new SecretKeySpec(encodedBytes, sigHS256.getJcaName());
SecretKey key = Keys.hmacShaKeyFor(theBytes);
return key;
}
public static long getUnixTime() {
Date current = new Date();
return current.getTime();
}
}
I did look at the source on JJWT (I couldn't find the link to proper JavaDocs but the source comments on each function served the same purpose), the signWith() function takes 1 or 2 arguments - either a Key (1 argument) which will "guess" the appropriate algorithm to use, or 2 args - A key and the signing algorithm (presumably for when you want to explicitly set the signature). In an older post, it quoted signWith() taking 1 arg as the Signing algorithm and the second as a base64 string but I seem to recall that is deprecated - it's a key or nothing.
On jwt.io, I provide the client secret I used to create the secret with. I suspect that I may be misunderstanding what I'm supposed to provide. When answering, assume that I'm given a human readable string which may or may not already be Base64 encoded. When I toggle the Base64 Encoded Secret checkbox on jwt.io, the signature changes - both ways to something other than what was in original token, even with the text staying the same for the key.
I think I discovered the problem. I looked at the code sample that K. Nicholas provided. The Nimbus-Jose library is one that I had previously tried. However the problem with the jwt.io site was more low-tech than that. WHen entering your client secret, make sure you erase the text that says "your 256 bit secret".
I thought it was an underlay but no it's actual text content. If you paste your secret in that box, it just prepends it to the text "your 256 bit secret".
In this case the solution was very low tech.
I am trying to create an app in flutter and the sample code which I have is in java.
Here is the sample java code gist
https://gist.github.com/kapiljhajhria/72a22ff75e238878f539f7bb21026208
and here is my flutter code gist
https://gist.github.com/kapiljhajhria/795d1a7c7cf1c76ca8e327bf8b2f51de
Here is a brief summary of what I am doing
Generate a unique Session Key: AES Random Key 256
Encrypt JSON data using the Session Key from step 1
Generate SHA256 hash of JSON data
Encrypt generated hash from step 3, using session key from step 1.
Encrypt the session key using the public key. Public key provided as certificate.cer file. I copied the String value and added it to the class as a constant in order to make it easier to use. Not sure if this was the best approach.
Created a POST request with 3 Parameters. As shown in the java code. I think I am doing this part correctly.
The response which I will get will be encrypted using the session key from step 1. So i will have to decrypt that response data. Haven't reached this step yet.
I don't have access to the server where this request is being made.
Since the post request is being made using web view, I can't figure out a way to get proper error from my request. All I get is web page which says "Invalid Request"
So My first guess is that I am not using public key properly to encrypt my session key.
if that part is correct, then I am not encrypting the data properly or my encryption method doesn't match encryption methods used in java code the java code. Maybe the session key which I am generating is not correct.
Any help would be greatly appreciated. Thank you. If you need anything from me then please let me know.
I used this document as my reference: https://developers.emsigner.com/signer-gateway/api-reference/signing-documents.html
You need 2 packages: pointycastle and x509, and to import them as follows:
import 'package:pointycastle/export.dart';
import 'package:x509/x509.dart';
Then you need these helper functions:
Uint8List generateSessionKey() {
final r = Random();
return Uint8List.fromList(List<int>.generate(32, (_) => r.nextInt(256)));
}
RSAPublicKey parseCert(String pemData) {
final cert = parsePem(pemData).first as X509Certificate;
final pub = cert.publicKey as RsaPublicKey;
return RSAPublicKey(pub.modulus, pub.exponent);
}
Uint8List encryptUsingPublicKey(RSAPublicKey key, Uint8List data) {
final cipher = PKCS1Encoding(RSAEngine())
..init(true, PublicKeyParameter<RSAPublicKey>(key));
return cipher.process(data);
}
Uint8List encryptUsingSessionKey(Uint8List key, Uint8List data) {
final cipher = PaddedBlockCipher('AES/ECB/PKCS7')
..init(true, PaddedBlockCipherParameters(KeyParameter(key), null));
return cipher.process(data);
}
Uint8List sha256Digest(Uint8List data) {
return SHA256Digest().process(data);
}
And you'd build your 3 parameters like this:
final pem = File('cert2.pem').readAsStringSync();
final publicKey = parseCert(pem);
final sessionKey = generateSessionKey();
final encryptedSessionKey = encryptUsingPublicKey(publicKey, sessionKey);
final jsonString = json.encode(<String, dynamic>{
'FileType': 'PDF',
'SignaturePosition': 'Top-Left',
'AuthToken': 'some token',
'File': '',
'SUrl': 'http://localhost:3000/Success',
'FUrl': 'http://localhost:3000/Error',
'CUrl': 'http://localhost:3000/Cancel',
'ReferenceNumber': 'generate unique reference number',
});
final jsonBytes = utf8.encode(jsonString) as Uint8List;
final encryptedJson = encryptUsingSessionKey(sessionKey, jsonBytes);
final hash = sha256Digest(jsonBytes);
final encryptedHash = encryptUsingSessionKey(sessionKey, hash);
final p1 = base64.encode(encryptedSessionKey);
final p2 = base64.encode(encryptedJson);
final p3 = base64.encode(encryptedHash);
BUT, the big problem I see is how you then do the POST, because you want to be in a web page, right? And the normal flutter web view doesn't support an initial post. It does look like there's another package. Just search for flutter webview post.
By the way, if you don't want to use the pointycastle registry, you can rewrite encryptUsingSessionKey without as:
final cipher = PaddedBlockCipherImpl(
PKCS7Padding(),
ECBBlockCipher(AESEngine()),
)..init(true, PaddedBlockCipherParameters(KeyParameter(key), null));
return cipher.process(data);
Finally, at least until you get the web view issue understood, you can just use http to do the post. But, let it do the work of encoding the parameters and setting the content type, as follows:
final response = await http.post(
Uri.parse('https://somewhere/V3_0/Index'),
body: <String, String>{
'Parameter1': p1,
'Parameter2': p2,
'Parameter3': p3,
},
);
print(response.statusCode);
print(response.body);
Have a look at this project, It's an example for the Encryption by Flutter with a good documentation, have 3 types of encryptions:
AES encryptions
Fernet encryptions
Salsa20 encryptions
I'm trying to integrate Apple Map Web Snapshot which needs a signature query parameter in the URL. I able to successfully generate and validate ES256 signature in JWA package from NPM but not in Java. Please help me on finding equivalent lib to generate valid signature, I have tried few JWA libs in Java.
// Required modules.
const { readFileSync } = require("fs");
const { sign } = require("jwa")("ES256");
/* Read your private key from the file system. (Never add your private key
* in code or in source control. Always keep it secure.)
*/
const privateKey = readFileSync("[file_system_path]");
// Replace the team ID and key ID values with your actual values.
const teamId = "[team ID]";
const keyId = "[key ID]";
// Creates the signature string and returns the full Snapshot request URL including the signature.
function sign(params) {
const snapshotPath = `/api/v1/snapshot?${params}`;
const completePath = `${snapshotPath}&teamId=${teamId}&keyId=${keyId}`;
const signature = sign(completePath, privateKey);
// In this example, the jwa module returns the signature as a Base64 URL-encoded string.
// Append the signature to the end of the request URL, and return.
return `${completePath}&signature=${signature}`;
}
// Call the sign function with a simple map request.
sign("center=apple+park")
// The return value expected is: "/api/v1/snapshot?center=apple+park&teamId=[team ID]&keyId=[key ID]&signature=[base64_url_encoded_signature]"
Apache CXF - This lib generates similar to JWA module in node but failed to authenticate.
String teamId = [Team Id];
String keyId = [Key id];
String privateKey = [private key path];
String privateKeyContent = getKeyFileContent(privateKey);
String API_VERSION_PATH = "/api/v1/snapshot?";
String param = [QueryParam];
//example -> param = "center=[city,country or lat,lang]&size=90x90&lang=en&radius=2";
String params = param + "&teamId="+ teamId + "&keyId=" + keyId;
String payload = API_VERSION_PATH + params;
PrivateKey key = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(
Base64.decodeBase64(privateKeyContent)));
JwsCompactProducer compactProducer = new JwsCompactProducer(payload);
compactProducer.getJwsHeaders().setSignatureAlgorithm(SignatureAlgorithm.ES256);
//compactProducer.getJwsHeaders().setKeyId(keyId);
compactProducer.signWith(key);
String signed = compactProducer.getEncodedSignature();
String encodedSignature = new String(Base64.encodeBase64URLSafe(compactProducer.getEncodedSignature().getBytes()));
System.out.println(SNAPSHOT_API_PATH + payload + "&signature=" + signed);
JJWT - This lib generates big signature then the signature generated in node module.
String signed = new String(Base64.encodeBase64URLSafe(Jwts.builder().setPayload(payload)
.signWith(io.jsonwebtoken.SignatureAlgorithm.ES256, key).compact().getBytes()));
System.out.println(SNAPSHOT_API_PATH + payload + "&signature=" + signed);
sample output signature
compactProducer.getEncodedSignature() signed --> qQ5G9_lwGJ9w158FVSmtPx_iH43xlg2_gx9BlHEJbER73xpAeIHtDRnT8wnveH_UEPxNe7Zgv4csJ48Oiq-ZIQ
Base64.encodeBase64URLSafe(signature) --> cVE1RzlfbHdHSjl3MTU4RlZTbXRQeF9pSDQzeGxnMl9neDlCbEhFSmJFUjczeHBBZUlIdERSblQ4d252ZUhfVUVQeE5lN1pndjRjc0o0OE9pcS1aSVE
JJWT signed -> ZXlKaGJHY2lPaUpGVXpJMU5pSjkuTDJGd2FTOTJNUzl6Ym1Gd2MyaHZkRDlqWlc1MFpYSTlRM1Z3WlhKMGFXNXZMRlZUUVNaMFpXRnRTV1E5V0ZaWU5GWlhSbEZUTXlaclpYbEpaRDFWUVRWTlNGWlhWMWhMLlExUEtoeGwzSjFoVWVUWGtmeXRLckliYm5zeDdZem5lZVpxTVc4WkJOVU9uLVlYeFhyTExVU05ZVTZCSG5Xc3FheFd3YVB5dlF0Yml4TVBSZGdjamJ3
The signature in the NodeJS code is generated by the jwa('ES256')#sign method, which has the following functionality:
ES256: ECDSA using P-256 curve and SHA-256 hash algorithm [1].
The signature will be the pair (r, s), where r and s are 256-bit unsigned integers [2].
The signature is base64url-encoded [3].
Ad 1: A corresponding implementation for ES256 is possible in Java using on-board means (SunEC provider, Java 1.7 or higher), [4]:
Signature ecdsa = Signature.getInstance("SHA256withECDSA");
ecdsa.initSign(privateKey);
String payload = "The quick brown fox jumps over the lazy dog";
ecdsa.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signatureDER = ecdsa.sign();
Here privateKey is the private key of type java.security.PrivateKey, analogous to key in the CXF code.
Ad 2: The Java code returns the signature in the ASN.1 DER format and must therefore be converted into the (r,s) format [5]. Either a user-defined method can be implemented or a method from a supporting library can be used, e.g. the method com.nimbusds.jose.crypto.impl.ECDSA.transcodeSignatureToConcat of the Nimbus JOSE + JWT library [6][7][8]:
byte[] signature = transcodeSignatureToConcat(signatureDER, 64);
Ad 3: Base64url encoding is possible in Java with on-board means [9]:
String signatureBase64url = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
Since a different signature is generated each time, a direct comparison of the signatures generated in both codes isn't possible. However, compatibility with the jwa-npm library can be tested by verifying the signature generated in the Java code with the jwa-npm library:
const jwa = require("jwa");
const ecdsa = jwa('ES256');
var message = "The quick brown fox jumps over the lazy dog";
var verify = ecdsa.verify(message, signatureBase64url, publicKey);
Here, signatureBase64url is the signature generated with the Java code. publicKey is the corresponding public key in X.509 PEM format (-----BEGIN PUBLIC KEY-----...) [10].
The functionality of the jwa('ES256')#sign method is different from that of the posted JJWT or Apache CXF code: The last two generate a JWT [11]. The header is the base64url encoding of {"alg": "ES256"}. Accordingly the signature is that for the Base64url encoded header and the Base64url encoded payload, both separated by a dot:
String payload = "The quick brown fox jumps over the lazy dog";
//JJWT
String jwtJJWT = Jwts.builder().setPayload(payload).signWith(io.jsonwebtoken.SignatureAlgorithm.ES256, privateKey).compact();
//CXF
JwsCompactProducer compactProducer = new JwsCompactProducer(payload);
compactProducer.getJwsHeaders().setSignatureAlgorithm(SignatureAlgorithm.ES256);
String jwtCXF = compactProducer.signWith(privateKey);
String signatureCXF = compactProducer.getEncodedSignature(); // signature, 3. portion of JWT
Example for a JWT generated by this:
eyJhbGciOiJFUzI1NiJ9.VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw.rcrzqr3ovu7SH9ci-S6deLn6BuiQkNv9CmeOnUPwva30pfK9BOX0YOgjZP5T08wjxCBTTHV3aex0M76toL8qpw
From the description of AWS
When an encryption context is provided in an encryption request, it is cryptographically bound to the ciphertext such that the same encryption context is required to decrypt (or decrypt and re-encrypt) the data. If the encryption context provided in the decryption request is not an exact, case-sensitive match, the decrypt request fails. Only the order of the encryption context pairs can vary.
However, in the example code of JAVA SDK, it does not specify encryption context when decrypting.
crypto.decryptString(prov, ciphertext);
These two posts sound a bit contradictory to me since I think the decryption user needs to provide the encryption context on his own. I checked the source code of AwsCrypto.decryptString(final CryptoMaterialsManager provider, final String ciphertext)) in the sdk and it seems that the encryption context is also contained in the cipher text.
May I know why it is set up this way ?
After some research, I found that there are at least two ways to perform encryption and decryption. Just post them here if anyone is interested. It is written in kotlin.
class AwsEncryptionSDKWrapper(private val keyIdArn: String, region: String) {
private val crypto = AwsCrypto()
private val prov: KmsMasterKeyProvider = KmsMasterKeyProvider.builder().withDefaultRegion(region).withKeysForEncryption(keyIdArn).build()
fun encrypt(raw: String, encryptionContext: Map<String, String>): String {
return crypto.encryptString(prov, raw, encryptionContext).result
}
fun decrypt(cipherText: String, encryptionContext: Map<String, String>): String {
val decryptedResponse = crypto.decryptString(prov, cipherText)
if (decryptedResponse.masterKeyIds[0] != keyIdArn) {
throw IllegalStateException("Wrong key ID!")
}
encryptionContext.entries.forEach { (key, value) ->
if (value != decryptedResponse.encryptionContext[key]) {
throw IllegalStateException("Wrong Encryption Context!")
}
}
return decryptedResponse.result
}
}
class AwsKMSSDKWrapper(region: String) {
private val client = AWSKMSClientBuilder.standard().withRegion(region).build()
fun encrypt(keyIdArn: String, raw: String, encryptionContext: Map<String, String>): String {
val plaintextBytes = raw.toByteArray(StandardCharsets.UTF_8)
val encReq = EncryptRequest()
encReq.keyId = keyIdArn
encReq.plaintext = ByteBuffer.wrap(plaintextBytes)
encReq.withEncryptionContext(encryptionContext)
val cipherText = client.encrypt(encReq).ciphertextBlob
return Base64.getEncoder().encodeToString(cipherText.array())
}
fun decrypt(base64CipherText: String, encryptionContext: Map<String, String>, keyIdArn: String): String {
val req = DecryptRequest()
.withCiphertextBlob(ByteBuffer.wrap(Base64.getDecoder().decode(base64CipherText)))
.withEncryptionContext(encryptionContext)
val resp = client.decrypt(req)
if (resp.keyId == null || resp.keyId!!.contentEquals(keyIdArn)) throw IllegalStateException("keyid not match ! provided $keyIdArn, actual ${resp.keyId}");
return resp.plaintext.array().toString(StandardCharsets.UTF_8)
}
}
Special credit to #kdgregory for pointing out my confusion.
TL;DR: RSA public key generated in iOS and stored in the keychain, exported as base64 and sent to a java backend, is not recognized.
I'm implementing a chat encryption feature in an iOS app, and I'm using symmetric + asymmetric keys to handle it.
Without going too much into details, at backend I use the user's public key to encrypt a symmetric key used to encrypt and decrypt messages.
I created two frameworks, respectively in Swift and in Java (backend) to handle key generation, encryption, decryption, etc. I also have tests for them, so I'm 100% everything works as expected.
However, it looks like the backend is unable to recognize the format of the public key passed from iOS. Using RSA both sides, this is the code I use in Swift to generate the key:
// private key parameters
static let privateKeyParams: [String : Any] = [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]
// public key parameters
static let publicKeyParams: [String : Any] = [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]
// global parameters for our key generation
static let keyCreationParameters: [String : Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: 2048,
kSecPublicKeyAttrs as String: publicKeyParams,
kSecPrivateKeyAttrs as String: privateKeyParams
]
...
var publicKey, privateKey: SecKey?
let status = SecKeyGeneratePair(Constants.keyCreationParameters as CFDictionary, &publicKey, &privateKey)
I use specular code to read the keys from the keychain.
This is the piece of code I use to export the public key as a base64 string:
extension SecKey {
func asBase64() throws -> String {
var dataPtr: CFTypeRef?
let query: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "...", // Same unique tag here
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnData as String: kCFBooleanTrue
]
let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)
switch (result, dataPtr) {
case (errSecSuccess, .some(let data)):
// convert to Base64 string
let base64PublicKey = data.base64EncodedString(options: [])
return base64PublicKey
default:
throw CryptoError.keyConversionError
}
}
}
At backend level I use this Java code to convert the base64 string to a public key:
public PublicKey publicKeyFrom(String data) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] publicBytes = Base64.decodeBase64(data);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
But this fails at the last line, with this exception:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence
Doing some manual debugging, I noticed that the format of the public key is different - when I generate a key in iOS and then export as base 64, it looks like this:
MIIBCgKCAQEA4M/bRDdH0f6qFIXxOg13RHka+g4Yv8u9PpPp1IR6pSwrM1aq8B6cyKRwnLe/MOkvODvDfJzvGXGQ01zSTxYWAW1B4uc/NCEemCmZqMosSB/VUJdNxxWtt2hJxpz06hAawqV+6HmweAB2dUn9tDEsQLsNHdwYouOKpyRZGimcF9qRFn1RjR0Q54sUh1tQAj/EwmgY2S2bI5TqtZnZw7X7Waji7wWi6Gz88IkuzLAzB9VBNDeV1cfJFiWsZ/MIixSvhpW3dMNCrJShvBouIG8nS+vykBlbFVRGy3gJr8+OcmIq5vuHVhqrWwHNOs+WR87K/qTFO/CB7MiyiIV1b1x5DQIDAQAB
for a total of 360 characters, whereas doing the same in Java (still using RSA) it's like:
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAAnWO4BXUGP0qM3Op36YXkWNxb4I2pPZuZ7jJtfUO7v+IO1mq43WzNaxLqqLPkTnMrv2ACRDK55vin+leQlL1z0LzVxjtZ9F6pajQo1r7PqBlL5N8bzBFKpagEf0QfyHPw0/0kG9DMnvQ+Im881QyN2zdl33wp5Fi+jRT7cunFQIDAQAB
with a length of 216 characters.
I'm unable to figure out what's wrong - apparently I wouldn't be surprised if iOS handles keys in a different key, and require special processing in order to talk with other folks.
Any idea?
We ran into the exact same problem when connecting an iOS app to a Java backend. And the CryptoExportImportManager mentioned by pedrofb helped us out too, which is awesome. However, the code in the CryptoExportImportManager class is a bit elaborated and might be hard to maintain. This is because a top-down approach is used when adding new components to the DER encoding. As a result, numbers contained by length fields must be calculated ahead (i.e. before the contents to which the length applies has been defined). I therefore created a new class that we now use to convert the DER encoding of an RSA public key:
class RSAKeyEncoding: NSObject {
// ASN.1 identifiers
private let bitStringIdentifier: UInt8 = 0x03
private let sequenceIdentifier: UInt8 = 0x30
// ASN.1 AlgorithmIdentfier for RSA encryption: OID 1 2 840 113549 1 1 1 and NULL
private let algorithmIdentifierForRSAEncryption: [UInt8] = [0x30, 0x0d, 0x06,
0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00]
/// Converts the DER encoding of an RSA public key that is either fetched from the
/// keychain (e.g. by using `SecItemCopyMatching(_:_:)`) or retrieved in another way
/// (e.g. by using `SecKeyCopyExternalRepresentation(_:_:)`), to a format typically
/// used by tools and programming languages outside the Apple ecosystem (such as
/// OpenSSL, Java, PHP and Perl). The DER encoding of an RSA public key created by
/// iOS is represented with the ASN.1 RSAPublicKey type as defined by PKCS #1.
/// However, many systems outside the Apple ecosystem expect the DER encoding of a
/// key to be represented with the ASN.1 SubjectPublicKeyInfo type as defined by
/// X.509. The two types are related in a way that if the SubjectPublicKeyInfo’s
/// algorithm field contains the rsaEncryption object identifier as defined by
/// PKCS #1, the subjectPublicKey field shall contain the DER encoding of an
/// RSAPublicKey type.
///
/// - Parameter rsaPublicKeyData: A data object containing the DER encoding of an
/// RSA public key, which is represented with the ASN.1 RSAPublicKey type.
/// - Returns: A data object containing the DER encoding of an RSA public key, which
/// is represented with the ASN.1 SubjectPublicKeyInfo type.
func convertToX509EncodedKey(_ rsaPublicKeyData: Data) -> Data {
var derEncodedKeyBytes = [UInt8](rsaPublicKeyData)
// Insert ASN.1 BIT STRING bytes at the beginning of the array
derEncodedKeyBytes.insert(0x00, at: 0)
derEncodedKeyBytes.insert(contentsOf: lengthField(of: derEncodedKeyBytes), at: 0)
derEncodedKeyBytes.insert(bitStringIdentifier, at: 0)
// Insert ASN.1 AlgorithmIdentifier bytes at the beginning of the array
derEncodedKeyBytes.insert(contentsOf: algorithmIdentifierForRSAEncryption, at: 0)
// Insert ASN.1 SEQUENCE bytes at the beginning of the array
derEncodedKeyBytes.insert(contentsOf: lengthField(of: derEncodedKeyBytes), at: 0)
derEncodedKeyBytes.insert(sequenceIdentifier, at: 0)
return Data(derEncodedKeyBytes)
}
private func lengthField(of valueField: [UInt8]) -> [UInt8] {
var length = valueField.count
if length < 128 {
return [ UInt8(length) ]
}
// Number of bytes needed to encode the length
let lengthBytesCount = Int((log2(Double(length)) / 8) + 1)
// First byte encodes the number of remaining bytes in this field
let firstLengthFieldByte = UInt8(128 + lengthBytesCount)
var lengthField: [UInt8] = []
for _ in 0..<lengthBytesCount {
// Take the last 8 bits of length
let lengthByte = UInt8(length & 0xff)
// Insert them at the beginning of the array
lengthField.insert(lengthByte, at: 0)
// Delete the last 8 bits of length
length = length >> 8
}
// Insert firstLengthFieldByte at the beginning of the array
lengthField.insert(firstLengthFieldByte, at: 0)
return lengthField
}
}
Usage
You could use this class in the function asBase64() like this:
extension SecKey {
func asBase64() throws -> String {
var dataPtr: CFTypeRef?
let query: [String:Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "...", // Same unique tag here
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnData as String: kCFBooleanTrue
]
let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)
switch (result, dataPtr) {
case (errSecSuccess, .some(let data)):
// convert to X509 encoded key
let convertedData = RSAKeyEncoding().convertToX509EncodedKey(data)
// convert to Base64 string
let base64PublicKey = convertedData.base64EncodedString(options: [])
return base64PublicKey
default:
throw CryptoError.keyConversionError
}
}
}
UPDATE - Other Issue
After using the above class for a while, we stumbled upon another issue. Occasionally, the public key that is fetched from the keychain seems to be invalid because, for some reason, it has grown in size. This behavior matches with findings described in the question (although in our case the Base64 encoded key has grown to a size of 392 characters instead of 360 characters). Unfortunately, we didn’t find the exact cause of this strange behavior, but we found two solutions. The first solution is to specify kSecAttrKeySizeInBits along with kSecAttrEffectiveKeySize when defining the query, like in the below code snippet:
let keySize = ... // Key size specified when storing the key, for example: 2048
let query: [String: Any] = [
kSecAttrKeySizeInBits as String: keySize,
kSecAttrEffectiveKeySize as String: keySize,
... // More attributes
]
var dataPtr: CFTypeRef?
let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)
The second solution is to always delete the old key from the keychain (if any) before adding a new key with the same tag.
UPDATE - Alternative Solution
I published this project on GitHub that can be used as an alternative to the above class.
References
A Layman’s Guide to a Subset of ASN.1, BER, and DER
RFC 5280 (X.509 v3)
RFC 8017 (PKCS #1 v2.2)
Some code I found here inspired me when creating the lengthField(...) function.
Java requires a public key encoded in DER format. Unfortunately iOS does not support this standard format and it is needed an additional conversion (I do not know if this will have improved in the latest versions of swift)
See my answer here You can convert the key using CryptoExportImportManager
func exportPublicKeyToDER(keyId:String) -> NSData?{
let publicKey = loadKeyStringFromKeyChainAsNSData(PUBLIC_KEY + keyId)
let keyType = kSecAttrKeyTypeRSA
let keySize = 2048
let exportImportManager = CryptoExportImportManager()
if let exportableDERKey = exportImportManager.exportPublicKeyToDER(publicKey, keyType: keyType as String, keySize: keySize) {
return exportableDERKey
} else {
return nil
}
}