Related
I want to convert this below piece of code into java but I am unable to do, Basically I have to implement 'crypto' module in Java. Thanks in advance!
let encKey = "0Z8ZUcy1Qh8lnt199MTwTPEe2g1E2tE3";
encKey = crypto.createHash('sha256').update(encKey).digest('bin').slice(0, 32);
let char = String.fromCharCode(0x0);
let iv = char + char + char + char + char + char + char + char + char + char + char + char + char + char + char + char;
let decryptor = crypto.createDecipheriv("aes-256-cbc", encKey, iv);
let dec = decryptor.update(someAuthString, 'base64', 'utf8') + decryptor.final('utf8');
dec = removePKCS5Padding(dec);
removePKCS5Padding
function removePKCS5Padding(text) {
let pad = ord(text[text.length - 1]);
pad = text.substr(0, -1 * pad)
if (_.isEmpty(pad)) {
return text;
} else {
return pad;
}
}
First you don't need to implement the whole module, just particular algorithms from it.
Second whoever wrote that code didn't know what they were doing. SHA-256 already produces a 32-byte value (always) so .slice(0,32) accomplishes nothing. And createCipher[iv] and createDecipher[iv] for a block mode already add and remove 'PKCS5' padding automatically unless explicitly disabled. (Prior to PKCS5v2.1 it was technically more correct to say PKCS7 or PKCS5/7, but in practice people often don't bother, and Java calls it PKCS5. OpenSSL, which nodejs crypto uses internally, punts and calls it PKCS padding -- although there are several PKCS1 paddings which are quite different, and which OpenSSL also implements.)
byte[] keyIn = "0Z8ZUcy1Qh8lnt199MTwTPEe2g1E2tE3" .getBytes("ASCII");
// if any non-ASCII char(s) must select same encoding nodejs does, I believe utf8
// instead of string form can use e.g. StandardCharsets.US_ASCII
byte[] keyHash = MessageDigest.getInstance("SHA-256") .doFinal(keyIn);
// or "sha-256" Java crypto names are case-insensitive
// can separate steps with hasher = .getInstance(); hasher.update(keyIn); result = hasher.doFinal()
// but cannot do fluent-style result = .getInstance() .update(keyIn) .doFinal()
byte[] iv = new byte[16]; // Java automatically fills numeric array with (binary) zeros
Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding");
dec.init (Cipher.DECRYPT_MODE, new SecretKeySpec(keyHash,"AES"), new IvParameterSpec(iv));
String clear = new String( dec.doFinal (Base64.getDecoder().decode( someAuthString )), "UTF-8");
// or StandardCharsets.UTF_8
I'm working with some Android Java code that uses ECDSA keys. The code compiles and runs fine, but has some logic errors during the verification process. I want to try using a constant key pair (that's known to be valid) to troubleshoot the program.
Using an online generator, I got an EC public key in hex,
0x044fb7cebbb1f4a1e0412c8e0b6f2d675ebfee000c5e860a81ffd795b5743033dec0e114abfba3de8db8705fc8ed985c5550c66a6ee9fdd258d058a2ef749eba78
As well as a valid private key to complete the pair,
0x0c84e7e707b31ecf0254e8cb3040513883d92d81e977ad4754a409a6ab18ee51
I can convert the hex string to a primitive byte array, but that byte array appears to be invalid. I cannot figure out how to convert a hex representation of my keys to a X509 representation so that I can make a Java key object.
KeyFactory mFactory = KeyFactory.getInstance("EC");
X509EncodedKeySpec mPublicKey = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = mFactory.generatePublic(mPublicKey);
That code results in:
java.security.spec.InvalidKeySpecException: com.android.org.conscrypt.OpenSSLX509CertificateFactory$ParsingException: Error parsing public key
I am reasonably sure that my conversion from hex string to byte array is working, but I'll include that method as well for a sanity check.
private static byte[] hexStringToByteArray(String s) throws IllegalArgumentException {
int len = s.length();
if (len % 2 == 1) {
throw new IllegalArgumentException("Hex string must have even number of characters");
}
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
for (int i = 0; i < len; i += 2) {
// Convert each character into a integer (base-16), then bit-shift into place
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
The end goal is to have a constant, valid PublicKey/PrivateKey object for testing. Any advice about how to generate those objects would be greatly appreciated.
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 = {...};
We've encountered a weird situation where the encryption method we're using in Java produces different output to openssl, despite them appearing identical in configuration.
Using the same key and IV, the text "The quick BROWN fox jumps over the lazy dog!" encrypts to base64'd strings...
openssl: A8cMRIrDVnBYj2+XEKaMOBQ1sufjptsAf58slR373JTeHGPWyRqJK+UQxvJ1B/1L
Java: A8cMRIrDVnBYj2+XEKaMOBQ1sufjptsAf58slR373JTEVySz5yJLGzGd7qsAkzuQ
This is our openssl call...
#!/bin/bash
keySpec="D41D8CD98F00B2040000000000000000"
ivSpec="03B13BBE886F00E00000000000000000"
plainText="The quick BROWN fox jumps over the lazy dog!"
echo "$plainText">plainText
openssl aes-128-cbc -nosalt -K $keySpec -iv $ivSpec -e -in plainText -out cipherText
base64 cipherText > cipherText.base64
printf "Encrypted hex dump = "
xxd -p cipherText | tr -d '\n'
printf "\n\n"
printf "Encrypted base64 = "
cat cipherText.base64
And this is our Java...
private static void runEncryption() throws Exception
{
String plainText = "The quick BROWN fox jumps over the lazy dog!";
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(hexToBytes("D41D8CD98F00B2040000000000000000"), 0, 16, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(hexToBytes("03B13BBE886F00E00000000000000000"));
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
String encryptedHexDump = bytesToHex(encrypted);
String encryptedBase64 = new String(DatatypeConverter.printBase64Binary(encrypted));
System.out.println("Encrypted hex dump = " + encryptedHexDump);
System.out.println("");
System.out.println("Encrypted base64 = " + encryptedBase64);
}
private static byte[] hexToBytes(String s)
{
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2)
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
return data;
}
final protected static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes)
{
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);
}
oopenssl output
Encrypted hex dump = 03c70c448ac35670588f6f9710a68c381435b2e7e3a6db007f9f2c951dfbdc94de1c63d6c91a892be510c6f27507fd4b
Encrypted base64 = A8cMRIrDVnBYj2+XEKaMOBQ1sufjptsAf58slR373JTeHGPWyRqJK+UQxvJ1B/1L
Java output
Encrypted hex dump = 03c70c448ac35670588f6f9710a68c381435b2e7e3a6db007f9f2c951dfbdc94c45724b3e7224b1b319deeab00933b90
Encrypted base64 = A8cMRIrDVnBYj2+XEKaMOBQ1sufjptsAf58slR373JTEVySz5yJLGzGd7qsAkzuQ
Are we missing something obvious? Or is there some hidden complexity?
I believe the difference is the padding, not the actual encrypted data.
Have you tried to decrypt the strings?
I believe they will show up as the same.
Why is the padding different? because they are either implementing it differently, or because one is provided a file, while the other a string, which in the end, when you read them, they are not the same thing (one has an EoF marker, for example).
BTW: Since it is CBC, Cipher Block Chaining, the whole last block is affected by this padding difference
It is indeed a problem of providing a string or a file. If you put a "\n" at the end of your Java code the result will be the same as in openSSL.
There are several reasons why these divergences can occur:
If you are providing OpenSSL and Java a password instead of a key, the key derivation from the password is different, unless you reimplement OpenSSL's algorithm in Java.
Still related to key derivation, the message digest used by OpenSSL by default depends on OpenSSL's version. Different versions can thus lead to different keys, and keys that differ from that computed by Java.
Finally, if you are sure to be using the same key through OpenSSL and Java, one reason why it can differ is because OpenSSL prepends Salted__<yoursalt> to the encrypted string.
Thus, in order to have the same output from Java as from OpenSSL, you need to prepend this to your result, like so:
byte[] rawEncryptedInput = cipher.doFinal(input.getBytes());
byte[] encryptedInputWithPrependedSalt = ArrayUtils.addAll(ArrayUtils.addAll(
"Salted__".getBytes(), SALT), rawEncryptedInput);
return Base64.getEncoder()
.encodeToString(encryptedInputWithPrependedSalt);
I have data encrypted in ColdFusion that I need to be able to decrypt and encrypt to the same exact value using Java. I was hoping someone may be able to help me with this. I will specify everything used in ColdFusion except for the actual PasswordKey, which I must keep secret for security purposes. The PasswordKey is 23 characters long. It uses upper and lowercase letters, numbers, and the + and = signs. I know this is a lot to ask, but any help would be greatly appreciated.
I tried using a Java encryption example I found online and just replacing the line below with the 23 characters used in our CF app:
private static final byte[] keyValue = new byte[] {'T', 'h', 'i', 's', 'I', 's', 'A', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y' };`
But I get the error:
java.security.InvalidKeyException: Invalid AES key length: 23 bytes
The CF code is:
Application.PasswordKey = "***********************";
Application.Algorithm = "AES";
Application.Encoding = "hex";
<cffunction name="encryptValue" access="public" returntype="string">
<cfargument name="strEncryptThis" required="yes">
<cfreturn Encrypt(TRIM(strEncryptThis), Application.PasswordKey, Application.Algorithm, Application.Encoding)>
</cffunction>
<cffunction name="decryptValue" access="public" returntype="string">
<cfargument name="strDecryptThis" required="yes">
<cfreturn Decrypt(TRIM(strDecryptThis), Application.PasswordKey, Application.Algorithm, Application.Encoding)>
</cffunction>
Your secret key is most likely a Base64 encoded key (23 chars should decode to about 16 bytes, which is the right length for a 128 bit key for AES).
So, in your java code, first run your secret key string through a Base64 decoder to get a byte[] of the appropriate length (16 bytes) for the AES algorithm.
128 but AES Encryption supports key key size of 16 bytes.
16 * 8 = 128 bits, even in the example the key is 16 bytes.
Sounds like your key is in Base64 so use Base64.decode(key or key.getBytes()) to get the byte array, check its in 16 bytes otherwise make it 16 bytes by padding.
Thank you everyone for your help. I wanted to post my final solution for others to use. I am including my entire encryption package code minus the specific password key (again for security). This code creates the same hex string as the CF code listed in the question, and decrypts it back to the proper english text string.
I found the bytesToHex and hexStringToByteArray functions in other question on stackoverflow, so my thanks to users maybeWeCouldStealAVan and Dave L. respectively also. I think I will look into other base 64 encoders/decoders in case the one from sun is ever made unavailable, but this definitely works for now. Thanks again.
package encryptionpackage;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import sun.misc.*;
public class encryption
{
// Note: The full CF default is "AES/ECB/PKCS5Padding"
private static final String ALGORITHM = "AES";
// The 24 character key from my CF app (base64 encoded)
// typically generated with: generateSecretKey("AES")
private static final String passKey = "***********************";
public static String encrypt(String valueToEnc) throws Exception
{
Key key = generateKey();
Cipher c = Cipher.getInstance(ALGORITHM);
c.init(Cipher.ENCRYPT_MODE, key);
byte[] encValue = c.doFinal(valueToEnc.getBytes());
String encryptedValue = bytesToHex(encValue);
return encryptedValue;
}
public static String decrypt(String encryptedValue) throws Exception
{
Key key = generateKey();
Cipher c = Cipher.getInstance(ALGORITHM);
c.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = hexStringToByteArray(encryptedValue);
byte[] decValue = c.doFinal(decordedValue);
String decryptedValue = new String(decValue);
return decryptedValue;
}
private static Key generateKey() throws Exception
{
byte[] keyValue;
keyValue = new BASE64Decoder().decodeBuffer(passKey);
Key key = new SecretKeySpec(keyValue, ALGORITHM);
return key;
}
public static String bytesToHex(byte[] bytes)
{
final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] hexChars = new char[bytes.length * 2];
int v;
for ( int j = 0; j < bytes.length; j++ )
{
v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] hexStringToByteArray(String s)
{
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2)
{
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
}
AES Encryption only supports a key-size of 128 bits, 192 bits or 256 bits.
http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
You can't just take any byte array and use it as an AES key. In the sample code you see above, the example cleverly used 16 characters, which corresponds to a 128-bit key.
This is because 1 character or rather 1 byte corresponds to 8 bits.
A 16-value byte array will then correspond to 16 * 8 = 128 bits
23 characters = 23 * 8 = 184 bits, thus it is an invalid key-size.
You need either 16 characters, 24 characters, or 32 characters.
That being said, using merely characters for AES encryption is extremely insecure. Do use a proper and secure random key for encrypt purposes.
To generate a secure and random AES key:
SecureRandom random = new SecureRandom();
byte [] secret = new byte[16];
random.nextBytes(secret);
http://docs.oracle.com/javase/6/docs/api/java/security/SecureRandom.html