I've two separate applications one written in Java and the other in golang. Java application is responsible to generate keypair out of which public key is shared with golang app. Whenever, there's a need to sign any payload golang app sends a request to Java app and gets the base64 encoded signature in return. This signature needs to be verified using the public key. The verification always fails in golang app. However, I'm able to successfully verify in Java app. Verification in golang works if key generation and signing the payload is also done using golang.
For Java, I'm using Bouncy Castle library and for golang using package https://pkg.go.dev/crypto/ed25519.
Generate Key
Ed25519KeyPairGenerator keyPairGenerator = new Ed25519KeyPairGenerator();
keyPairGenerator.init(new Ed25519KeyGenerationParameters(new SecureRandom()));
AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair();
Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters) asymmetricCipherKeyPair.getPrivate();
Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters) asymmetricCipherKeyPair.getPublic();
String privateKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
String publicKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
Sign Payload
byte[] privateKeyContent = Base64.getDecoder().decode(privateKeyInfo);
Ed25519PrivateKeyParameters privateKeyParameters = new Ed25519PrivateKeyParameters(privateKeyContent, 0);
byte[] payload = Base64.getEncoder().encode(input.getBytes(StandardCharsets.UTF_8));
Signer signer = new Ed25519Signer();
signer.init(true, privateKeyParameters);
signer.update(payload, 0, payload.length);
byte[] signature = signer.generateSignature();
String encodedSignature = Base64.getEncoder().encodeToString(signature);
Golang Verify Signature
func verifySignature(payload []byte, publicKeyStr string, signatureStr string) {
publicKey, error := base64.StdEncoding.DecodeString(publicKeyStr)
if error != nil {
fmt.Println(error)
} else {
signature, error := base64.StdEncoding.DecodeString(signatureStr)
if error != nil {
fmt.Println(error)
} else {
isVerified := ed25519.Verify(publicKey, payload, signature)
fmt.Println(isVerified)
}
}
}
The Java code does not sign the message itself, but the Base64 encoded message.
I suspect that verification fails for you because signed and verified message are different. However, this cannot be answered for sure, since you did not post the content of payload on the Go side.
In any case, verification is successful if on the Java side the message itself is signed (which is usually the case):
String input = "...";
byte[] payload = input.getBytes(StandardCharsets.UTF_8);
Similarly, verification is successful if, with unmodified Java code, the Go side verifies the Base64 encoded message (which would be rather unusual):
input := "..."
payload := []byte(base64.StdEncoding.EncodeToString([]byte(input)))
Related
I need to integrate with some service provider API, where each request needs to be signed with private key of our certificate and service provider will decrypt it using our public key and similarly for response service provider will encrypt it using their private key and we will decrypt it using their public key (Means we will share our Public Key between each other)
Service Provider did not have sample code in php
Instructions which they provided are:
Get contents in Bytes to Sign (Using UTF-8 Encoding)
Get the X509 Certificate (Private Key Certificate)
Compute the Message Digest
Sign the Message and generate attached Signature
(use Detached/attached property of Signing Library)
Encode your Signed Message into Base64 Encoding
Send the Encoded Message on HTTP
Sample Code in Java provided by service provider:
string data = "SAMPLE REQUEST XML";
priv = (PrivateKey)keystore.getKey(name, passw);
CMSSignedDataGenerator sgen = new CMSSignedDataGenerator();
sgen.addSigner(priv, (X509Certificate)cert, CMSSignedDataGenerator.DIGEST_SHA1);
sgen.addCertificatesAndCRLs(certs);
byte[] utf8 = data.getBytes("UTF8");
// The 2nd parameter need to be true (dettached form) we need to attach original message to signed message
CMSSignedData csd = sgen.generate(new CMSProcessableByteArray(utf8), true, "BC");
byte[] signedData = csd.getEncoded();
char[] signedDataB64 = Base64Coder.encode(signedData);
String str = new String(signedDataB64);
I have written following code in php:
$utf8_data = utf8_encode($xmlString);
$byte_array = unpack('C*', $utf8_data);
$bytesStr = implode("", $byte_array);
$data = file_get_contents('/path/to/abc.pfx');
$certPassword = 'certpassword';
openssl_pkcs12_read($data, $certs, $certPassword);
$private_key = $certs['pkey'];
$binary_signature = "";
openssl_sign($bytesStr, $binary_signature, $private_key, OPENSSL_ALGO_SHA1);
Now my question is my above code correct ? Also how openssl_sign give me signature + sample request data,
As i am not able to generate same encoded string which service provider has provided into their sample,
I have some data that I'm signing on iOS with SecKeyRawSign using Elliptic Curve private key. However, verifying that data in Java using Signature.verify() returns false
The data is a random 64 bit integer, split into bytes like so
uint64_t nonce = (some 64 bit integer)
NSData *nonceData = [NSData dataWithBytes: &nonce length: sizeof(nonce)];
From that data I'm creating a SHA256 digest
int digestLength = CC_SHA256_DIGEST_LENGTH;
uint8_t *digest = malloc(digestLength);
CC_SHA256(nonceData.bytes, (CC_LONG)nonceData.length, digest);
NSData *digestData = [NSData dataWithBytes:digest length:digestLength];
and then signing it with private key
size_t signedBufferSize = kMaxCipherBufferSize;
uint8_t *signedBuffer = malloc(kMaxCipherBufferSize);
OSStatus status = SecKeyRawSign(privateKeyRef,
kSecPaddingPKCS1SHA256,
(const uint8_t *)digestData.bytes,
digestData.length,
&signedBuffer[0],
&signedBufferSize);
NSData *signedData = nil;
if (status == errSecSuccess) {
signedData = [NSData dataWithBytes:signedBuffer length:signedBufferSize];
}
Everything appears to work fine.
Then, in Java server, I'm trying to verify that signed data
PublicKey publicKey = (a public key sent from iOS, X509 encoded)
Long nonce = (64 bit integer sent from iOS)
String signedNonce = (base64 encoded signed data)
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(nonce);
byte[] nonceBytes = buffer.array();
byte[] signedNonceBytes = Base64.getDecoder().decode(signedNonce.getBytes());
Signature signer = Signature.getInstance( "SHA256withECDSA" );
signer.initVerify( publicKey );
signer.update( nonceBytes );
Boolean isVerified = signer.verify( signedNonceBytes );
At this point, signer.verify() returns false
I also tried to sign plain data, instead of SHA256 digest, but that doesn't work either.
What am I missing? Am I signing the data correctly? Am I using correct padding? Is there something else to be done with data to be able to verify it with SHA256withECDSA algorithm?
The byte ordering does not match:
iOS is little endian. The way you create nonceData, this order is retained.
On the Java side, ByteBuffer defaults to big endian, independent of the underlying operating system / hardware.
So you need to change the byte order:
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putLong(nonce);
I'm a java guy, so I can't say anything about the iOS side, but a quick check of the java side can be done using the commented assumptions:
// Generate a new random EC keypair for testing
KeyPair keys = KeyPairGenerator.getInstance("EC").generateKeyPair();
PrivateKey privateKey = keys.getPrivate();
PublicKey publicKey = keys.getPublic();
// Generate a Random nonce to test with
byte[] nonceBytes = new byte[8]; // (some 64 bit integer)
new Random(System.nanoTime()).nextBytes(nonceBytes);
// sign
Signature sign = Signature.getInstance("SHA256withECDSA");
sign.initSign(privateKey);
sign.update(nonceBytes);
byte[] signature = sign.sign();
//verify
Signature verify = Signature.getInstance("SHA256withECDSA");
verify.initVerify(publicKey);
verify.update(nonceBytes);
Boolean isVerified = verify.verify(signature);
// print results
System.out.println("nonce used ::" + Base64.getEncoder().encodeToString(nonceBytes));
System.out.println("Signed nonce ::" + Base64.getEncoder().encodeToString(signature));
System.out.println("nonce used ::" + isVerified);
As you'd expect returns, the code above will always return that the signature is verified. Check your assumptions are accurate and validate the keys being used are correct on both sides.
I can advice you to use some Crypto Library which is available for both iOS and JAVA sides (f.e.:https://github.com/VirgilSecurity/virgil-crypto). This will ensure that the algorithm and block types (etc.) are the same in both cases and you won't need to worry about it anymore. I believe you will finds many crypto libraries on a GitHub.
In getBytes() you could specify the encoding technique using java.nio.charset.StandardCharsets. and do the same with the decoder.
https://docs.oracle.com/javase/7/docs/api/java/nio/charset/StandardCharsets.html
I need to generate public/private key for RSA algorithm on IOS device and send public key to server with encrypted text. Server must read public key and decrypt user message.
I have code on swift:
func generateKeys(){
var publicKey: SecKey?
var privateKey: SecKey?
let publicKeyAttr: [NSObject: NSObject] = [kSecAttrIsPermanent:true as NSObject, kSecAttrApplicationTag:"publicTag".data(using: String.Encoding.utf8)! as NSObject]
let privateKeyAttr: [NSObject: NSObject] = [kSecAttrIsPermanent:true as NSObject, kSecAttrApplicationTag:"privateTag".data(using: String.Encoding.utf8)! as NSObject]
var keyPairAttr = [NSObject: NSObject]()
keyPairAttr[kSecAttrKeyType] = kSecAttrKeyTypeRSA
keyPairAttr[kSecAttrKeySizeInBits] = 4096 as NSObject
keyPairAttr[kSecPublicKeyAttrs] = publicKeyAttr as NSObject
keyPairAttr[kSecPrivateKeyAttrs] = privateKeyAttr as NSObject
_ = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
var error:Unmanaged<CFError>?
if #available(iOS 10.0, *) {
if let cfdata = SecKeyCopyExternalRepresentation(publicKey!, &error) {
let data:Data = cfdata as Data
let b64Key = data.base64EncodedString(options: .lineLength64Characters)
print("public base 64 : \n\(b64Key)")
}
if let cfdata = SecKeyCopyExternalRepresentation(privateKey!, &error) {
let data:Data = cfdata as Data
let b64Key = data.base64EncodedString(options: .lineLength64Characters)
print("private base 64 : \n\(b64Key)")
}
}
let encrypted = encryptBase64(text: "test", key: publicKey!)
let decrypted = decpryptBase64(encrpted: encrypted, key: privateKey!)
print("decrypted \(String(describing: decrypted))")
self.dismiss(animated: true, completion: nil);
}
func encryptBase64(text: String, key: SecKey) -> String {
let plainBuffer = [UInt8](text.utf8)
var cipherBufferSize : Int = Int(SecKeyGetBlockSize(key))
var cipherBuffer = [UInt8](repeating:0, count:Int(cipherBufferSize))
// Encrypto should less than key length
let status = SecKeyEncrypt(key, SecPadding.PKCS1, plainBuffer, plainBuffer.count, &cipherBuffer, &cipherBufferSize)
if (status != errSecSuccess) {
print("Failed Encryption")
}
let mudata = NSData(bytes: &cipherBuffer, length: cipherBufferSize)
return mudata.base64EncodedString()
}
func decpryptBase64(encrpted: String, key: SecKey) -> String? {
let data : NSData = NSData(base64Encoded: encrpted, options: .ignoreUnknownCharacters)!
let count = data.length / MemoryLayout<UInt8>.size
var array = [UInt8](repeating: 0, count: count)
data.getBytes(&array, length:count * MemoryLayout<UInt8>.size)
var plaintextBufferSize = Int(SecKeyGetBlockSize(key))
var plaintextBuffer = [UInt8](repeating:0, count:Int(plaintextBufferSize))
let status = SecKeyDecrypt(key, SecPadding.PKCS1, array, plaintextBufferSize, &plaintextBuffer, &plaintextBufferSize)
if (status != errSecSuccess) {
print("Failed Decrypt")
return nil
}
return NSString(bytes: &plaintextBuffer, length: plaintextBufferSize, encoding: String.Encoding.utf8.rawValue)! as String
}
This code returns public key in PKCS1. I found the library: SwCrypt
This code helps me to convert PKCS1 into PKCS8 and read public key with java
SwKeyConvert.PublicKey.pemToPKCS1DER(publicKeyPEM)
But I can't decrypt user message. Can you help me with message decryption? I wrote small unit test.
import org.junit.Test;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import static org.junit.Assert.assertNotNull;
public class TestExample {
String publicKeyContent = "MIMAAiMwDQYJKoZIhvcNAQEBBQADgwACDwAwggIKAoICAQC4K4zr1jTi4SSypXbrNeGd2HbYlrDRIPsPcL5a4JwGUKXwi+Rpf8Xh0D4dcRRH+Rtd5F66aqdGnhCBKtU5XsmlT+QssIggihI0iF3LEPsMlKapDrDdSbWmuitVDSSlulReMcN3hEUl8AzlNyu817snZtYESiFxm87QV6xZAcrWzvIdyiStBbngCT/v76tOZDX56IIRGoLMi3WND7538PqqYheh2+oZk05O+Bf5LZc6YteTRLLOSyIIxesoABo8tvaFyIo2ihMcnDRnGAzOMNTLXiQdj2scAMCVr3oiLpU48+Iw8ptOUBDQioW15FsYd3ugZhUX+/mFtMFsYkJyYjyG5HCqAs2/wm6eIjjy1QQwUF2hB8Z7sqyF5KrVZOv6Q7+pB83tT02ZXcDXCdsiP10G3sA4kjc/r9TuQHjCIwZa1LO4tPaO8qAzlROHIkQ4FhdaAM9U9DUq3nBywQLcEVQmXeH1OA1ve96QbMQoN+SRPh0Kq6W0U4TbzvMskQ7bePKDjiWP2fdtgSfrnOsyJaLi04n+hDsgiMfd4N9tauSMpCY6H9l7yYPc5Z+3qG2ANhteZGa7wT1OZoGLkZV0OurnA4xkzwcB7h0RVEvABB9dtl6S60FK1NELQy6sC/HCcivo9sJ+C1g2Sln+8qEdiju86X5ja5pGiRhJAxwSp2ZKgwIDAQAB";
String encryptedMessage = "g81SOC9XOD9zq5qfyhkdP/7ronNb82g3ueDtEh711L43zPSgrFksLEdIud/1fiDcV6N97RD41vb/iXtCg2/Gu6XliEhCaoG28reetG1cBndKF9UzQw9cYChp54S1wnhBkAAZQ4Of3c77DtPBCL4gcgv2ilBTm7o+NR2wXunfJ7Olbbau+7C1pa+Qv/+sz45r4gJmQ1MfGjHtw9e/U/3vjL9BfCEPn9Mo2zAZhkI81S0Ewth+csHwb3YTlE8mtHni1fvLRVXjvHk+57U3keoYPZk+93ytFL6pqkWMk+9VbLuUFHXn1mpSMiEr9GRN6XKRvEbbPp5lI9WjwRvtWfmRm5gLY76QinTrPb0KJg7oWmEoQie5o9W6MOkD+8vYV/SkkLT855SB3O57QLKCZmlSPlccE6GWfglHhAwRwrcTDY1bO/xH38gvYYPaAJMtJKtOVrqGxNkIUPwCCkdBa9JQwDSyTYxeh8AxC0ACs9cYVjMPrmC9zIZuRbmcneIGSugtzMZmI9qbLtW1aMlWuGrVyVhJlcCZuTJXWyBgx8xj8coX9YwUXSi1A4dL/Hl5Sme+HhAQs7OcH6ZZpsPmIIozXxHgOMhUo8k++cWg6+pudSoB2tr4NhxX/ID2jd1ELsg1C6mbxaKaGgXwfU9w4ZngbRxGTBlKWXwUP/xBa5BARZ4=";
#Test
public void encryptTest() throws Exception {
PublicKey publicKey = convertPublicKey(publicKeyContent);
assertNotNull(publicKey);
String s = decryptString(publicKey, encryptedMessage);
assertNotNull(s);
}
private PublicKey convertPublicKey(String publicKey) throws RSAAlgorithmException {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//generate public key
byte[] publicBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
return keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RSAAlgorithmException("Unable to generate public key from string " + publicKey + " . " + e.getMessage());
}
}
private String decryptString(PublicKey publicKey, String value) throws Exception {
byte[] decodedBytes;
try {
Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding");
c.init(Cipher.DECRYPT_MODE, publicKey);
decodedBytes = c.doFinal(value.getBytes());
} catch (Exception e) {
System.out.println("Error = " + e);
throw new Exception(e);
}
return new String(decodedBytes);
}
}
I have next error:
java.lang.Exception: javax.crypto.IllegalBlockSizeException: Data must not be longer than 512 bytes
In an asymmetric cryptosystem, you have a key pair consisting of both a public and a private key.
You encrypt with the public key and you decrypt with the private key. The public key can be shared (publicly) with other parties, enabling them to send you encrypted messages. The private key is kept secret so that only you can decrypt messages encrypted with your public key.
You normally don't encrypt messages directly with RSA, since the message has to be shorter than the modulus and it might have security implications. What you do instead is, you generate a random key for a symmetric encryption scheme, for example AES-256-CTR (or AES-256-GCM if you need authentication in addition to secrecy), encrypt the message with the symmetric encryption scheme, encrypt the key for the symmetric cipher with the asymmetric encryption scheme and send both the (asymmetrically) encrypted key and the (symmetrically) encrypted message to the receiver.
The receiver will first use his/her private key to decrypt the key for the symmetric encryption scheme, then use that to decrypt the actual message. This is sometimes referred to as "hybrid encryption" and it enables the message to be (more or less) arbitrarily long.
So, what you have to do is the following.
You have to generate a key pair for the receiver of the encrypted message. Therefore, if your communication is one-way (iOS device sends data to server, but no data ever comes back), you need to generate a key pair for your server only. If your server needs to talk back, you need to generate a key pair for your client as well.
In order to send an encrypted message to the server, the client needs to have the public key of your server. Therefore, you have to somehow transfer it there. The problem is that this transfer needs to be secure, otherwise an attacker may impersonate the server, present you his/her public key instead (for which he/she knows the private counterpart), intercept all traffic, decrypt it with his/her private key, re-encrypt it with the server's public key and pass it on to the server. This is called a man in the middle attack and enables the attacker to intercept (and possibly manipulate) all communication between you and the server. Therefore, your best choice might be not to exchange public keys at all but rather to embed them into the application. This will prevent man in the middle attacks, as long as the application code can be shared by an authenticated means.
When you want to send a message to the server, generate a random symmetric encryption key (with a cryptographically secure random number generator - this is not your language's default "random" function), encrypt the message with it and an appropriate symmetric encryption scheme, which you choose according to your requirements (e. g. authentication required? then use AES-GCM - only secrecy required? then use AES-CTR). Most encryption schemes also require a random (unpredictable) initialization vector which you also generate with a CSPRNG and have to send along to the receiver since it's required for decryption, but needs not be kept secret.
Encrypt the key for the symmetric encryption scheme with an asymmetric encryption scheme and the server's public key. RSA-PKCS1 is "dated". I'd try to use RSA-OAEP instead since it has more desirable security properties. Send the encrypted key to the server.
The server decrypts the key for the symmetric encryption scheme with the asymmetric encryption scheme and his private key (which is kept secret). Then it decrypts the message with the symmetric encryption scheme.
Since most of this is complicated and a lot of subtle details can lead to security breaches, I'd suggest you do not implement this yourself. I'd suggest you just use TLS (possibly with a restricted parameter set) and implement your own certificate validator where you compare the server's public key to a known-good value to get rid of the entire PKI stuff, which costs money and also is not very secure in the first place. At least, that's how I would do it.
Alternatively, if you want to roll out your own, "proprietary" protocol, you can try to use one of the more "developer friendly" cryptographic libraries, especially NaCl. This abstracts away a lot of the "gory details" and chooses lots of sane defaults for you, which cannot be overridden, all of which makes it a lot harder to implement insecure protocols.
Keep in mind this is not to say you're "too dumb". It's just the proper way of doing these things. When it comes to crypto, the less "DIY", the better. The more widespread the crypto is, that you use, the more it gets reviewed and the quicker flaws will get fixed, so using something like NaCl, which is used in thousands of applications, is pretty neat. As long as other NaCl applications are secure, your application is (probably) secure as well. When a breach is found, NaCl will get updated, you just update the library in your application and are automatically safe, so you're left with (almost) no need for internal review and patching and your windows of vulnerability will (usually) be short.
Some background of what I'm trying to accomplish.
Part 1.
PHP server communicates with a Java-based device. PHP uses OpenSSL to generate a public/private keypair, then sends the public key to the device which in turn gives back an encrypted macKey (generated using the public key), encoded in base64. PHP now needs to base64-decode and decrypt the macKey using the private key.
What is the equivalent of the below Java code snippet in PHP?
String base64EncodedMacKey = "LkvTT9LFj5lcxRRB8KrwwN906fSIDDcJvQK3E7a5PbR+Ox9WnslOs32jSCC9FkE8ouvr2MfWwtppuZmoPjaxwg3yAQI4UN3T1loISuF2VwKWfJ45fywbK9bNnD5Cw7336mjoGctv77Tg3JXPrsRwgMGIlBsNwdt1B0wgT4MMMAjl32TnBI3iwQ94VTMHffrK+QToddTahRHHoVsr3FVrETdiqKXdkiX1jES53im5lrXYIsY89UFkGzPo+3u4ijKIQWSLvYnA5wXI128gFHKxKYS82MbJDUn9i1RVFsGaP6T3nQRSX5SZNpSe5yGFWwMgYOx0KXMgET82FeaL2hfWuw==";
byte[] base64DecodedMacKey = DatatypeConverter.parseBase64Binary(base64EncodedMacKey);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, keypair.getPrivate());
byte[] macKey = cipher.doFinal(base64DecodedMacKey);
Here's what I attempted in PHP, however I'm confused about using byte array versus string when decrypting the macKey
$macKey = 'LkvTT9LFj5lcxRRB8KrwwN906fSIDDcJvQK3E7a5PbR+Ox9WnslOs32jSCC9FkE8ouvr2MfWwtppuZmoPjaxwg3yAQI4UN3T1loISuF2VwKWfJ45fywbK9bNnD5Cw7336mjoGctv77Tg3JXPrsRwgMGIlBsNwdt1B0wgT4MMMAjl32TnBI3iwQ94VTMHffrK+QToddTahRHHoVsr3FVrETdiqKXdkiX1jES53im5lrXYIsY89UFkGzPo+3u4ijKIQWSLvYnA5wXI128gFHKxKYS82MbJDUn9i1RVFsGaP6T3nQRSX5SZNpSe5yGFWwMgYOx0KXMgET82FeaL2hfWuw==';
$base64DecodedMacKey = base64_decode($macKey);
openssl_private_decrypt($base64DecodedMacKey, $decrypted, $privateKey);
The $decrypted above holds some binary data as it appears, so I'm unsure whether I need to convert it into a byte array or treat it as a string...
Part 2.
Each request has a counter. The macKey in Java code above is used to create a MAC value out of the counter.
What is the equivalent of the below Java code snippet in PHP?
int counter = 0;
String nextCounter = String.valueOf(++counter);
SecretKeySpec signingKey = new SecretKeySpec(macKey, "AES");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
byte[] counterMac = mac.doFinal(nextCounter.getBytes("UTF-8"));
String base64EncodedMac = DatatypeConverter.printBase64Binary(counterMac);
The base64EncodedMac above is finally sent to the device to validate communication.
I've tried googling different solutions, however I've not been successful in generating a valid base64EncodedMac string in PHP for the device to approve it.
Found the solution myself. For Part 1, I chose to use phpseclib to generate the public/private keys and to specify the encryption algorithm. Decrypting macKey:
$rsa = new Crypt_RSA();
$keys = $rsa->createKey(2048);
// [...]
$macKey = base64_decode($base64EncodedMacKey);
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$rsa->loadKey($keys['privatekey']);
$decryptedMac = $rsa->decrypt($macKey);
Followed by Part 2:
$counter = 0;
$hmac = hash_hmac('sha256', ++$counter, $decryptedMac, true);
$counterMac = base64_encode($hmac);
The main confusing part was that in Java, HMAC was done out of byte array, while in PHP the hash_hmac function expects a String as its 2nd parameter, so using unpack() was not sufficient. However, it seems to have worked with passing the $counter directly. It was also important to use the 4th parameter as TRUE to return raw data.
An Android client (4.2.1) application sends a public key via a HttpPost request to a PHP (5.6) API. This API encrypts the data with AES compliant RIJNDAEL_128, then encrypts the key for the AES encryption with the client public key with OpenSSL public encryption and RSA_PKCS1_OAEP_PADDING. It sends this data base64 encoded via XML back to the client android application which shall encrypt the data. I've setup a basic PHP test script which tests the whole process, this works as expected.
Currently I'm working on implementing the decryption in the client Android application but already decrypting the AES-key fails. I have other questions besides this current problem (see at the end).
Here is a text graphical synopsis of what is happening:
client -> public key -> API -> data -> AESencrypt(data), RSAencrypt(AES-key) -> base64encode[AES(data)], base64encode[RSA(AES-key)] -> <xml>base64[AES(data)], base64[RSA(AES-key)]</xml> -> client -> base64[AES(data)], base64[RSA(AES-key)] -> base64decode[AES(data)], base64decode[RSA(AES-key)] -> AESdecrypt(data), RSAdecrypt(AES-key) -> data
I'm encrypting the data with MCRYPT_RIJNDAEL_128 which I read is AES compatible (see PHP doc for mycrypt).
Here is the code:
<?php
$randomBytes = openssl_random_pseudo_bytes(32, $safe);
$randomKey = bin2hex($randomBytes);
$randomKeyPacked = pack('H*', $randomKey);
// test with fixed key:
// $randomKeyPacked = "12345678901234567890123456789012";
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$dataCrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $randomKeyPacked, $data, MCRYPT_MODE_CBC, $iv);
The AES-key coming out of this is encoded with openssl_public_encrypt and the padding setting OPENSSL_PKCS1_OAEP_PADDING. Reading the source code (source of PHP OpenSSL implementation) this is equivalent to RSA_PKCS1_OAEP_PADDING described as
EME-OAEP as defined in PKCS #1 v2.0 with SHA-1, MGF1 and an empty encoding parameter.
in the OpenSSL documentation found here. Afterwards I base64_encode the data to be able to transfer it via an XML string to the client. The code looks like this:
openssl_public_encrypt($randomKeyPacked, $cryptionKeyCrypted, $clientPublicKey, OPENSSL_PKCS1_OAEP_PADDING);
$content = array(
'cryptionKeyCryptedBase64' => base64_encode($cryptionKeyCrypted),
'cryptionIVBase64' => base64_encode($iv),
'dataCryptedBase64' => base64_encode($dataCrypted)
);
// $content gets parsed to a valid xml element here
The client Android application gets the return data via HttpPost request via a BasicResponseHandler. This returned XML string is valid and parsed via Simple to respective java objects. In the the class holding the actual content of the transferred data I currently try to decrypt the data. I decrypt the AES-key with the transformation RSA/ECB/OAEPWithSHA-1AndMGF1Padding which due to this site (only I could find) is a valid string and seems to be the equivalent of the padding I used in PHP. I included the way I generated the private key as it is the same way I generate the public key that was send to the PHP API. Here is that class:
public class Content {
#Element
private String cryptionKeyCryptedBase64;
#Element
private String cryptionIVBase64;
#Element
private String dataCryptedBase64;
#SuppressLint("TrulyRandom")
public String getData() {
String dataDecrypted = null;
try {
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PrivateKey privateKey = keypair.getPrivate();
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.DEFAULT);
//byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.DEFAULT);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipherRSA.init(Cipher.DECRYPT_MODE, privateKey);
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.DEFAULT);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return dataDecrypted;
}
}
Doing this I currently fail at line
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
with Bad padding exceptions for nearly all PHP openssl_public_encrypt padding parameter - Android Cipher transformation string combinations I tried. Using the standard PHP padding parameter by omitting the padding parameter in the openssl_public_encrypt which defaults to OPENSSL_PKCS1_PADDING and a Cipher transformation string of just Cipher.getInstance("RSA") I do not get a bad padding exception. But the encrypted key seems not to be valid as AES decryption fails with
java.security.InvalidKeyException: Key length not 128/192/256 bits.
I tried validating this with a fixed key (see code comment in PHP code above) and I don't get the same key back after decrypting it and transforming it to a string. It seems it is just garbled data although it is 256 bits long if I read the Eclipse ADT debugger correctly.
What might be the correct Cipher transformation string to use as an equivalent for PHP's OPENSSL_PKCS1_OAEP_PADDING. Reading this documentation I need the transformation string in the form "algorithm/mode/padding", I guessed that algorithm = RSA but I couldn't find out how to translate what the OpenSSL (above) documentation states about the padding into a valid cipher transformation string. I.e. what is mode for example?
Unfortunately this Android RSA decryption (fails) / server-side encryption (openssl_public_encrypt) accepted answer did not solve my problem.
Anyway might this solve my problem or does my problem originate elsewhere?
How would I further debug this? What is the correct way to transform the base64 decoded, decrypted key into a human readable form so I can compare it with the key used to encrypt?
I tried with:
String keyString = new String(keyBytes, "UTF-8");
But this doesn't give any human readable text back so I assume either the key is wrong or my method of transforming it.
Also decrypting the AES encrypted data in PHP the IV is needed in the decryption function mcrypt_decrypt. As you can see in the code I send it but it seems in Android this is not needed? Why so?
PS: I hope I provided all needed information, I can add further in the comments.
PPS: For completeness here is the Android client code making the HttpPost request:
#SuppressLint("TrulyRandom")
protected String doInBackground(URI... urls) {
try {
System.setProperty("jsse.enableSNIExtension", "false");
HttpClient httpClient = createHttpClient();
HttpPost httpPost = new HttpPost(urls[0]);
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PublicKey publickey = keypair.getPublic();
byte[] publicKeyBytes = publickey.getEncoded();
String pubkeystr = "-----BEGIN PUBLIC KEY-----\n"+Base64.encodeToString(publicKeyBytes,
Base64.DEFAULT)+"-----END PUBLIC KEY-----";
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("publickey", pubkeystr));
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
// Execute HTTP Post Request
HttpResponse response = httpClient.execute(httpPost);
return new BasicResponseHandler().handleResponse(response);
} catch (Exception e) {
Toast toast = Toast.makeText(asyncResult.getContext(),
"unknown exception occured: " + e.getMessage(),
Toast.LENGTH_SHORT);
toast.show();
return "error";
}
}
You are generating one RSA keypair in doInBackground and telling the host to use the public half of that keypair to encrypt the DEK (data encryption key). You are then generating a completely different RSA keypair in getData and attempting to use the private half of that keypair to decrypt the encrypted DEK. The way public-key encryption works is you encrypt with the public half of a keypair and decrypt with the private half of the same keypair; the public and private halves are mathematically related. You need to save and use at least the private half of the keypair (optionally the keypair with both halves) whose public half you send.
Once you've got the DEK correctly, in order to decrypt CBC-mode data, yes you do need to use the same IV for decryption as was used for encryption. Your receiver needs to put it in an IvParameterSpec and pass that on the Cipher.init(direction,key[,params]) call. Alternatively if you can change the PHP, since you are using a new DEK for each message it is safe to use a fixed IV; easiest is to encrypt with '\0'x16 and allow the Java decrypt to default to all-zero.
Additionally you need to set Base64.decode with the parameter Base64.NO_WRAPas PHP will just put out the base64 delimited by \0. And to that you will also need to use the "AES/CBC/ZeroBytePadding" transformation cipher to decrypt the AES data as the PHP function mycrypt_encrypt will pad the data with zeros.
Here is what the getData function will have to look like:
public String getData() {
String dataDecrypted = null;
try {
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.NO_WRAP);
byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.NO_WRAP);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
// get private key from the pair used to grab the public key to send to the api
cipherRSA.init(Cipher.DECRYPT_MODE, rsaKeyPair.getPrivateKey());
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.NO_WRAP);
IvParameterSpec ivSpec = new IvParameterSpec(cryptionIV);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES/CBC/ZeroBytePadding");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return dataDecrypted;
}