I'm using a private RSA key to encrypt a random AES key with the default Java RSA implementation:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] cipherBytes = cipher.doFinal(plainText.getBytes());
Since we need a public key anyway, this is a convenient method to disguise the key and make sure it had been encrypted with our private key. The decryption is done similarly:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] plainBytes = cipher.doFinal(cipherBytes);
This works fine with Oracle's JDK, but with IBM's this fails because IBM thinks using the private key for encryption is not a valid use case. Unfortunately, I have to support both JDKs, so I'm trying to re-implement the RSA decryption myself.
This is the code I have so far:
BigInteger big = new BigInteger(cipherBytes);
big = big.modPow(pub.getPublicExponent(), pub.getModulus());
System.out.println(new String(big.toByteArray()));
It almost works, but there seems to be a padding issue. Most of the time I'm getting the original text with a string of dot-like symbols in front of it, but sometimes it's only random bytes.
Unfortunately, I wasn't able to figure out which padding scheme is used by default. Does anyone know what's missing in my code or can at least give a hint with which algorithm the padding is handled?
Here is an example of input and output values, as requested. I have used 512 bit keys to avoid too huge numbers.
Public modulus : 8117919732251191237549784557538073836207094968952416063837701691514861428726690140363567956265691836505266266364256892197254736023284927189008247933889303
Public exponent: 65537
Plaintext: teststring
Plaintext as BN: 549665952565679142563431
Ciphertext as BN: 6304229782339071167863563708554898540621778162930150363326921290545577949349781053660336996882823758722402137580193903457839924005473545992074817339077456
"Decrypted" BN: 409173825987017733751648712103449894027080255755383098685411421012016724550584319360408761540738019643860835515945008876151848132891805352276483731047
Resultstring: ˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇˇteststring
To address the discussion why am I doing this:
The public key is hard-coded into my software. I use the private key to encrypt another key for AES. Therefore, to actually decode anything with AES, you need the AES key first. To get this key, you have to decrypt it with the public key first. Since the public key cannot be modified without serious manipulation, only AES keys encrypted with the private key work. You may extract the public key somehow and decrypt the AES key, but that's elaborate and only gets you the AES key to decrypt the secured content. There is also a signature calculated with the private key, which is verified with the public key as well. So manipulations aren't possible.
So yes, technically the signature is sufficient, because there are methods to read the content. But those are elaborate and I don't mind if anyone really takes all the trouble, but I don't want to make things easy.
Public keys are for encrypting and verifying signatures. Private keys are for decrypting and signing. Public keys are intended to be just that: public. If you're doing things right, there should be no reason to hide a public key.
What you are trying to do looks more like signing rather than encryption. Use a separate key pair for signing, as it is not exactly the same as encrypting.
Ok, I've figured it out by reading the RSA spec. To add more security, a padding is added before encryption and the following "string" is created:
0x00 + BT + Padding + 0x00 + Data
The block type (BT) indicates the kind of padding. With BT = 0x01 the padding is 0xff and with BT = 0x02 the padding is random but non-zero. The concatenated string is then encrypted.
When decrypting, the format can be verified, but to just read the data, the leading bytes have to be removed. They are all non-zero until the 0x00 just before the data. Therefore, everything until and including the 0x00 after the padding can be removed. What's left is the message.
This code works now:
// Decrypt
byte[] decryptedBytes = (new BigInteger(1, cipherBytes)).modPow(pub.getPublicExponent(), pub.getModulus()).toByteArray();
// Extract msg
int dataStart;
for (dataStart = 0; decryptedBytes[msgStart] != 0; dataStart++);
dataStart++;
byte finalBytes[] = new byte[decryptedBytes.length - msgStart];
System.arraycopy(decryptedBytes, msgStart, finalBytes, 0, finalBytes.length);
This also explains the string of "^" in my previous attempts. Those were the padding bytes, which are 0xff with BT = 0x01.
I only needed decryption, but for the sake of completeness, this is the code for encryption:
int bitLength = 512;
String plainText = "teststring";
// Convert to bytes
byte plainBytes[] = plainText.getBytes();
byte encryptionBytes[] = new byte[bitLength / 8];
encryptionBytes[0] = 0; // Leading 0
encryptionBytes[1] = 1; // Block type
// Padding String
int paddingEnd = (bitLength / 8) - plainBytes.length - 2;
for (int i = 2; i < paddingEnd; i++) {
encryptionBytes[i] = (byte) 0xff;
}
encryptionBytes[paddingEnd + 1] = 0;
// Actual data
System.arraycopy(plainBytes, 0, encryptionBytes, paddingEnd + 1, plainBytes.length);
// Encrypt
byte[] cipherBytes = (new BigInteger(1, encryptionBytes)).modPow(priv.getPrivateExponent(), priv.getModulus()).toByteArray();
Hope this helps anyone :)
Related
I've created an RSA Key Pair. Now, I'm trying to encrypt the private key with a DES algorithm, format it to PKCS#5 and print it on the console. Unfortunately, the generated private key does not work. When I try to use it, after entering the right passphrase, the ssh client returns the passphrase is not valid:
Load key "test.key": incorrect passphrase supplied to decrypt private key
Could please someone tells me where I'm wrong?
This is the code:
private byte[] iv;
public void generate() throws Exception {
RSAKeyPairGenerator generator = new RSAKeyPairGenerator();
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
String passphrase = "passphrase";
byte[] encryptedData = encrypt(keyPair.getPrivate().getEncoded(), passphrase);
System.out.println(getPrivateKeyPem(Base64.encodeBase64String(encryptedData)));
}
private byte[] encrypt(byte[] data, String passphrase) throws Exception {
String algorithm = "PBEWithMD5AndDES";
salt = new byte[8];
int iterations = 1024;
// Create a key from the supplied passphrase.
KeySpec ks = new PBEKeySpec(passphrase.toCharArray());
SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm);
SecretKey key = skf.generateSecret(ks);
// Create the salt from eight bytes of the digest of P || M.
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(passphrase.getBytes());
md.update(data);
byte[] digest = md.digest();
System.arraycopy(digest, 0, salt, 0, 8);
AlgorithmParameterSpec aps = new PBEParameterSpec(salt, iterations);
Cipher cipher = Cipher.getInstance(AlgorithmID.pbeWithSHAAnd3_KeyTripleDES_CBC.getJcaStandardName());
cipher.init(Cipher.ENCRYPT_MODE, key, aps);
iv = cipher.getIV();
byte[] output = cipher.doFinal(data);
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(salt);
out.write(output);
out.close();
return out.toByteArray();
}
private String getPrivateKeyPem(String privateKey) throws Exception {
StringBuffer formatted = new StringBuffer();
formatted.append("-----BEGIN RSA PRIVATE KEY----- " + LINE_SEPARATOR);
formatted.append("Proc-Type: 4,ENCRYPTED" + LINE_SEPARATOR);
formatted.append("DEK-Info: DES-EDE3-CBC,");
formatted.append(bytesToHex(iv));
formatted.append(LINE_SEPARATOR);
formatted.append(LINE_SEPARATOR);
Arrays.stream(privateKey.split("(?<=\\G.{64})")).forEach(line -> formatted.append(line + LINE_SEPARATOR));
formatted.append("-----END RSA PRIVATE KEY-----");
return formatted.toString();
}
private String bytesToHex(byte[] bytes) {
char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
And this is the generated private key in PKCS#5 PEM format:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,CA138D5D3C048EBD
+aZNZJKLvNtlmnkg+rFK6NFm45pQJNnJB9ddQ3Rc5Ak0C/Igm9EqHoOS+iy+PPjx
pEKbhc4Qe3U0GOT9L5oN7iaWL82gUznRLRyUXtOrGcpE7TyrE+rydD9BsslJPCe+
y7a9LnSNZuJpJPnJCeKwzy5FGVv2KmDzGTcs9IqCMKgV69qf83pOJU6Dk+bvh9YP
3I05FHeaQYQk8c3t3onfljVIaYOfbNYFLZgNgGtPzFD4OpuDypei/61i3DeXyFUA
SNSY5fPwp6iSeSKtwduSEJMX31TKSpqWeZmEmMNcnh8oZz2E0jRWkbkaFuZfNtqt
aVpLN49oRpbsij+i1+udyuIXdBGRYt9iDZKnw+LDjC3X9R2ceq4AOdfsmEVYbO1i
YNms9eXSkANuchiI2YqkKsCwqI5S8S/2Xj76zf+pCDhCTYGV3RygkN6imX/Qg2eF
LOricZZTF/YPcKnggqNrZy4KSUzAgZ9NhzWCWOCiGFcQLYIo+qDoJ8t4FwxQYhx9
7ckzXML0n0q5ba5pGekLbBUJ9/TdtnqfqmYrHX+4OlrR7XAu478v2QH6/QtNKdZf
VRTqmKKH0n8JL9AgaXWipQstW5ERNZJ9YPBASQzewVNLv4gRZRTw8bYcU/hiPbWp
eqULYYI9324RzY3UTsz3N9X+zQsT02zNdxud7XmmoHL493yyvqT9ERmF4uckGYei
HZ16KFeKQXE9z+x0WNFAKX3nbttVlN5O7TAmUolFTwu11UDsJEjrYMZRwjheAZyD
UnV1LwhFT+QA0r68Mto3poxpAawCJqPP50V4jbhsOb0J7sxT8fo2mBVSxTdb9+t1
lG++x/gHcK51ApK1tF1FhRRKdtOzSib376Kmt23q0jVDNVyy09ys+8LRElOAY1Es
LIuMMM3F7l+F4+knKh3/IkPZwRIz3f9fpsVYIePPS1bUdagzNoMqUkTwzmq6vmUP
C5QvN6Z5ukVCObK+T8C4rya8KQ/2kwoSCRDIX6Mzpnqx6SoO4mvtBHvPcICGdOD6
aX/SbLd9J2lenTxnaAvxWW0jkF6q9x9AAIDdXTd9B5LnOG0Nq+zI+6THL+YpBCB9
6oMO4YChFNoEx0HZVdOc8E7xvXU2NqinmRnyh7hCR5KNfzsNdxg1d8ly67gdZQ1Q
bk1HPKvr6T568Ztapz1J/O6YWRIHdrGyA6liOKdArhhSI9xdk3H3JFNiuH+qkSCB
0mBYdS0BVRVdKbKcrk4WRHZxHsDsQn1/bPxok4dCG/dGO/gT0QlxV+hOV8h/4dJO
mcUvzdW4I8XKrX5KlTGNusVRiFX3Cy8FFZQtSxdWzr6XR6u0bUKS+KjDl1KoFxPH
GwYSTkJVE+fbjsSisQwXjWnwGGkNDuQ1IIMJOAHMK4Mly1jMdFF938WNY7NS4bIb
IXXkRdwxhdkRDiENSMXY8YeCNBJMjqdXZtR4cwGEXO+G+fpT5+ZrfPbQYO+0E0r4
wGPKlrpeeR74ALiaUemUYVIdw0ezlGvdhul2KZx4L82NpI6/JQ7shq9/BEW2dWhN
aDuWri2obsNL3kk2VBWPNiE6Rn/HtjwKn7ioWZ3IIgOgyavcITPBe0FAjxmfRs5w
VWLFBXqcyV9cu1xS4GoCNLk0MrVziUCwHmwkLIzQZos=
-----END RSA PRIVATE KEY-----
Thanks in advance.
There is no such thing as PKCS#5 format. PKCS#5 primarily defines two password-based key derivation functions and password-based encryption schemes using them, plus a password-based MAC scheme, but does not define any format for the data. (It does define ASN.1 OIDs for these operations, and ASN.1 structures for their parameters -- primarily PBKDF2 and PBES2, because the only parameter for PBKDF1 and PBES1 is the salt.) PKCS#5 also defines a padding scheme for the CBC mode data encryption; this padding was slightly enhanced by PKCS#7 and used by many other applications which usually call it PKCS5 padding or PKCS7 padding. None of these are data formats, and none of them involves RSA (or other) private keys as such.
The file format you apparently want is the one used by OpenSSH (for a long time always, then for the last few years as the default, until OpenSSH 7.8 just a month ago made it optional) and as a result also used by other software that wants to be compatible or even interchangeable with OpenSSH. This format is actually defined by OpenSSL, which OpenSSH has long used for most of its cryptography. (Following Heartbleed, OpenSSH created a fork of OpenSSL called LibreSSL, which tries to be more robust and secure internally but intentionally maintains the same external interfaces and formats, and in any case hasn't been widely adopted.)
It is one of several 'PEM' formats defined by OpenSSL, and is mostly described on the man page for a number of 'PEM' routines including PEM_write[_bio]_RSAPrivateKey -- on your system if you have OpenSSL and it's not Windows, or on the web with the encryption part near the end in the section 'PEM ENCRYPTION FORMAT', and the EVP_BytesToKey routine it references similarly on its own man page. In short:
it does not use the pbeSHAwith3_keyTripleDES-CBC (meaning SHA1) scheme defined by PKCS#12/rfc7292 or the pbeMD5withDES-CBC scheme defined by PKCS#5/rfc2898 in PBES1. Instead it uses EVP_BytesToKey (which is partly based on PBKDF1) with md5 and 1 iteration, and salt equal to the IV, to derive the key, and then encrypts/decrypts with any supported symmetric cipher mode that uses an IV (thus not stream or ECB) but usually defaulting to DES-EDE3 (aka 3key-TripleDES) CBC as you ask for. Yes, EVP_BytesToKey with niter=1 is a poor PBKDF and makes these files insecure unless you use a very strong password; there are numerous Qs about that already.
And finally the plaintext of this file format is not the PKCS#8 (generic) encoding returned by [RSA]PrivateKey.getEncoded() but rather the RSA-only format defined by PKCS#1/rfc8017 et pred. And the empty line between the Proc-type and DEK-info headers and the base64 is required, and the line terminator on the dashes-END line may be needed depending on what software does the reading.
The easiest way to do this is to use software already compatible with OpenSSL private-key PEM format(s), including OpenSSL itself. Java can run an external program: OpenSSH's ssh-keygen if you have it, or openssl genrsa if you have that. The BouncyCastle bcpkix library supports this and other OpenSSL PEM formats. If 'ssh client' is jsch, that normally reads keyfiles in several formats including this one, but com.jcraft.jsch.KeyPairRSA actually supports generating a key and writing it in this PEM format as well. Puttygen also supports this format, but the other formats it can convert from and to aren't Java-friendly. I'm sure there are more.
But if you need to do it in your own code, here's how:
// given [RSA]PrivateKey privkey, get the PKCS1 part from the PKCS8 encoding
byte[] pk8 = privkey.getEncoded();
// this is wrong for RSA<=512 but those are totally insecure anyway
if( pk8[0]!=0x30 || pk8[1]!=(byte)0x82 ) throw new Exception();
if( 4 + (pk8[2]<<8 | (pk8[3]&0xFF)) != pk8.length ) throw new Exception();
if( pk8[4]!=2 || pk8[5]!=1 || pk8[6]!= 0 ) throw new Exception();
if( pk8[7] != 0x30 || pk8[8]==0 || pk8[8]>127 ) throw new Exception();
// could also check contents of the AlgId but that's more work
int i = 4 + 3 + 2 + pk8[8];
if( i + 4 > pk8.length || pk8[i]!=4 || pk8[i+1]!=(byte)0x82 ) throw new Exception();
byte[] old = Arrays.copyOfRange (pk8, i+4, pk8.length);
// OpenSSL-Legacy PEM encryption = 3keytdes-cbc using random iv
// key from EVP_BytesToKey(3keytdes.keylen=24,hash=md5,salt=iv,,iter=1,outkey,notiv)
byte[] passphrase = "passphrase".getBytes(); // charset doesn't matter for test value
byte[] iv = new byte[8]; new SecureRandom().nextBytes(iv); // maybe SIV instead?
MessageDigest pbh = MessageDigest.getInstance("MD5");
byte[] derive = new byte[32]; // round up to multiple of pbh.getDigestLength()=16
for(int off = 0; off < derive.length; off += 16 ){
if( off>0 ) pbh.update(derive,off-16,16);
pbh.update(passphrase); pbh.update(iv);
pbh.digest(derive, off, 16);
}
Cipher pbc = Cipher.getInstance("DESede/CBC/PKCS5Padding");
pbc.init (Cipher.ENCRYPT_MODE, new SecretKeySpec(derive,0,24,"DESede"), new IvParameterSpec(iv));
byte[] enc = pbc.doFinal(old);
// write to PEM format (substitute other file if desired)
System.out.println ("-----BEGIN RSA PRIVATE KEY-----");
System.out.println ("Proc-Type: 4,ENCRYPTED");
System.out.println ("DEK-Info: DES-EDE3-CBC," + DatatypeConverter.printHexBinary(iv));
System.out.println (); // empty line
String b64 = Base64.getEncoder().encodeToString(enc);
for( int off = 0; off < b64.length(); off += 64 )
System.out.println (b64.substring(off, off+64<b64.length()?off+64:b64.length()));
System.out.println ("-----END RSA PRIVATE KEY-----");
Finally, OpenSSL format requires the encryption IV and the PBKDF salt be the same, and it makes that value random, so I did also. The computed value you used for salt only, MD5(password||data), vaguely resembles the synthetic-IV (SIV) construction that is now accepted for use with encryption, but it is not the same, plus I don't know if any competent analyst has considered the case where SIV is also used for PBKDF salt, so I would be reluctant to rely on this technique here. If you want to ask about that point, it's not really a programming Q and would be more suitable on cryptography.SX or maybe security.SX.
added for comments:
That code's output works for me with puttygen from 0.70, both on Windows (from upstream=chiark) and on CentOS6 (from EPEL). According to the source, the error message you gave occurs only if cmdgen has called key_type in sshpubk.c which recognized the first line as beginning with "-----BEGIN " but not "-----BEGIN OPENSSH PRIVATE KEY" (which is a very different format), then via import_ssh2 and openssh_pem_read called load_openssh_pem_key in import.c which does NOT find the first line beginning with "-----BEGIN " and ending with "PRIVATE KEY-----". This is very weird because both of those PLUS "RSA " in between is generated by my code and is needed for OpenSSH (or openssl) to accept it. Try looking at every byte of the first line at least (maybe first two lines) with something like cat -vet or sed -n l or in a pinch od -c.
RFC 2898 is rather old now; good practice today is usually 10s of thousands to 100s of thousands of iterations, and better practice is not to use an iterated hash at all but instead something memory-hard like scrypt or Argon2. But as I already wrote, OpenSSL legacy PEM encryption, which was designed back in the 1990s, uses ONE (un, eine, 1) iteration and therefore is a POOR and INSECURE scheme. Nobody can change it now because that's how it was designed. If you want decent PBE, don't use this format.
If you need a key only for SSH: OpenSSH (for several years now) supports and recent versions of Putty(gen) can import the OpenSSH-defined 'new format', which uses bcrypt, but jsch can't. OpenSSH (using OpenSSL) can also read (PEM) PKCS8 which allows PBKDF2 (better though not best) with iterations as desired, and it looks like jsch can, but not Putty(gen). I don't know for Cyberduck or other implementations.
mister
I think that before invoking encrypt you need to decrypt two times more for security reasons. Instead of salt use also pepper salt and pepper. Do not mix algorithm with aes256.
Kind regards, rajeesh
I am in a situation where a JSON is encrypted in PHP's openssl_encrypt and needs to be decrypted in JAVA.
$encrypted = "...ENCRYPTED DATA...";
$secretFile = "/path/to/secret/saved/in/text_file";
$secret = base64_decode(file_get_contents($secretFile));
var_dump(strlen($secret)); // prints : int(370)
$iv = substr($encrypted, 0, 16);
$data = substr($encrypted, 16);
$decrypted = openssl_decrypt($data, "aes-256-cbc", $secret, null, $iv);
This $decrypted has correct data which is now decrypted.
Now, the problem is when I try to do same things in Java it doesn't work :(
String path = "/path/to/secret/saved/in/text";
String payload = "...ENCRYPTED DATA...";
StringBuilder output = new StringBuilder();
String iv = payload.substring(0, 16);
byte[] secret = Base64.getDecoder().decode(Files.readAllBytes(Paths.get(path)));
String data = payload.substring(16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(), 0, cipher.getBlockSize());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); // This line throws exception :
cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
Here it is:
Exception in thread "main" java.security.InvalidKeyException: Invalid AES key length: 370 bytes
at com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:87)
at com.sun.crypto.provider.CipherBlockChaining.init(CipherBlockChaining.java:91)
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:591)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:346)
at javax.crypto.Cipher.init(Cipher.java:1394)
at javax.crypto.Cipher.init(Cipher.java:1327)
at com.sample.App.main(App.java:70)
I have already visited similar question like
AES-256 CBC encrypt in php and decrypt in Java or vice-versa
openssl_encrypt 256 CBC raw_data in java
Unable to exchange data encrypted with AES-256 between Java and PHP
and list continues.... but no luck there
btw, this is how encryption is done in PHP
$secretFile = "/path/to/secret/saved/in/text_file";
$secret = base64_decode(file_get_contents($secretFile));
$iv = bin2hex(openssl_random_pseudo_bytes(8));
$enc = openssl_encrypt($plainText, "aes-256-cbc", $secret, false, $iv);
return $iv.$enc;
and yes, I forgot to mention that my JRE is already at UnlimitedJCEPolicy and I can't change PHP code.
I am totally stuck at this point and can't move forward. Please help out.
EDIT#1
byte[] payload = ....;
byte[] iv = ....;
byte[] secret = ....; // Now 370 bits
byte[] data = Base64.getDecoder().decode(payload);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOfRange(secret, 0, 32), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv, 0, cipher.getBlockSize());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] output = cipher.doFinal(data);
System.out.println(new String(output).trim());
Above snippet seems to be working with openssl_encrypt
EDIT#2
I am not sure if this is correct, but following is what I have done and encryption-decryption on both side are working fine.
Encrypt in PHP, Decrypt in JAVA use AES/CBC/NoPadding
Encrypt in JAVA, Decrypt in PHP use AES/CBC/PKCS5Padding
I won't provide a complete solution, but there are a few differences you should take care of
Encoding:
String iv = payload.substring(0, 16);
String data = payload.substring(16);
are you sure the IV and data are the same in Java and PHP (The IV is string?)? If the data are encrypted, they should be treated as a byte array, not string. Just REALLY make sure they are THE SAME (print hex/base64 in php and java)
For the IV you at the end call iv.getBytes(), but the locale encoding may/will corrupt your values. The String should be use only when it's really string (text). Don't use string for binaries.
Simply treat data and iv as byte[]
Key generation according to the openssl
AES key must have length of 256 bit for aes-256-cbc used. The thing is - openssl by default doesn't use the provided secret as a key (I believe it can, but I don't know how it is to be specified in PHP).
see OpenSSL EVP_BytesToKey issue in Java
and here is the EVP_BytesToKey implementation: https://olabini.com/blog/tag/evp_bytestokey/
you should generate a 256 bit key usging the EVP_BytesToKey function (it's a key derivation function used by openssl).
Edit:
Maarten (in the comments) is right. The key parameter is the key. Seems the PHP function is accepting parameter of any length which is misleading. According to some articles (e.g. http://thefsb.tumblr.com/post/110749271235/using-opensslendecrypt-in-php-instead-of) the key is trucated or padded to necessary length (so seems 370 bit key is truncated to length of 256 bits).
According to your example, I wrote fully working code for PHP and Java:
AesCipher class: https://gist.github.com/demisang/716250080d77a7f65e66f4e813e5a636
Notes:
-By default algo is AES-128-CBC.
-By default init vector is 16 bytes.
-Encoded result = base64(initVector + aes crypt).
-Encoded/Decoded results present as itself object, it gets more helpful and get possibility to check error, get error message and get init vector value after encode/decode operations.
PHP:
$secretKey = '26kozQaKwRuNJ24t';
$text = 'Some text'
$encrypted = AesCipher::encrypt($secretKey, $text);
$decrypted = AesCipher::decrypt($secretKey, $encrypted);
$encrypted->hasError(); // TRUE if operation failed, FALSE otherwise
$encrypted->getData(); // Encoded/Decoded result
$encrypted->getInitVector(); // Get used (random if encode) init vector
// $decrypted->* has identical methods
JAVA:
String secretKey = "26kozQaKwRuNJ24t";
String text = "Some text";
AesCipher encrypted = AesCipher.encrypt(secretKey, text);
AesCipher decrypted = AesCipher.decrypt(secretKey, encrypted);
encrypted.hasError(); // TRUE if operation failed, FALSE otherwise
encrypted.getData(); // Encoded/Decoded result
encrypted.getInitVector(); // Get used (random if encode) init vector
// decrypted.* has identical methods
I'm only asking this because I have read many posts for 2 days now about crypto AES encryption, and just when I thought I was getting it, I realized I wasn't getting it at all.
This post is the closest one to my issue, I have exactly the same problem but it is unanswered:
CryptoJS AES encryption and JAVA AES decryption value mismatch
I have tried doing it in many ways but I haven't gotten it right.
First Off
I'm getting the already encrypted string (I only got the code to see how they were doing it), so modifying the encryption way is not an option. That's why all the similar questions aren't that useful to me.
Second
I do have access to the secret key and I can modify it (so adjusting length is an option if neccessary).
The encryption is done on CryptoJS and they send the encrypted string as a GET parameter.
GetParamsForAppUrl.prototype.generateUrlParams = function() {
const self = this;
return new Promise((resolve, reject) => {
const currentDateInMilliseconds = new Date().getTime();
const secret = tokenSecret.secret;
var encrypted = CryptoJS.AES.encrypt(self.authorization, secret);
encrypted = encrypted.toString();
self.urlParams = {
token: encrypted,
time: currentDateInMilliseconds
};
resolve();
});
};
I can easily decrypt this on javascript using CryptoJS with:
var decrypted = CryptoJS.AES.decrypt(encrypted_string, secret);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
But I don't want to do this on Javascript, for security reasons, so I'm trying to decrypt this on Java:
String secret = "secret";
byte[] cipherText = encrypted_string.getBytes("UTF8");
SecretKey secKey = new SecretKeySpec(secret.getBytes(), "AES");
Cipher aesCipher = Cipher.getInstance("AES");
aesCipher.init(Cipher.DECRYPT_MODE, secKey);
byte[] bytePlainText = aesCipher.doFinal(byteCipherText);
String myDecryptedText = = new String(bytePlainText);
Before I had any idea of what I was doing, I tried base64 decoding, adding some IV and a lot of stuff I read, of course none of it worked.
But after I started to understand, kinda, what I was doing, I wrote that simple script above, and got me the same error on the post: Invalid AES key length
I don't know where to go from here. After reading a lot about this, the solution seems to be hashing or padding, but I have no control on the encryption method, so I can't really hash the secret or pad it.
But as I said, I can change the secret key so it can match some specific length, and I have tried changing it, but as I'm shooting in the dark here, I don't really know if this is the solution.
So, my question basically is, If I got the encrypted string (in javascript like the first script) and the secret key, is there a way to decrypt it (in Java)? If so, how to do it?
Disclaimer: Do not use encryption unless you understand encryption concepts including chaining mode, key derivation functions, IV and block size. And don't roll your own security scheme but stick to an established one. Just throwing in encryption algorithms doesn't mean an application has become any more secure.
CryptoJS implements the same key derivation function as OpenSSL and the same format to put the IV into the encrypted data. So all Java code that deals with OpenSSL encoded data applies.
Given the following Javascript code:
var text = "The quick brown fox jumps over the lazy dog. 👻 👻";
var secret = "René Über";
var encrypted = CryptoJS.AES.encrypt(text, secret);
encrypted = encrypted.toString();
console.log("Cipher text: " + encrypted);
We get the cipher text:
U2FsdGVkX1+tsmZvCEFa/iGeSA0K7gvgs9KXeZKwbCDNCs2zPo+BXjvKYLrJutMK+hxTwl/hyaQLOaD7LLIRo2I5fyeRMPnroo6k8N9uwKk=
On the Java side, we have
String secret = "René Über";
String cipherText = "U2FsdGVkX1+tsmZvCEFa/iGeSA0K7gvgs9KXeZKwbCDNCs2zPo+BXjvKYLrJutMK+hxTwl/hyaQLOaD7LLIRo2I5fyeRMPnroo6k8N9uwKk=";
byte[] cipherData = Base64.getDecoder().decode(cipherText);
byte[] saltData = Arrays.copyOfRange(cipherData, 8, 16);
MessageDigest md5 = MessageDigest.getInstance("MD5");
final byte[][] keyAndIV = GenerateKeyAndIV(32, 16, 1, saltData, secret.getBytes(StandardCharsets.UTF_8), md5);
SecretKeySpec key = new SecretKeySpec(keyAndIV[0], "AES");
IvParameterSpec iv = new IvParameterSpec(keyAndIV[1]);
byte[] encrypted = Arrays.copyOfRange(cipherData, 16, cipherData.length);
Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCBC.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedData = aesCBC.doFinal(encrypted);
String decryptedText = new String(decryptedData, StandardCharsets.UTF_8);
System.out.println(decryptedText);
The result is:
The quick brown fox jumps over the lazy dog. 👻 👻
That's the text we started with. And emojis, accents and umlauts work as well.
GenerateKeyAndIV is a helper function that reimplements OpenSSL's key derivation function EVP_BytesToKey (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
/**
* Generates a key and an initialization vector (IV) with the given salt and password.
* <p>
* This method is equivalent to OpenSSL's EVP_BytesToKey function
* (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
* By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data.
* </p>
* #param keyLength the length of the generated key (in bytes)
* #param ivLength the length of the generated IV (in bytes)
* #param iterations the number of digestion rounds
* #param salt the salt data (8 bytes of data or <code>null</code>)
* #param password the password data (optional)
* #param md the message digest algorithm to use
* #return an two-element array with the generated key and IV
*/
public static byte[][] GenerateKeyAndIV(int keyLength, int ivLength, int iterations, byte[] salt, byte[] password, MessageDigest md) {
int digestLength = md.getDigestLength();
int requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength;
byte[] generatedData = new byte[requiredLength];
int generatedLength = 0;
try {
md.reset();
// Repeat process until sufficient data has been generated
while (generatedLength < keyLength + ivLength) {
// Digest data (last digest if available, password data, salt if available)
if (generatedLength > 0)
md.update(generatedData, generatedLength - digestLength, digestLength);
md.update(password);
if (salt != null)
md.update(salt, 0, 8);
md.digest(generatedData, generatedLength, digestLength);
// additional rounds
for (int i = 1; i < iterations; i++) {
md.update(generatedData, generatedLength, digestLength);
md.digest(generatedData, generatedLength, digestLength);
}
generatedLength += digestLength;
}
// Copy key and IV into separate byte arrays
byte[][] result = new byte[2][];
result[0] = Arrays.copyOfRange(generatedData, 0, keyLength);
if (ivLength > 0)
result[1] = Arrays.copyOfRange(generatedData, keyLength, keyLength + ivLength);
return result;
} catch (DigestException e) {
throw new RuntimeException(e);
} finally {
// Clean out temporary data
Arrays.fill(generatedData, (byte)0);
}
}
Note that you have to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy. Otherwise, AES with key size of 256 won't work and throw an exception:
java.security.InvalidKeyException: Illegal key size
Update
I have replaced Ola Bini's Java code of EVP_BytesToKey, which I used in the first version of my answer, with a more idiomatic and easier to understand Java code (see above).
Also see How to decrypt file in Java encrypted with openssl command using AES?.
When encrypting on one system and decrypting on another you are at the mercy of system defaults. If any system defaults do not match (and they often don't) then your decryption will fail.
Everything has to be byte for byte the same on both sides. Effectively that means specifying everything on both sides rather than relying on defaults. You can only use defaults if you are using the same system at both ends. Even then, it is better to specify exactly.
Key, IV, encryption mode, padding and string to bytes conversion all need to be the same at both ends. It is especially worth checking that the key bytes are the same. If you are using a Key Derivation Function (KDF) to generate your key, then all the parameters for that need to be the same, and hence specified exactly.
Your "Invalid AES key length" may well indicate a problem with generating your key. You use getBytes(). That is probably an error. You need to specify what sort of bytes you are getting: ANSI, UTF-8, EBCDIC, whatever. The default assumption for the string to byte conversion is the likely cause of this problem. Specify the conversion to be used explicitly at both ends. That way you can be sure that they match.
Crypto is designed to fail if the parameters do not match exactly for encryption and decryption. For example, even a one bit difference in the key will cause it to fail.
I use the code like 3des-encryption-decryption-in-java,but when I use decrypt it,it got a error like this
javax.crypto.IllegalBlockSizeException: last block incomplete in decryption
07-17 11:27:27.580: WARN/System.err(22432): at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.java:705)
07-17 11:27:27.580: WARN/System.err(22432): at javax.crypto.Cipher.doFinal(Cipher.java:1111)
But if I change final Cipher decipher = Cipher.getInstance("DESede/CBC/PKCS5Padding"); to final Cipher decipher = Cipher.getInstance("DESede/CFB/NoPadding");,the method can run but got a wrong result (the mode is different from server).
So I want to know the reason about it.
The decrypt method:
public static String decrypt(byte[] message) throws Exception {
final MessageDigest md = MessageDigest.getInstance("SHA-1");
final byte[] digestOfPassword = md.digest(token.getBytes("utf-8"));
final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
for (int j = 0, k = 16; j < 8;) {
keyBytes[k++] = keyBytes[j++];
}
final SecretKey key = new SecretKeySpec(keyBytes, "DESede");
final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
final Cipher decipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
// final Cipher decipher = Cipher.getInstance("DESede/CFB/NoPadding");
decipher.init(Cipher.DECRYPT_MODE, key, iv);
final byte[] plainText = decipher.doFinal(message);
return new String(plainText, "UTF-8");
}
The encrypt method:
public static byte[] encrypt(String message) throws Exception {
final MessageDigest md = MessageDigest.getInstance("SHA-1");
final byte[] digestOfPassword = md.digest(token
.getBytes("utf-8"));
final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
for (int j = 0, k = 16; j < 8; ) {
keyBytes[k++] = keyBytes[j++];
}
final SecretKey key = new SecretKeySpec(keyBytes, "DESede");
final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
final Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, iv, new SecureRandom(new byte[5]));
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
final byte[] plainTextBytes = message.getBytes("utf-8");
final byte[] cipherText = cipher.doFinal(plainTextBytes);
return cipherText;
}
There are many possibilitiesThe most common is if you atr encoding the key as a String, especially without specifying the character encoding. If you want to do this, use Base-64, which is designed to encode any binary data, rather than a character encodingAlso make sure that source platform and target platform encoding should be same.As you are using UTF-8 here and then on the other en , UTF-8 must be usedNow have a look under the facts which you are telling that code is running with final Cipher decipher = Cipher.getInstance("DESede/CFB/NoPadding"); but not with final Cipher decipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
While Decryption, you must know the padding size and mode in which you have selected at encryption time.As you are saying that when you use CBC mode than it throws exception but when you changed it to CFB then it got able to run.In this case you need to make sure which mode you are using at encrytpion time.
As a side note: CBC, OFB and CFB are identical, however OFB/CFB is better because you only need encryption and not decryption, which can save code space.
CBC(Cipher Block Chaining) is used where the data goes through the AES function, and feedback is applied to modify the pre-encrypted data, so that repeated plain data does not yield the same encrypted data. Data can only be processed in blocks which match the block-size of the underlying encryption function (so 128-bit blocks on the case of AES), and synchronisation at this block level must be provided between the encrypting and decrypting engines, otherwise data will be indecipherable
CFB(Cipher FeedBack mode) is also a common mode, and offers the possibility of making an underlying block cipher work like a stream cipher; ie. so that the data being processed can be a stream of shorter values (for example bytes or even individual bits) rather than being processed only as much larger blocks.In CFB mode, the data itself does not go through the AES engine, but gets XORed with a value which the AES engine generates from previous message history. This means that the latency through the CFB function can be minimised, as the only processing applied to the data is an XOR function. Data widths can be set to any size up to the underlying cipher block size, but note that throughput decreases as the widths get smaller in the ratio of the data width to the block size.(Side note ended :D)
If you encrypt using Cipher-Feedback (CFB) or Output-Feedback (OFB) or counter (CTR) modes then the ciphertext will be the same size as the plaintext and so padding is not required. Be careful, though, when using these modes, because the initialisation vectors (IV) must be unique.
Similarly, encrypting using a stream cipher like RC4 or PC1 does not require padding.
Now if we investigate more critically than you should take care about the block size and padding size(already mentioned above).Now the first thing you need to make sure is that the padding size defined by your encryption algorithm.As I mentioned that in CFB case padding is not required so first try it without giving padding.if issue still resides then check either it is pkcs5 or pkcs7.Try your code by setting decrytpion padding size to pkcs7.If it is pkcs7 then I guess it should work with CBC too.I recommend you to read Using Padding in Encryption As a additional information
PKCS#5 padding is defined in RFC 2898 (PKCS #5: Password-Based Cryptography Specification Version 2.0).
PKCS5 padding is a padding scheme for extending arbitrary data to match the block-size of a block cipher in a way that allows the receiving end to reliably remove the padding.
PKCS#7 (CMS, RFC 3369) defines a padding scheme, but it is an extension of PKCS#5 padding for block ciphers that have more than 8 bytes for block.
I have an encrypt method in Java.
public static String encrypt(String orignal){
SecretKeySpec key = new SecretKeySpec(keyString.getBytes(), "AES");
IvParameterSpec initalVector = new IvParameterSpec(initialVectorParam.getBytes());
try{
Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
/////////////// encrypt /////////////////
cipher.init(Cipher.ENCRYPT_MODE, key, initalVector);
Log.d("AES", "oriByte: "+ orignal.getBytes());
int length = orignal.length();
for(int i=0; i<length; i++){
}
byte[] test = cipher.doFinal(orignal.getBytes());
Log.d("AES", "encByte: "+ test);
return bytes2Hex(test);
}catch (Exception e) {
Log.d("AES", "Encrypt Exception:"+orignal);
return "";
}
}
For compatibility with PHP, I use "AES/CFB8/NoPadding" options.
In PHP: $sCipher = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $sKey, $sStr, MCRYPT_MODE_CFB, $sIV);
And I have a Objective-c Cipher code from here.
https://gist.github.com/838614
I found that there is no IvParameterSpec in Objective-c Cipher like java.
Besides, the getBytes method returns a different value with java.
(I think this is because java uses different encoding way.)
So, how can I apply IvParameterSpec in Objective-c.
And is there any way to get 'getBytes' value like java in Objective-c?
For the initialization vector, see line 24 in your pastie:
NULL /* initialization vector (optional) */,
That's where you would pass your IV.
But without knowing the string encoding the Java code used to create the bytes used as the IV, you won't be able to seed the encryption properly to decrypt the data, even if you know what the string displays to the screen as. Put another way, just because the IV looks like "abc123" doesn't mean the bytes Java is writing to the IV buffer are going to be the same bytes you'll get if you strncpy() from a C character literal buffer. You have to agree on the encoding as part of the protocol for handling the data.
You will also need to agree on a key size. Your Java code does not specify how many bits are in the AES key.
Once you've got that worked out, you'll want to use a call like:
const void *key = /* KEY BYTES */;
const void *iv = /* IV BYTES */;
const void *text = /* CIPHER TEXT */;
size_t textlen = /*...*/;
size_t outlen = 0;
(void)CCCrypt(kCCDecrypt, kCCAlgorithmAES128, 0/*use CBC mode*/,
key, kCCKeySizeAES128, iv,
text, textlen,
&text, textlen, &outlen);
The plaintext will be written over the ciphertext, assuming all goes well. The amount o data written to text during decryption will be stored in outlen. Error checking is your responsibility; the header is well-commented.
Once you have the data, you'll want to slurp it into an NSString with the correct encoding (+[NSString initWithData:encoding:] would work), and then you have a string you can work with from Obj-C like any other string.