Encrypt message for Web Push API in Java - java

I'm trying to create a server capable of sending push messages using the Push API: https://developer.mozilla.org/en-US/docs/Web/API/Push_API
I've got the client side working but now I want to be able to send messages with a payload from a Java server.
I saw the nodejs web-push example (https://www.npmjs.com/package/web-push) but I couldn't translate that correctly to Java.
I tried following the example to use the DH key exchange found here: http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html#DH2Ex
With the help of sheltond below I was able to figure out some code that should be working but isn't.
When I post the encrypted message to the Push service, I get back the expected 201 status code but the push never reaches Firefox. If I remove the payload and headers and simply send a POST request to the same URL the message successfully arrives in Firefox with no data. I suspect it may have something to do with the way I'm encrypting the data with Cipher.getInstance("AES/GCM/NoPadding");
This is the code I'm using currently:
try {
final byte[] alicePubKeyEnc = Util.fromBase64("BASE_64_PUBLIC_KEY_FROM_PUSH_SUBSCRIPTION");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec kpgparams = new ECGenParameterSpec("secp256r1");
kpg.initialize(kpgparams);
ECParameterSpec params = ((ECPublicKey) kpg.generateKeyPair().getPublic()).getParams();
final ECPublicKey alicePubKey = fromUncompressedPoint(alicePubKeyEnc, params);
KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("EC");
bobKpairGen.initialize(params);
KeyPair bobKpair = bobKpairGen.generateKeyPair();
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("ECDH");
bobKeyAgree.init(bobKpair.getPrivate());
byte[] bobPubKeyEnc = toUncompressedPoint((ECPublicKey) bobKpair.getPublic());
bobKeyAgree.doPhase(alicePubKey, true);
Cipher bobCipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKey bobDesKey = bobKeyAgree.generateSecret("AES");
byte[] saltBytes = new byte[16];
new SecureRandom().nextBytes(saltBytes);
Mac extract = Mac.getInstance("HmacSHA256");
extract.init(new SecretKeySpec(saltBytes, "HmacSHA256"));
final byte[] prk = extract.doFinal(bobDesKey.getEncoded());
// Expand
Mac expand = Mac.getInstance("HmacSHA256");
expand.init(new SecretKeySpec(prk, "HmacSHA256"));
String info = "Content-Encoding: aesgcm128";
expand.update(info.getBytes(StandardCharsets.US_ASCII));
expand.update((byte) 1);
final byte[] key_bytes = expand.doFinal();
// Use the result
SecretKeySpec key = new SecretKeySpec(key_bytes, 0, 16, "AES");
bobCipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cleartext = "{\"this\":\"is a test that is supposed to be working but it is not\"}".getBytes();
byte[] ciphertext = bobCipher.doFinal(cleartext);
URL url = new URL("PUSH_ENDPOINT_URL");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Length", ciphertext.length + "");
urlConnection.setRequestProperty("Content-Type", "application/octet-stream");
urlConnection.setRequestProperty("Encryption-Key", "keyid=p256dh;dh=" + Util.toBase64UrlSafe(bobPubKeyEnc));
urlConnection.setRequestProperty("Encryption", "keyid=p256dh;salt=" + Util.toBase64UrlSafe(saltBytes));
urlConnection.setRequestProperty("Content-Encoding", "aesgcm128");
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
final OutputStream outputStream = urlConnection.getOutputStream();
outputStream.write(ciphertext);
outputStream.flush();
outputStream.close();
if (urlConnection.getResponseCode() == 201) {
String result = Util.readStream(urlConnection.getInputStream());
Log.v("PUSH", "OK: " + result);
} else {
InputStream errorStream = urlConnection.getErrorStream();
String error = Util.readStream(errorStream);
Log.v("PUSH", "Not OK: " + error);
}
} catch (Exception e) {
Log.v("PUSH", "Not OK: " + e.toString());
}
where "BASE_64_PUBLIC_KEY_FROM_PUSH_SUBSCRIPTION" is the key the Push API subscription method in the browser provided and "PUSH_ENDPOINT_URL" is the push endpoint the browser provided.
If I get values (ciphertext, base64 bobPubKeyEnc and salt) from a successful nodejs web-push request and hard-code them in Java, it works. If I use the code above with dynamic values it does not work.
I did notice that the ciphertext that worked in the nodejs implementation is always 1 byte bigger then the Java ciphertext with the code above. The example I used here always produces a 81 byte cipher text but in nodejs it's always 82 bytes for example. Does this give us a clue on what might be wrong?
How do I correctly encrypt the payload so that it reaches Firefox?
Thanks in advance for any help

Able to receive notifications after changing code as per https://jrconlin.github.io/WebPushDataTestPage/
Find the modified code below :
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class WebPushEncryption {
private static final byte UNCOMPRESSED_POINT_INDICATOR = 0x04;
private static final ECParameterSpec params = new ECParameterSpec(
new EllipticCurve(new ECFieldFp(new BigInteger(
"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
16)), new BigInteger(
"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
16), new BigInteger(
"5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B",
16)), new ECPoint(new BigInteger(
"6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
16), new BigInteger(
"4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
16)), new BigInteger(
"FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551",
16), 1);
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
String endpoint = "https://updates.push.services.mozilla.com/push/v1/xxx";
final byte[] alicePubKeyEnc = Base64.decode("base64 encoded public key ");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDH", "BC");
keyGen.initialize(params);
KeyPair bobKpair = keyGen.generateKeyPair();
PrivateKey localPrivateKey = bobKpair.getPrivate();
PublicKey localpublickey = bobKpair.getPublic();
final ECPublicKey remoteKey = fromUncompressedPoint(alicePubKeyEnc, params);
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("ECDH", "BC");
bobKeyAgree.init(localPrivateKey);
byte[] bobPubKeyEnc = toUncompressedPoint((ECPublicKey) bobKpair.getPublic());
bobKeyAgree.doPhase(remoteKey, true);
SecretKey bobDesKey = bobKeyAgree.generateSecret("AES");
byte[] saltBytes = new byte[16];
new SecureRandom().nextBytes(saltBytes);
Mac extract = Mac.getInstance("HmacSHA256", "BC");
extract.init(new SecretKeySpec(saltBytes, "HmacSHA256"));
final byte[] prk = extract.doFinal(bobDesKey.getEncoded());
// Expand
Mac expand = Mac.getInstance("HmacSHA256", "BC");
expand.init(new SecretKeySpec(prk, "HmacSHA256"));
//aes algorithm
String info = "Content-Encoding: aesgcm128";
expand.update(info.getBytes(StandardCharsets.US_ASCII));
expand.update((byte) 1);
final byte[] key_bytes = expand.doFinal();
byte[] key_bytes16 = Arrays.copyOf(key_bytes, 16);
SecretKeySpec key = new SecretKeySpec(key_bytes16, 0, 16, "AES-GCM");
//nonce
expand.reset();
expand.init(new SecretKeySpec(prk, "HmacSHA256"));
String nonceinfo = "Content-Encoding: nonce";
expand.update(nonceinfo.getBytes(StandardCharsets.US_ASCII));
expand.update((byte) 1);
final byte[] nonce_bytes = expand.doFinal();
byte[] nonce_bytes12 = Arrays.copyOf(nonce_bytes, 12);
Cipher bobCipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
byte[] iv = generateNonce(nonce_bytes12, 0);
bobCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] cleartext = ("{\n"
+ " \"message\" : \"great match41eeee!\",\n"
+ " \"title\" : \"Portugal vs. Denmark4255\",\n"
+ " \"icon\" : \"http://icons.iconarchive.com/icons/artdesigner/tweet-my-web/256/single-bird-icon.png\",\n"
+ " \"tag\" : \"testtag1\",\n"
+ " \"url\" : \"http://www.yahoo.com\"\n"
+ " }").getBytes();
byte[] cc = new byte[cleartext.length + 1];
cc[0] = 0;
for (int i = 0; i < cleartext.length; i++) {
cc[i + 1] = cleartext[i];
}
cleartext = cc;
byte[] ciphertext = bobCipher.doFinal(cleartext);
URL url = new URL(endpoint);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Length", ciphertext.length + "");
urlConnection.setRequestProperty("Content-Type", "application/octet-stream");
urlConnection.setRequestProperty("encryption-key", "keyid=p256dh;dh=" + Base64.encode(bobPubKeyEnc));
urlConnection.setRequestProperty("encryption", "keyid=p256dh;salt=" + Base64.encode(saltBytes));
urlConnection.setRequestProperty("content-encoding", "aesgcm128");
urlConnection.setRequestProperty("ttl", "60");
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
final OutputStream outputStream = urlConnection.getOutputStream();
outputStream.write(ciphertext);
outputStream.flush();
outputStream.close();
if (urlConnection.getResponseCode() == 201) {
String result = readStream(urlConnection.getInputStream());
System.out.println("PUSH OK: " + result);
} else {
InputStream errorStream = urlConnection.getErrorStream();
String error = readStream(errorStream);
System.out.println("PUSH" + "Not OK: " + error);
}
}
static byte[] generateNonce(byte[] base, int index) {
byte[] nonce = Arrays.copyOfRange(base, 0, 12);
for (int i = 0; i < 6; ++i) {
nonce[nonce.length - 1 - i] ^= (byte) ((index / Math.pow(256, i))) & (0xff);
}
return nonce;
}
private static String readStream(InputStream errorStream) throws Exception {
BufferedInputStream bs = new BufferedInputStream(errorStream);
int i = 0;
byte[] b = new byte[1024];
StringBuilder sb = new StringBuilder();
while ((i = bs.read(b)) != -1) {
sb.append(new String(b, 0, i));
}
return sb.toString();
}
public static ECPublicKey fromUncompressedPoint(
final byte[] uncompressedPoint, final ECParameterSpec params)
throws Exception {
int offset = 0;
if (uncompressedPoint[offset++] != UNCOMPRESSED_POINT_INDICATOR) {
throw new IllegalArgumentException(
"Invalid uncompressedPoint encoding, no uncompressed point indicator");
}
int keySizeBytes = (params.getOrder().bitLength() + Byte.SIZE - 1)
/ Byte.SIZE;
if (uncompressedPoint.length != 1 + 2 * keySizeBytes) {
throw new IllegalArgumentException(
"Invalid uncompressedPoint encoding, not the correct size");
}
final BigInteger x = new BigInteger(1, Arrays.copyOfRange(
uncompressedPoint, offset, offset + keySizeBytes));
offset += keySizeBytes;
final BigInteger y = new BigInteger(1, Arrays.copyOfRange(
uncompressedPoint, offset, offset + keySizeBytes));
final ECPoint w = new ECPoint(x, y);
final ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(w, params);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec);
}
public static byte[] toUncompressedPoint(final ECPublicKey publicKey) {
int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1)
/ Byte.SIZE;
final byte[] uncompressedPoint = new byte[1 + 2 * keySizeBytes];
int offset = 0;
uncompressedPoint[offset++] = 0x04;
final byte[] x = publicKey.getW().getAffineX().toByteArray();
if (x.length <= keySizeBytes) {
System.arraycopy(x, 0, uncompressedPoint, offset + keySizeBytes
- x.length, x.length);
} else if (x.length == keySizeBytes + 1 && x[0] == 0) {
System.arraycopy(x, 1, uncompressedPoint, offset, keySizeBytes);
} else {
throw new IllegalStateException("x value is too large");
}
offset += keySizeBytes;
final byte[] y = publicKey.getW().getAffineY().toByteArray();
if (y.length <= keySizeBytes) {
System.arraycopy(y, 0, uncompressedPoint, offset + keySizeBytes
- y.length, y.length);
} else if (y.length == keySizeBytes + 1 && y[0] == 0) {
System.arraycopy(y, 1, uncompressedPoint, offset, keySizeBytes);
} else {
throw new IllegalStateException("y value is too large");
}
return uncompressedPoint;
}
}

