I have created a trigger and a trigger function which invokes on every update operation on a table and encrypts a specific value as below:
create trigger project_trigger
before update
on projects
for each row
execute procedure project_function();
create or replace function project_function()
returns trigger as
$BODY$
begin
IF (TG_OP = 'UPDATE') THEN
NEW.title = armor(pgp_sym_encrypt(NEW.title, 'cipher-algo=aes256' ));
return NEW;
END IF;
end;
$BODY$
language plpgsql;
The above approach for encryption is working fine and an armored PGP encrypted value gets saved as below:
-----BEGIN PGP MESSAGE-----
ww0EBwMCneBsNZw1gYFq0jYB9y58EoTaRXWmDFqvQArWU5tZ+wS+7yAm9ycVUpkH1EzvYLbfRoDj
rqR83I0nGErHcLSLlAs=
=IYg8
-----END PGP MESSAGE-----
Decryption needs to be done at the application level for which I followed the following 2 steps:
Added bcpg-jdk15on and bcprov-jdk15on dependencies. (v1.47)
Implementation:
String key = "aes_key";
File file = new File("D:\\file.txt.asc"); //this file contains the PGP encrypted value as shown above
InputStream input = new FileInputStream(file);
byte[] byt = new byte[input.available()];
input.read(byt);
input.close();
Security.addProvider(new BouncyCastleProvider());
System.out.println(new String(ByteArrayHandler.decrypt(byt,
key.toCharArray())));
I keep getting the following exception while using the above approach to decrypt the value:
Exception in thread "main"
org.bouncycastle.openpgp.PGPDataValidationException: data check
failed. at
org.bouncycastle.openpgp.PGPPBEEncryptedData.getDataStream(Unknown
Source) at
org.bouncycastle.openpgp.examples.ByteArrayHandler.decrypt(Unknown
Source) at
abc.demo.encryption.SymmetricDecyption.main(SymmetricDecyption.java:59)
So can someone guide me to the appropriate approach to achieve decryption at the application level (not in the queries).
There are two problems. The PGPDataValidationException is caused by using a different pass-phrase for encryption and decryption. If you had used the correct pass-phrase, then you would have found that the Bouncy Castle example code is not fully functional.
The trigger is probably not what you intended. The call to pgp_sym_encrypt should look more like this:
create or replace function project_function()
returns trigger as
$BODY$
begin
IF (TG_OP = 'UPDATE') THEN
NEW.title = armor(pgp_sym_encrypt(NEW.title, 'my-secret-passphrase', 'cipher-algo=aes256, compress-algo=2' ));
return NEW;
END IF;
end;
$BODY$
language plpgsql;
The three input parameters to pgp_sym_encrypt are the text to be encrypted, the pass phrase from which the cipher key will be derived, and options. In your question, you omitted the pass phrase.
Second the BouncyCastle example code assumes that the plain text has been compressed. I have added RFC1950 compression (ZLIB) to the pgp_sym_encrypt.
With those changes to the trigger I get:
postgres=# update projects set title = 'My secret compressed title.';
UPDATE 1
postgres=# \t off
postgres=# select * from projects;
title
-----BEGIN PGP MESSAGE-----
ww0ECQMCuN3MyfrWhBt50lcBGbUtjOlTBxGpAFCl7aYEybhhXRJodDsikWxdLmOsXnE6vWr9mwd7
dGy7N1eE5VFmwI5N29eCNhEvG5U4YmVC7fV1A1sBeoJMtsO/nz2mi2jbFiZHlzo=
=s6uI
-----END PGP MESSAGE-----
(1 row)
postgres=#
Feeding that into a Java program:
String value = "-----BEGIN PGP MESSAGE-----\n"
+ "\n"
+ "ww0ECQMCuN3MyfrWhBt50lcBGbUtjOlTBxGpAFCl7aYEybhhXRJodDsikWxdLmOsXnE6vWr9mwd7\n"
+ "dGy7N1eE5VFmwI5N29eCNhEvG5U4YmVC7fV1A1sBeoJMtsO/nz2mi2jbFiZHlzo=\n"
+ "=s6uI\n"
+ "-----END PGP MESSAGE-----\n";
String key = "my-secret-passphrase";
byte[] byt = value.getBytes(StandardCharsets.UTF_8);
Security.addProvider(new BouncyCastleProvider());
System.out.println(new String(ByteArrayHandler.decrypt(byt, key.toCharArray()), StandardCharsets.UTF_8));
Produces the output:
My secret compressed title.
Exactly as desired.
If you want to not compress the plain text before encrypting it, then you can look at the example PBEFileProcessor as this handles both compressed and uncompressed data, or you can just use this code:
public static byte[] decrypt(
byte[] encrypted,
char[] passPhrase
) throws IOException, PGPException {
JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(new ByteArrayInputStream(encrypted)));
// Find the encrypted data list. The first object might be a PGP marker packet, or the actual data list
PGPEncryptedDataList enc;
Object o = pgpF.nextObject();
if (o instanceof PGPEncryptedDataList) {
enc = (PGPEncryptedDataList) o;
} else {
enc = (PGPEncryptedDataList) pgpF.nextObject();
}
// Do the decryption
PGPPBEEncryptedData pbe = (PGPPBEEncryptedData) enc.get(0);
InputStream clear = pbe.getDataStream(new JcePBEDataDecryptorFactoryBuilder(
new JcaPGPDigestCalculatorProviderBuilder().setProvider("BC").build()).setProvider("BC").build(passPhrase)
);
// Process the decrypted data. It may be compressed, or it may be literal
JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(clear);
o = pgpFact.nextObject();
if (o instanceof PGPCompressedData) {
// Need to decompress the data
PGPCompressedData cData = (PGPCompressedData) o;
pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
o = pgpFact.nextObject();
}
// We should have the literal data now, so convert it into bytes
PGPLiteralData ld = (PGPLiteralData) o;
return Streams.readAll(ld.getInputStream());
}
Finally, when decrypting in postgresql you do not need to specify whether the plain text was compressed, nor how it was encrypted, as the PGP data specifies this, so you can do:
select pgp_sym_decrypt(dearmor(title), 'my-secret-passphrase') from projects;
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 working on PostgreSQL and getting below error during update statement execution from java code.
ERROR: invalid byte sequence for encoding "UTF8": 0x00
The code snippet for password encrption is :
StringBuilder encryptionKey = new StringBuilder().append(newPassword).append(userEmail).append(userEmail).append(userEmail);
AdvancedEncryptionStandard aes = new AdvancedEncryptionStandard(encryptionKey.toString().getBytes(StandardCharsets.UTF_8));
newEncryptedPwd = new String(aes.encrypt(newPassword.getBytes(StandardCharsets.UTF_8)));
EntityManager em = EntityManagerUtil.getEntityManager(schemaName);
EntityManagerUtil.beginTransaction(schemaName);
Query query = em.createQuery("UPDATE User um SET um.password=:newPassword WHERE um.loginId=:userID");
query.setParameter("userID", userName);
query.setParameter("newPassword", newEncryptedPwd);
query.executeUpdate();
Encrypt function is as below:
public byte[] encrypt(byte[] plainText) throws Exception
{
SecretKeySpec secretKey = new SecretKeySpec(key,0,16,'AES');
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(plainText);
}
public AdvancedEncryptionStandard(byte[] key)
{
this.key = key;
}
I have checked client_encoding by show client_encoding command and it is showing UTF-8.
Can someone give me pointer on resolving this issue? I have gone through the suggestions provided in other threads but none helped.
0 actually is invalid UTF8. UTF8 contains æ, it contains ™, but none of what it contains has code point 0.
Your problem is that you're trying to store arbitrary binary data as though it were UTF8-encoded text. Don't. If you encrypt something, the encryption will have hidden its nature (that's the job description for encryption) so it was an integer, you no longer can store it in an integer column, it if was text you no longer can store it in a text column. What you have is just an array of bytes.
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 was asked to encrypt some text from client side ( web ) before sending it to server side ( java )
So i try to use CryptoJS library for client side.
I encrypt it like this :
var key = "aaaaaaaaaaaaaaaaaaaaaaaa";
var value = "KF169841";
var encryptedString = CryptoJS.TripleDES.encrypt(value, key);
console.log(encryptedString.toString());
And i get something like this : U2FsdGVkX19eYFFHgYGCr3v9/skTOKVp0pLWRNK9JTg=
I use this encryptedString and key in other Decrypt tool online ( Which also use CryptoJS ) and got back exact value KF169841.
After sending this value and key to server ( well key isn't sending directly to server though but for test, it is ), i need to decrypt it using Java.
But i quite don't know how to decrypt it. I'm tried some code from google search but it end up wrong padding if use DESese or get wrong value if i use ECB/NoPadding.
I did try to something like setting sfg for CryptoJS side like:
mode: CryptoJS.mode.EBC,
padding: CryptoJS.pad.NoPadding
But they got javascript exception ( a is not define )
So any have any experience with CryptoJS can help me decrypt this one using java ?
=============================================================
UPDATE : Sorry here my server side code i'm using
/**
* Method To Decrypt An Ecrypted String
*/
public String decrypt(String encryptedString, String myEncryptionKey) {
String decryptedText = null;
try {
byte[] keyAsBytes = myEncryptionKey.getBytes("UTF8");
KeySpec myKeySpec = new DESedeKeySpec(keyAsBytes);
SecretKeyFactory mySecretKeyFactory =
SecretKeyFactory.getInstance("DESede");
Cipher cipher = Cipher.getInstance("DESede/ECB/NoPadding");
SecretKey key = mySecretKeyFactory.generateSecret(myKeySpec);
cipher.init(Cipher.DECRYPT_MODE, key);
// BASE64Decoder base64decoder = new BASE64Decoder();
// byte[] encryptedText = base64decoder.decodeBuffer(encryptedString);
byte[] encryptedText = org.apache.commons.codec.binary.Base64.decodeBase64(encryptedString);
byte[] plainText = cipher.doFinal(encryptedText);
decryptedText= bytes2String(plainText);
} catch (Exception e) {
e.printStackTrace();
}
return decryptedText;
}
According to the documentation, your encryptedString variable contains structured data that must be split apart to be sent to Java code. You will need to send encryptedString.iv and encryptedString.ciphertext to your Java code. If you continue to use passwords (see below), you will need to send encryptedString.salt as well.
If you pass your key as a string it will be interpreted as a password and a key will be derived from it. If you actually want to pass an explicit key, follow the documentation and specify the IV and key as suggested by the code snippet below. If you stick with supplying a password, then you must figure out the derivation scheme and use the same process in your Java code.
// Code snippet from http://code.google.com/p/crypto-js/#Custom_Key_and_IV
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script>
var key = CryptoJS.enc.Hex.parse('000102030405060708090a0b0c0d0e0f');
var iv = CryptoJS.enc.Hex.parse('101112131415161718191a1b1c1d1e1f');
var encrypted = CryptoJS.AES.encrypt("Message", key, { iv: iv });
</script>
Regarding your Java code, it looks mostly OK (although there is plenty of room for error with string conversions). However, you probably want to convert your key from hex to binary rather than grabbing the bytes:
byte[] keyAsBytes = DatatypeConverter.parseHexBinary(myEncryptionKey);
This assumes you alter your JavaScript code to pass the literal key value.
You will also need to switch to DESede/CBC/PKCS5Padding and pass an IVParameterSpec object to your Cipher.init call, specifying the IV value sent from your Java Script code.
This is a follow up to this question, but I'm trying to port C# code to Java instead of Ruby code to C#, as was the case in the related question. I am trying to verify the encrypted signature returned from the Recurly.js api is valid. Unfortunately, Recurly does not have a Java library to assist with the validation, so I must implement the signature validation myself.
Per the related question above (this), the following C# code can produce the hash needed to validate the signature returned from Recurly:
var privateKey = Configuration.RecurlySection.Current.PrivateKey;
var hashedKey = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(privateKey));
var hmac = new HMACSHA1(hashedKey);
var hash = hmac.ComputeHash(Encoding.ASCII.GetBytes(dataToProtect));
return BitConverter.ToString(hash).Replace("-", "").ToLower();
Recurly provides the following example data on their signature documentation page:
unencrypted verification message:
[1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]
private key:
0123456789ABCDEF0123456789ABCDEF
resulting signature:
0f5630424b32402ec03800e977cd7a8b13dbd153-1312701386
Here is my Java implementation:
String unencryptedMessage = "[1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]";
String privateKey = "0123456789ABCDEF0123456789ABCDEF";
String encryptedMessage = getHMACSHA1(unencryptedMessage, getSHA1(privateKey));
private static byte[] getSHA1(String source) throws NoSuchAlgorithmException, UnsupportedEncodingException{
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] bytes = md.digest(source.getBytes("UTF-8"));
return bytes;
}
private static String getHMACSHA1(String baseString, byte[] keyBytes) throws GeneralSecurityException, UnsupportedEncodingException {
SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(secretKey);
byte[] bytes = baseString.getBytes("ASCII");
return Hex.encodeHexString(mac.doFinal(bytes));
}
However, when I print out the encryptedMessage variable, it does not match the message portion of the example signature. Specifically, I get a value of "c8a9188dcf85d1378976729e50f1de5093fabb78" instead of "0f5630424b32402ec03800e977cd7a8b13dbd153".
Update
Per #M.Babcock, I reran the C# code with the example data, and it returned the same output as the Java code. So it appears my hashing approach is correct, but I am passing in the wrong data (unencryptedMessage). Sigh. I will update this post if/when I can determine what the correct data to encrypt is- as the "unencrypted verification message" provided in the Recurly documentation appears to be missing something.
Update 2
The error turned out to be the "unencrypted verification message" data/format. The message in the example data does not actually encrypt to the example signature provided- so perhaps outdated documentation? At any rate, I have confirmed the Java implementation will work for real-world data. Thanks to all.
I think the problem is in your .NET code. Does Configuration.RecurlySection.Current.PrivateKey return a string? Is that value the key you expect?
Using the following code, .NET and Java return identical results.
.NET Code
string message = "[1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]";
string privateKey = "0123456789ABCDEF0123456789ABCDEF";
var hashedKey = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(privateKey));
var hmac = new HMACSHA1(hashedKey);
var hash = hmac.ComputeHash(Encoding.ASCII.GetBytes(message));
Console.WriteLine(" Message: {0}", message);
Console.WriteLine(" Key: {0}\n", privateKey);
Console.WriteLine("Key bytes: {0}", BitConverter.ToString(hashedKey).Replace("-", "").ToLower());
Console.WriteLine(" Result: {0}", BitConverter.ToString(hash).Replace("-", "").ToLower());
Result:
Message: [1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]
Key: 0123456789ABCDEF0123456789ABCDEF
Key bytes: 4d857d2408b00c3dd17f0c4ffcf15b97f1049867
Result: c8a9188dcf85d1378976729e50f1de5093fabb78
Java
String message = "[1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]";
String privateKey = "0123456789ABCDEF0123456789ABCDEF";
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] keyBytes = md.digest(privateKey.getBytes("UTF-8"));
SecretKey sk = new SecretKeySpec(keyBytes, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(sk);
byte[] result = mac.doFinal(message.getBytes("ASCII"));
System.out.println(" Message: " + message);
System.out.println(" Key: " + privateKey + "\n");
System.out.println("Key Bytes: " + toHex(keyBytes));
System.out.println(" Results: " + toHex(result));
Result:
Message: [1312701386,transactioncreate,[account_code:ABC,amount_in_cents:5000,currency:USD]]
Key: 0123456789ABCDEF0123456789ABCDEF
Key Bytes: 4d857d2408b00c3dd17f0c4ffcf15b97f1049867
Results: c8a9188dcf85d1378976729e50f1de5093fabb78
I suspect the default encoding of the values you're working on may be different. As they do not have it specified, they will use the default encoding value of the string based on the platform you're working on.
I did a quick search to verify if this was true and it was still inconclusive, but it made me think that strings in .NET default to UTF-16 encoding, while Java defaults to UTF-8. (Can someone confirm this?)
If such's the case, then your GetBytes method with UTF-8 encoding is already producing a different output for each case.
Based on this sample code, it looks like Java expects you to have not already SHA1'd your key before creating a SecretKeySpec. Have you tried that?