See https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-01#section-5 and https://w3c.github.io/push-api/#widl-PushSubscription-getKey-ArrayBuffer-PushEncryptionKeyName-name (point 4).
The key is encoded using the uncompressed format defined in ANSI X9.62, so you can't use x509EncodedKeySpec.
You could use BouncyCastle, that should support the X9.62 encoding.

Have a look at the answer from Maarten Bodewes in this question.
He gives Java source for encoding/decoding from the X9.62 uncompressed format into an ECPublicKey, which I think should be suitable for what you're trying to do.
== Update 1 ==
The spec says "User Agents that enforce encryption MUST expose an elliptic curve Diffie-Hellman share on the P-256 curve".
The P-256 curve is a standard curve approved by NIST for use in US government encryption applications. The definition, parameter values and rationale for choosing this particular curve (along with a few others) are given here.
There is support for this curve in the standard library using the name "secp256r1", but for reasons that I haven't been able to fully work out (I think it's to do with the separation of cryptography providers from the JDK itself), you seem to have to jump through some very inefficient hoops to get one of these ECParameterSpec values from this name:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec kpgparams = new ECGenParameterSpec("secp256r1");
kpg.initialize(kpgparams);
ECParameterSpec params = ((ECPublicKey) kpg.generateKeyPair().getPublic()).getParams();
This is pretty heavyweight because it actually generates a keypair using the named ECGenParameterSpec object, then extracts the ECParameterSpec from it. You should then be able to use this to decode (I'd recommend caching this value somewhere to avoid having to do this key-generation frequently).
Alternatively, you can just take the numbers from page 8 of the NIST document and plug them in directly to the ECParameterSpec constructor.
There is some code here which looks like it does exactly that (around line 124). That code is Apache licensed. I haven't used that code myself, but it looks like the constants match what's in the NIST document.
== Update 2 ==
The actual encryption key is derived from the salt (randomly generated) and the shared secret (agreed by the DH key exchange), using the HMAC-based key derivation function (HKDF) described in section 3.2 of Encrypted Content-Encoding for HTTP.
That document references RFC 5869 and specifies the use of SHA-256 as the hash used in the HKDF.
This RFC describes a two stage process: Extract and Expand. The Extract phase is defined as:
PRK = HMAC-Hash(salt, IKM)
In the case of web-push, this should be an HMAC-SHA-256 operation, the salt value should be the "saltBytes" value that you already have, and as far as I can see the IKM value should be the shared secret (the webpush document just says "These values are used to calculate the content encryption key" without specifically stating that the shared secret is the IKM).
The Expand phase takes the value produced by the Extract phase plus an 'info' value, and repeatedly HMACs them until it has produced enough key data for the encryption algorithm that you're using (the output of each HMAC is fed into the next one - see the RFC for details).
In this case, the algorithm is AEAD_AES_128_GCM which requires a 128-bit key, which is smaller than the output of SHA-256, so you only need to do one hash in the Expand stage.
The 'info' value in this case has to be "Content-Encoding: aesgcm128" (specified in Encrypted Content-Encoding for HTTP), so the operation that you need is:
HMAC-SHA-256(PRK, "Content-Encoding: aesgcm128" | 0x01)
where the '|' is concatenation. You then take the first 16 bytes of the result, and that should be the encryption key.
In Java terms, that would look something like:
// Extract
Mac extract = Mac.getInstance("HmacSHA256");
extract.init(new SecretKeySpec(saltBytes, "HmacSHA256"));
final byte[] prk = extract.doFinal(bobDesKey.getEncoded());
// Expand
Mac expand = Mac.getInstance("HmacSHA256");
expand.init(new SecretKeySpec(prk, "HmacSHA256"));
String info = "Content-Encoding: aesgcm128";
expand.update(info.getBytes(StandardCharsets.US_ASCII));
expand.update((byte)1);
final byte[] key_bytes = expand.doFinal();
// Use the result
SecretKeySpec key = new SecretKeySpec(key_bytes, 0, 16, "AES");
bobCipher.init(Cipher.ENCRYPT_MODE, key);
For reference, here's a link to the part of the BouncyCastle library that does this stuff.
Finally, I just noticed this part in the webpush document:
Public keys, such as are encoded into the "dh" parameter, MUST be in
the form of an uncompressed point
so it looks like you will need to use something like this:
byte[] bobPubKeyEnc = toUncompressedPoint((ECPublicKey)bobKpair.getPublic());
instead of using the standard getEncoded() method.
== Update 3 ==
First, I should point out that there is a more recent draft of the spec for http content encryption than the one that I have previous linked to: draft-ietf-httpbis-encryption-encoding-00. People who want to use this system should make sure that they are using the latest available draft of the spec - this is work in progress and seems to be changing slightly every few months.
Second, in section 2 of that document, it specifies that some padding must be added to the plaintext before encryption (and removed after decryption).
This would account for the one byte difference in length between what you mentioned that you're getting and what the Node.js example produces.
The document says:
Each record contains between 1 and 256 octets of padding, inserted
into a record before the enciphered content. Padding consists of a
length byte, followed that number of zero-valued octets. A receiver
MUST fail to decrypt if any padding octet other than the first is
non-zero, or a record has more padding than the record size can
accommodate.
So I think what you need to do is to push a single '0' byte into the cipher before your plaintext. You could add more padding than that - I couldn't see anything that specified that the padding must be the minimum amount possible, but a single '0' byte is the simplest (anyone reading this who is trying to decode these messages from the other end should make sure that they support any legal amount of padding).
In general for http content encryption, the mechanism is a bit more complicated than that (since you have to split up the input into records and add padding to each one), but the webpush spec says that the encrypted message must fit into a single record, so you don't need to worry about that.
Note the following text in the webpush encryption spec:
Note that a push service is not required to support more than 4096
octets of payload body, which equates to 4080 octets of cleartext
The 4080 octets of cleartext here includes the 1 byte of padding, so there effectively seems to be a limit of 4079 bytes. You can specify a larger record size using the "rs" parameter in the "Encryption" header, but according to the text quoted above, the recipient isn't required to support that.
One warning: some of the code that I've seen to do this seems to be changing to using 2 bytes of padding, presumably as a result of some proposed spec change, but I haven't been able to track down where this is coming from. At the moment 1 byte of padding should be ok, but if this stops working in the future, you may need to go to 2 bytes - as I mentioned above this spec is a work in progress and browser support is experimental right now.

The solution of santosh kumar works with one modification:
I added a 1-byte cipher padding right before defining the cleartext byte[].
Cipher bobCipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
byte[] iv = generateNonce(nonce_bytes12, 0);
bobCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
// adding firefox padding:
bobCipher.update(new byte[1]);
byte[] cleartext = {...};

Related

Java AES Decryption with keyFile using BouncyCastle SSL

I am trying to write a Java code decrypt a file encrypted with AES256 using BouncyCastle compatible with OpenSSL decryption.
s_key is the file provided which contains the key that will be used to encrypt and decrypt
Steps to be done: 1 - Read the key file 2 - Use the key provided to decrypt file inputfilename
Below I have use so far but I am getting error:
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator;
import org.bouncycastle.crypto.io.CipherOutputStream;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV;
import javax.crypto.NoSuchPaddingException;
public class test5_encrypt {
public static void main(String[] args) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
File file = new File("/home/roxane/key");
String passwordStr = FileUtils.readFileToString(file, "UTF-8");
String outputPath = "/home/roxane/test1";
String inputPath = "/home/roxane/test";
SecureRandom random = new SecureRandom();
byte salt[] = new byte[8];
random.nextBytes(salt);
// Derive 32 bytes key (AES_256) and 16 bytes IV
byte[] password = passwordStr.getBytes(StandardCharsets.UTF_8);
OpenSSLPBEParametersGenerator pbeGenerator = new OpenSSLPBEParametersGenerator(new MD5Digest()); // SHA256 as of v1.1.0 (if in OpenSSL the default digest is applied)
pbeGenerator.init(password, salt);
ParametersWithIV parameters = (ParametersWithIV) pbeGenerator.generateDerivedParameters(256, 128);// keySize, ivSize in bits
System.out.println(parameters.getIV());
// Decrypt with AES-256
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
// Encrypt chunkwise (for large data)
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
cipher.init(false, parameters);
try (FileInputStream fis = new FileInputStream(inputPath);
CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
int bytesRead = -1;
byte[] buffer = new byte[64 * 1024 * 1024];
while ((bytesRead = fis.read(buffer)) != -1) {
cos.write(buffer, 0, bytesRead);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Error:
Exception in thread "main" java.lang.RuntimeException: org.bouncycastle.crypto.io.InvalidCipherTextIOException: Error finalising cipher data
at decrypt.test5_encrypt.main(test5_encrypt.java:61)
Caused by: org.bouncycastle.crypto.io.InvalidCipherTextIOException: Error finalising cipher data
at org.bouncycastle.crypto.io.CipherOutputStream.close(Unknown Source)
at decrypt.test5_encrypt.main(test5_encrypt.java:59)
Caused by: org.bouncycastle.crypto.InvalidCipherTextException: pad block corrupted
When using a password, OpenSSL stores the ciphertext in a specific format, namely the ASCII encoding of Salted__, followed by the 8 bytes salt, then the actual ciphertext.
During decryption, the salt must not be randomly generated (as it is done in the posted code), otherwise the wrong key and IV will be derived. Instead, the salt must be determined from the metadata of the ciphertext. Also the use of the stream classes must be fixed:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.ParametersWithIV;
...
String inputPath = "..."; // path to enc file
String outputPath = "..."; // path to dec file
String passwordStr = "...";
// Decrypt with AES-256, CBC using streams
try (FileInputStream fis = new FileInputStream(inputPath)){
// Determine salt from OpenSSL format
fis.readNBytes(8); // Skip prefix Salted__
byte[] salt = fis.readNBytes(8); // Read salt
// Derive 32 bytes key (AES_256) and 16 bytes IV via EVP_BytesToKey()
byte[] password = passwordStr.getBytes(StandardCharsets.UTF_8);
OpenSSLPBEParametersGenerator pbeGenerator = new OpenSSLPBEParametersGenerator(new MD5Digest()); // SHA256 as of v1.1.0 (if in OpenSSL the default digest is applied)
pbeGenerator.init(password, salt);
ParametersWithIV parameters = (ParametersWithIV) pbeGenerator.generateDerivedParameters(256, 128); // keySize, ivSize in bits
// Decrypt chunkwise (for large data)
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
cipher.init(false, parameters);
try (CipherInputStream cis = new CipherInputStream(fis, cipher);
FileOutputStream fos = new FileOutputStream(outputPath)) {
int bytesRead = -1;
byte[] buffer = new byte[64 * 1024 * 1024]; // chunksize, e.g. 64 MiB
while ((bytesRead = cis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
}
This is functionally equivalent to the OpenSSL statement:
openssl enc -d -aes256 -k <passpharse> -in <enc file> -out <dec file>
Note that OpenSSL applied MD5 as digest by default in earlier versions and SHA256 as of v.1.1.0. Code and OpenSSL statement must use the same digest for compatibility.
In the code the digest is explicitly specified, in the OpenSSL statement it can be explicitly set via the -md option so that matching is possible on both sides.
Keep in mind that EVP_BytesToKey(), which is used by default by OpenSSL for key derivation, is deemed insecure nowadays.
Addition regarding Java 8: For Java 8, e.g. the following implementation can be applied for the determination of the salt:
int i = 0;
byte[] firstBlock = new byte[16];
while (i < firstBlock.length) {
i += fis.read(firstBlock, i, firstBlock.length - i);
}
byte[] salt = Arrays.copyOfRange(firstBlock, 8, 16);
The loop is necessary because read(byte[],int,int), unlike readNBytes(int), does not guarantee that the buffer is completely filled (considering here the non-EOF and non-error case).
If you omit the loop (which means using the equivalent read(byte[])), the code will still run for those JVMs which also fill the buffer completely. Since this applies to the most common JVMs for small buffer sizes the code will mostly work, see the comment by dave_thompson_085. However, this is not guaranteed for any JVM and is therefore less robust (though probably not by much).

Encrypting Java then Decrypting C# AES256 Encryption with HMACSHA256, Padding is invalid

I'm currently running into an issue where our decryption portion of our C# site is having trouble with the padding with the encrypted string from java. The .Net code throws this error "Padding is invalid and cannot be removed". The _signKey and _encKey are both 64 bytes.
public String encryptString(String plainText) {
byte[] ciphertext;
byte[] iv = new byte[16];
byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
String _signKey = "****************************************************************";
String _encKey = "****************************************************************";
try {
Mac sha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec shaKS = new SecretKeySpec(_signKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256.init(shaKS);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecureRandom randomSecureRandom = SecureRandom.getInstance("SHA1PRNG");
iv = new byte[cipher.getBlockSize()];
randomSecureRandom.nextBytes(iv);
IvParameterSpec ivParams = new IvParameterSpec(iv);
byte[] sessionKey = sha256.doFinal((_encKey + iv).getBytes(StandardCharsets.UTF_8));
// Perform Encryption
SecretKeySpec eks = new SecretKeySpec(sessionKey, "AES");
cipher.init(Cipher.ENCRYPT_MODE, eks, ivParams);
ciphertext = cipher.doFinal(plainBytes);
System.out.println("ciphertext= " + new String(ciphertext));
// Perform HMAC using SHA-256 on ciphertext
SecretKeySpec hks = new SecretKeySpec(_signKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(hks);
ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
outputStream2.write(iv);
outputStream2.write(ciphertext);
outputStream2.flush();
outputStream2.write(mac.doFinal(outputStream2.toByteArray()));
return Base64.encodeBase64String(outputStream2.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
return plainText;
}
Does does encrypt the string properly as far as I can tell. We cannot change any code on the .Net side to decrypt this because this is being used today.
public static string DecryptString(string ciphertext)
{
using (HMACSHA256 sha256 = new HMACSHA256(Encoding.UTF8.GetBytes(_signKey)))
{
// Convert message to bytes
byte[] encBytes = Convert.FromBase64String(ciphertext);
// Get arrays for comparing HMAC tags
byte[] sentTag = new byte[sha256.HashSize / 8];
byte[] calcTag = sha256.ComputeHash(encBytes, 0, (encBytes.Length - sentTag.Length));
// If message length is too small return null
if (encBytes.Length < sentTag.Length + _ivLength) { return null; }
// Copy tag from end of encrypted message
Array.Copy(encBytes, (encBytes.Length - sentTag.Length), sentTag, 0, sentTag.Length);
// Compare tags with constant time comparison, return null if no match
int compare = 0;
for (int i = 0; i < sentTag.Length; i++) { compare |= sentTag[i] ^ calcTag[i]; }
if (compare != 0) { return null; }
using (AesCryptoServiceProvider csp = new AesCryptoServiceProvider())
{
// Set parameters
csp.BlockSize = _blockBits;
csp.KeySize = _keyBits;
csp.Mode = CipherMode.CBC;
csp.Padding = PaddingMode.PKCS7;
// Copy init vector from message
var iv = new byte[_ivLength];
Array.Copy(encBytes, 0, iv, 0, iv.Length);
// Derive session key
byte[] sessionKey = sha256.ComputeHash(Encoding.UTF8.GetBytes(_encKey + iv));
// Decrypt message
using (ICryptoTransform decrypt = csp.CreateDecryptor(sessionKey, iv))
{
return Encoding.UTF8.GetString(decrypt.TransformFinalBlock(encBytes, iv.Length, encBytes.Length - iv.Length - sentTag.Length));
}
}
}
}
If there is anything that sticks out it would be appreciated for the reply.
I didn't read all your code, but this line in Java:
byte[] sessionKey = sha256.doFinal((_encKey + iv).getBytes(StandardCharsets.UTF_8));
does nothing useful or sensible. The "+" operator does string concatenation, but iv is a byte[], not a String. So java uses iv.toString(), which simply returns a String containing something like [B#1188e820 which is meaningless in this context.
Refer four java code and DotNet code:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //Java
csp.Padding = PaddingMode.PKCS7; //.Net
You are essentially using different padding, that is the probable source of error; however, there is an alternate view, Refer this great post and this for general fundamentals on padding
The cipher suites supported by deafult Oracle JVM implementation are here
If you notice it does not have 'AES/CBC/PKCS7Padding', a PKCS#7 padding implementation is available in sun.security package, refer this, otherwise you could use Bouncy Castle packages. It would be recommendable to use Bouncy Castle as com.sun package are generally considered unsupported.

Cryptography in Java

I'm making an app that encrypts some files. I want to use gnu's cryptix library. It says it is no longer developed since 2005, but I guess it has everything I need... should I use something else?
And I have a question about encrypting a single file. Right now I do it with a loop like this:
for(int i=0; i+block_size < bdata.length; i += block_size)
cipher.encryptBlock(bdata, i, cdata, i);
So my question is how to encrypt the last block that may not have the same size as the block_size. I was thinking maybe a should add some extra data to the last block, but than I don't know how to decrypt that...
I would strongly suggest using AES encryption and it too comes with the JAVA SDK. Have a look at: Using AES with Java Technology which will give you some great example. To read up more on AES see: Advanced Encryption Standard - Wikipedia.
Never use your own encryption scheme or an older form of an encryption scheme. AES has been tried and tested by people with far greater knowledge in that field then us, so you know it will work. Where as with your own or an old encryption scheme we might miss a fatal loop hole that will leave our data open to attacks.
See this question here to see the difference in the encryption schemes: Comparison of DES, Triple DES, AES, blowfish encryption for data
Addendum:
AES in java will work flawlessly for 192 and 256bit keys but you will have to install the newer JCE Policy Files. See here and here. You should also place the files in your JDK or else it wont work when executed from your IDE.
Note: Make sure you download the correct JCE policy files, depending on your Java version i.e 1.4, 1.5 1.6 or 7.
However if you use 128bit keys no need to install the newer JCE files.
Here is a template of some secure AES usage in java it use CBC/AES/PKCS5Padding and a random IV using RandomSecure.
Note you need both the key and IV for decrypting:
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* This program generates a AES key, retrieves its raw bytes, and then
* reinstantiates a AES key from the key bytes. The reinstantiated key is used
* to initialize a AES cipher for encryption and decryption.
*/
public class AES {
/**
* Encrypt a sample message using AES in CBC mode with a random IV genrated
* using SecyreRandom.
*
*/
public static void main(String[] args) {
try {
String message = "This string contains a secret message.";
System.out.println("Plaintext: " + message + "\n");
// generate a key
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(128); // To use 256 bit keys, you need the "unlimited strength" encryption policy files from Sun.
byte[] key = keygen.generateKey().getEncoded();
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
// build the initialization vector (randomly).
SecureRandom random = new SecureRandom();
byte iv[] = new byte[16];//generate random 16 byte IV AES is always 16bytes
random.nextBytes(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
// initialize the cipher for encrypt mode
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivspec);
System.out.println("Key: " + new String(key, "utf-8") + " This is important when decrypting");
System.out.println("IV: " + new String(iv, "utf-8") + " This is important when decrypting");
System.out.println();
// encrypt the message
byte[] encrypted = cipher.doFinal(message.getBytes());
System.out.println("Ciphertext: " + asHex(encrypted) + "\n");
// reinitialize the cipher for decryption
cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivspec);
// decrypt the message
byte[] decrypted = cipher.doFinal(encrypted);
System.out.println("Plaintext: " + new String(decrypted) + "\n");
} catch (IllegalBlockSizeException | BadPaddingException | UnsupportedEncodingException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
}
/**
* Turns array of bytes into string
*
* #param buf Array of bytes to convert to hex string
* #return Generated hex string
*/
public static String asHex(byte buf[]) {
StringBuilder strbuf = new StringBuilder(buf.length * 2);
int i;
for (i = 0; i < buf.length; i++) {
if (((int) buf[i] & 0xff) < 0x10) {
strbuf.append("0");
}
strbuf.append(Long.toString((int) buf[i] & 0xff, 16));
}
return strbuf.toString();
}
}
I always use BouncyCastle
I also use the streaming framework instead of the for loop you were describing: it deals with the issue raised. Mostly I use that because when it comes to cryptography (and threading) I rarely trust my own code, I trust the people that live eat and breath it. Here is the code I use when I want "gash" cryptography. i.e. I have no particular threat model, and just want something "a little secure".
The hex encoding of the keys makes them much easier to manipulate / store and so on. I use "makeKey" to ... well ... make a key, then I can use the key in the encrypt and decrypt methods. You can obviously go back to using byte[] instead of hex strings for the keys.
private static boolean initialised;
private static void init() {
if (initialised)
return;
Security.addProvider(new BouncyCastleProvider());
initialised = true;
}
public static String makeKey() {
init();
KeyGenerator generator = KeyGenerator.getInstance(algorithm, provider);
generator.init(keySize);
Key key = generator.generateKey();
byte[] encoded = key.getEncoded();
return Strings.toHex(encoded);
}
public static String aesDecrypt(String hexKey, String hexCoded) {
init();
SecretKeySpec key = new SecretKeySpec(Strings.fromHex(hexKey), algorithm);
Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding", provider);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] codedBytes = Strings.fromHex(hexCoded);
CipherInputStream inputStream = new CipherInputStream(new ByteArrayInputStream(codedBytes), cipher);
byte[] bytes = getBytes(inputStream, 256);
String result = new String(bytes, "UTF-8");
return result;
}
public static String aesEncrypt(String hexKey, String input) {
init();
SecretKeySpec key = new SecretKeySpec(Strings.fromHex(hexKey), algorithm);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, key);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(input.length());
CipherOutputStream outputStream = new CipherOutputStream(byteArrayOutputStream, cipher);
setText(outputStream, input);
byte[] outputBytes = byteArrayOutputStream.toByteArray();
String output = new String(Strings.toHex(outputBytes));
return output;
}
public static void setText(OutputStream outputStream, String text, String encoding) {
try {
outputStream.write(text.getBytes(encoding));
outputStream.flush();
} finally {
outputStream.close();
}
}
public static byte[] getBytes(InputStream inputStream, int bufferSize) {
try {
List<ByteArrayAndLength> list = Lists.newList();
while (true) {
byte[] buffer = new byte[bufferSize];
int count = inputStream.read(buffer);
if (count == -1) {
byte[] result = new byte[ByteArrayAndLength.length(list)];
int index = 0;
for (ByteArrayAndLength byteArrayAndLength : list) {
System.arraycopy(byteArrayAndLength.bytes, 0, result, index, byteArrayAndLength.length);
index += byteArrayAndLength.length;
}
assert index == result.length;
return result;
}
list.add(new ByteArrayAndLength(buffer, count));
}
} finally {
inputStream.close();
}
}
static class ByteArrayAndLength {
byte[] bytes;
int length;
public ByteArrayAndLength(byte[] bytes, int length) {
super();
this.bytes = bytes;
this.length = length;
}
static int length(List<ByteArrayAndLength> list) {
int result = 0;
for (ByteArrayAndLength byteArrayAndLength : list) {
result += byteArrayAndLength.length;
}
return result;
}
}
I've taken out some of the exception catching to reduce the size of the code, and Strings.fromHex turns the string back into a byte[]
Maybe you should consider using a javax.crypto package.
Here is an example of how to use Ciphers:
DES encryption
Hope this helps
I would seriously think twice before going this route. The development of the software was halted because standard alternatives exist, and have a look at the mailing list, there's been no significant activity since 2009. In my book that means that the software is abandoned, and abandoned software means you're more or less on your own.
Have a look here on SO, there are several questions and answers that may help you like this one. An at first sight interesting package that could simplify things for you (but still using the standard JCE infrastructure) is jasypt

PHP Java AES CBC Encryption Different Results

PHP Function:
$privateKey = "1234567812345678";
$iv = "1234567812345678";
$data = "Test string";
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_CBC, $iv);
echo(base64_encode($encrypted));
Result: iz1qFlQJfs6Ycp+gcc2z4w==
Java Function
public static String encrypt() throws Exception{
try{
String data = "Test string";
String key = "1234567812345678";
String iv = "1234567812345678";
javax.crypto.spec.SecretKeySpec keyspec = new javax.crypto.spec.SecretKeySpec(key.getBytes(), "AES");
javax.crypto.spec.IvParameterSpec ivspec = new javax.crypto.spec.IvParameterSpec(iv.getBytes());
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(data.getBytes());
return new sun.misc.BASE64Encoder().encode(encrypted);
}catch(Exception e){
return null;
}
}
returns null.
Please note that we are not allowed to change the PHP code. Could somebody please help us get the same results in Java? Many thanks.
You'd have had a better idea of what was going on if you didn't simply swallow up possible Exceptions inside your encrypt() routine. If your function is returning null then clearly an exception happened and you need to know what it was.
In fact, the exception is:
javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes
at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:854)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:828)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:676)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:313)
at javax.crypto.Cipher.doFinal(Cipher.java:2087)
at Encryption.encrypt(Encryption.java:20)
at Encryption.main(Encryption.java:6)
And sure enough, your plaintext is only 11 Java characters long which, in your default encoding, will be 11 bytes.
You need to check what the PHP mcrypt_encrypt function actually does. Since it works, it is clearly using some padding scheme. You need to find out which one it is and use it in your Java code.
Ok -- I looked up the man page for mcrypt_encrypt. It says:
The data that will be encrypted with the given cipher and mode. If the size of the data is not n * blocksize, the data will be padded with \0.
So you need to replicate that in Java. Here's one way:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Encryption
{
public static void main(String args[]) throws Exception {
System.out.println(encrypt());
}
public static String encrypt() throws Exception {
try {
String data = "Test string";
String key = "1234567812345678";
String iv = "1234567812345678";
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
// We need to pad with zeros to a multiple of the cipher block size,
// so first figure out what the size of the plaintext needs to be.
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
int remainder = plaintextLength % blockSize;
if (remainder != 0) {
plaintextLength += (blockSize - remainder);
}
// In java, primitive arrays of integer types have all elements
// initialized to zero, so no need to explicitly zero any part of
// the array.
byte[] plaintext = new byte[plaintextLength];
// Copy our actual data into the beginning of the array. The
// rest of the array is implicitly zero-filled, as desired.
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return new sun.misc.BASE64Encoder().encode(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
And when I run that I get:
iz1qFlQJfs6Ycp+gcc2z4w==
which is what your PHP program got.
Update (12 June 2016):
As of Java 8, JavaSE finally ships with a documented base64 codec. So instead of
return new sun.misc.BASE64Encoder().encode(encrypted);
you should do something like
return Base64.Encoder.encodeToString(encrypted);
Alternatively, use a 3rd-party library (such as commons-codec) for base64 encoding/decoding rather than using an undocumented internal method.

Java decryption and encryption compatible with SJCL?

I need to encrypt & decrypt data with both Java (on Android) and SJCL (I could plausibly switch to another JS crypto library, but am familiar with SJCL so would prefer to stick with it if possible).
I have the SJCL end working fine, but at the Java end I'm not really sure what parameters I need to use to set up the key generator and cipher. The code I have so far for decryption is:
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 1024, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
String plaintext = new String(cipher.doFinal(ciphertext), "UTF-8");
return plaintext;
Where salt, iv and ciphertext are extracted as strings from the JSON object produced by SJCL and then decoded using a Base64 decoder to byte arrays.
Unfortunately, I have a few problems with this and the code above doesn't work.
The first problem I have is that PBKDF2WithHmacSHA256 doesn't seem to be a recognised key generation algorithm. I'm not entirely sure that this is what I want, but it appears to be right based on reading the SJCL documentation? Java does recognise PBKDF2WithHmacSHA1, but this doesn't seem to be the same algorithm SJCL implements.
Secondly, if I try using the SHA1 key algorithm, I get an error about invalid key size. Do I need to install something to enable AES with 256-bit keys? Telling the key factory to produce a 128-bit key works OK (although obviously is not compatible with SJCL, which is using a 256-bit key).
Thirdly, what cipher mode should I be using? I'm pretty sure CBC isn't right... SJCL's documentation mentions both CCM and OCB, but Java doesn't seem to support either of these out of the box -- again, do I need to install something to make this work? And which one does SJCL default to?
And finally, even if I pick parameters that make Java not complain about missing algorithms, it complains that the IV provided by decoding the SJCL output is the wrong length, which it certainly appears to be: there are 17 bytes in the resulting output, not 16 as is apparently required by AES. Do I just ignore the last byte?
I haven't tried it (in the end I switched away from using Javascript crypto in favour of using an embedded java applet with bouncycastle to handle communication), but GnuCrypto (a bouncycastle fork) supports PBKDFWithHmacSHA256. The fixed character encoding handling in SJCL presumably fixes the unexpected length of the IV (?), so this would just leave the cipher mode. From this point, it appears that the easiest approach would be to implement a relatively simple cipher mode (e.g. CTR) as an add-on for SJCL, which ought to be only a few hours work even for someone unfamiliar with the code, after which it is simply a matter of encoding and decoding the JSON-encoded data packets that are used by SJCL (which ought to be trivial).
As an alternative, it would certainly be possible to implement OCB mode for Java, despite the fact that the algorithm is proprietary, as there is a public patent grant for software distributed under the GPL (http://www.cs.ucdavis.edu/~rogaway/ocb/grant.htm).
Interestingly, I wonder whether GnuCrypto would accept a patch for OCB mode support? GnuCrypto is distributed under GPL-with-libraries-exemption, which would appear to qualify as "any version of the GNU General Public License as published by the Free Software Foundation", so theoretically at least this should be possible.
SJCL AES in java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.*;
import java.util.HashMap;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* SJCL 1.0.8
*
* dependencies:
* compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.64'
* compile group: 'org.json', name: 'json', version: '20190722'
*
* ref: https://blog.degering.name/posts/java-sjcl
*/
public class AesSJCL {
// Simply prints out the decoded string.
public static void main(String[] args) throws Exception {
String password = "password";
String plainText = "Who am I?";
// encryption
Map<String, Object> result = new AesSJCL().encrypt( password, plainText);
String json = new JSONObject(result).toString();
System.out.printf("encrypted output:\n%s\n", json);
System.out.printf("\njavascript testing code:\nsjcl.decrypt(\"%s\", '%s')\n", password, json);
// decryption
String decryptedText = new AesSJCL().decrypt(password, json);
System.out.printf("\ndecrypted output: \n%s\n", decryptedText);
}
/**
*
* #param password - password
* #param encryptedText - {"cipher":"aes","mode":"ccm","ct":"r7U/Gp2r8LVNQR7kl5qLNd8=","salt":"VwSOS3jCn6M=","v":1,"ks":128,"iter":10000,"iv":"5OEwQPtHK2ej1mHwvOf57A==","adata":"","ts":64}
* #return
* #throws Exception
*/
public String decrypt(String password, String encryptedText) throws Exception {
Decoder d = Base64.getDecoder();
// Decode the encoded JSON and create a JSON Object from it
JSONObject j = new JSONObject(new String(encryptedText));
// We need the salt, the IV and the cipher text;
// all of them need to be Base64 decoded
byte[] salt=d.decode(j.getString("salt"));
byte[] iv=d.decode(j.getString("iv"));
byte[] cipherText=d.decode(j.getString("ct"));
// Also, we need the keySize and the iteration count
int keySize = j.getInt("ks"), iterations = j.getInt("iter");
// https://github.com/bitwiseshiftleft/sjcl/blob/master/core/ccm.js#L60
int lol = 2;
if (cipherText.length >= 1<<16) lol++;
if (cipherText.length >= 1<<24) lol++;
// Cut the IV to the appropriate length, which is 15 - L
iv = Arrays.copyOf(iv, 15-lol);
// Crypto stuff.
// First, we need the secret AES key,
// which is generated from password and salt
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(),
salt, iterations, keySize);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
// Now it's time to decrypt.
Cipher cipher = Cipher.getInstance("AES/CCM/NoPadding",
new BouncyCastleProvider());
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
// Return the final result after converting it to a string.
return new String(cipher.doFinal(cipherText));
}
/**
*
* #param password
* #param plainText
* #return
* #throws Exception
*/
public Map<String, Object> encrypt(String password, String plainText) throws Exception {
int iterations = 10000; // default in SJCL
int keySize = 128;
// https://github.com/bitwiseshiftleft/sjcl/blob/master/core/convenience.js#L321
// default salt bytes are 8 bytes
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] salt = new byte[8];
sr.nextBytes(salt);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keySize);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
// https://github.com/bitwiseshiftleft/sjcl/blob/master/core/random.js#L87
// default iv bytes are 16 bytes
SecureRandom randomSecureRandom = SecureRandom.getInstanceStrong();
byte[] iv = new byte[16];
randomSecureRandom.nextBytes(iv);
int ivl = iv.length;
if (ivl < 7) {
throw new RuntimeException("ccm: iv must be at least 7 bytes");
}
// compute the length of the length
int ol=plainText.length();
int L=2;
for (; L<4 && ( ol >>> 8*L ) > 0; L++) {}
if (L < 15 - ivl) { L = 15-ivl; }
byte[] shortIV = Arrays.copyOf(iv, 15-L);
// Now it's time to decrypt.
Cipher cipher = Cipher.getInstance("AES/CCM/NoPadding", new BouncyCastleProvider());
cipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(shortIV));
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(UTF_8));
Encoder encoder = Base64.getEncoder();
Map<String, Object> map = new HashMap<>();
map.put("iv", encoder.encodeToString(iv));
map.put("iter", iterations);
map.put("ks", keySize);
map.put("salt", encoder.encodeToString(salt));
map.put("ct", encoder.encodeToString(encryptedBytes));
map.put("cipher", "aes");
map.put("mode", "ccm");
map.put("adata", "");
map.put("v", 1); // I don't know what it is.
map.put("ts", 64); // I don't know what it is.
return map;
}
}
github gist by me
ref: Java talks SJCL
You may have to use BouncyCastle to get all the cryptographic features used in SJCL. Make sure you're base64 decoding everything correctly and that SJCL doesn't add in length indicators or similar.

Categories