Java AES Decryption with keyFile using BouncyCastle SSL - java

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).

Related

Cipher's doFinal() doesn't write bytes

This is my full code:
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Test {
public static void main(String[] args) throws Exception {
encrypt();
decrypt();
}
void encrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
SecureRandom secureRandom = new SecureRandom();
byte[] initializeVector = new byte[96 / Byte.SIZE];
secureRandom.nextBytes(initializeVector);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, initializeVector);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, p);
ByteBuffer ivBuffer = ByteBuffer.allocate(Integer.BYTES + cipher.getIV().length);
ivBuffer.putInt(cipher.getIV().length);
ivBuffer.put(cipher.getIV());
ivBuffer.flip();
dest.write(ivBuffer);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.delete(backupFile);
} catch (ShortBufferException e) {
//Should not happen!
throw new RuntimeException(e);
}
}
void decrypt() throws Exception {
Path file = Paths.get("path/to/file");
Path backupFile = file.getParent().resolve(file.getFileName().toString() + ".bak");
Files.deleteIfExists(backupFile);
Files.copy(file, backupFile);
try (FileChannel src = FileChannel.open(backupFile, READ);
FileChannel dest = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
ByteBuffer ivLengthBuffer = ByteBuffer.allocate(Integer.BYTES);
src.read(ivLengthBuffer);
ivLengthBuffer.flip();
int ivLength = ivLengthBuffer.getInt();
ByteBuffer ivBuffer = ByteBuffer.allocate(ivLength);
src.read(ivBuffer);
ivBuffer.flip();
byte[] iv = new byte[ivBuffer.limit()];
ivBuffer.get(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec p = new GCMParameterSpec(128, iv);
SecretKeySpec secretKeySpec =
new SecretKeySpec(MessageDigest.getInstance("MD5").digest(new byte[]{0x00}), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, p);
ByteBuffer readBuf = ByteBuffer.allocateDirect(8192);
ByteBuffer writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
while (src.read(readBuf) >= 0) {
if (cipher.getOutputSize(8192) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(8192));
}
readBuf.flip();
cipher.update(readBuf, writeBuf);
writeBuf.flip();
dest.write(writeBuf);
readBuf.clear();
writeBuf.clear();
}
if (cipher.getOutputSize(0) > writeBuf.capacity()) {
writeBuf = ByteBuffer.allocateDirect(cipher.getOutputSize(0));
}
cipher.doFinal(ByteBuffer.allocate(0), writeBuf);
writeBuf.flip();
dest.write(writeBuf);
Files.deleteIfExists(backupFile);
}
}
}
I found a strange issue: if the original file (unencrypted) is bigger than 4KB, upon decrypting, cipher.update(readBuf, writeBuf) will write nothing to the buffer, cipher.doFinal(ByteBuffer.allocate(0), writeBuf) also write nothing, and finally I get my data lost. Every calling to cipher.getOutputSize(8192), increases the result, I don't know why it happen but it may help.
Why is it happening and how can I fix it?
.update() is easy; SunJCE implements the GCM (and CCM) requirement that authenticated decryption not release (any) plaintext if the authentication fails; see How come putting the GCM authentication tag at the end of a cipher stream require internal buffering during decryption? and https://moxie.org/blog/the-cryptographic-doom-principle/ . Because the tag is at the end of the ciphertext, this means it must buffer all the ciphertext until (one of the overloads of) doFinal() is called. (This is why for a large file your reallocation of writeBuf to cipher.getOutputSize(8192) keeps growing as you keep reading and buffering more data.)
.doFinal() is harder; it is supposed to work. However, I've narrowed down the failure: it only happens when you use ByteBuffers not raw byte[] arrays -- which is implemented in javax.crypto.CipherSpi.bufferCrypt rather than dispatching to the implementation class; and the output ByteBuffer has no backing array (i.e. was direct-allocated); and the plaintext is more than 4096 bytes. I'll try to look deeper into why this fails, but in the meantime changing either of the first two fixes it (or limiting your data to 4096 bytes, but presumably you don't want that).

Java - decrypting messages using a private key [duplicate]

I'm writing a program which takes as input from the console - the name of a zip file, name of a zip file to be made containing the (de/en)crypted files generated from the first zip and a file containing the public key. I get the exception when decrypting:
exception Exception in thread "main" javax.crypto.BadPaddingException: Decryption error
at sun.security.rsa.RSAPadding.unpadV15(RSAPadding.java:380)
at sun.security.rsa.RSAPadding.unpad(RSAPadding.java:291)
at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:363)
at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at com.Main.decrypt(Main.java:67)
at com.Main.main(Main.java:201)
Can't figure out why I get this exception?
Public key:
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCE3pA746UfpC8sFk8ZJp0yupyJqj5jy6cjdxUYoP7mCm7c0mqQDeCcDNBYW2eSozCioPrH/9L+CDQEPLYakoem+jFnUKDH5+pru/0PJTJJF8Xh/ZT9eJlvsYBr1/qSfICf6RTs7kzwq9IuSZBw7/tfNEF9i0A8FVox6HOopXod1QIDAQAB
Private key:
MIICXQIBAAKBgQCE3pA746UfpC8sFk8ZJp0yupyJqj5jy6cjdxUYoP7mCm7c0mqQDeCcDNBYW2eSozCioPrH/9L+CDQEPLYakoem+jFnUKDH5+pru/0PJTJJF8Xh/ZT9eJlvsYBr1/qSfICf6RTs7kzwq9IuSZBw7/tfNEF9i0A8FVox6HOopXod1QIDAQABAoGANOFrYBqK5lvu1koOswDWQZFZqcSSzh8IZyoGwGWa7S0r0EECXlDXmuPSq8e9IfRG8ALHrH+ZlrbnFOSgyVSWHfpj3aH+qknoSX5TW2rMQHih8865xuqheMQ+RTZ7+BRDqNsYkzxB/Z8mqzpoJQSYf+H7nWxdDCgAJVYZzxl3DmUCQQD32iEjnwiwUjii8slcmvCEZl+z84DWNdvJOg6Z38sI4AvrfpKc1WAcDg1rNZCKrRgokh54wpLt08cpFcrD04c3AkEAiTzDmc0bdgfg5wj6xHFZpYlBwiGm/bjOR2PS57P0GNU5PsDllRbFqIuzArITutO5lvZZImzuYz7Lf+cQ73pxUwJBAOdEwmdaneDo17A0m2+to3/nhqWDMVSwLMU3RyiNigZeCMFU+bkd4PBMrHi9IoJDwacZsRU9eZwxYEUV8H2Jg0ECQEEkOqRSm2pXKwX/WSjNtQPCNxhy6NUeV6vDUmTxIjh3XYjP/ynZeVEbnoj1BjB0N2/U11Jj6nPpZqb7gyppMEkCQQCoGdVYDipU+hMMnvxa0zOIyQc/a+HE0lESqn+2ZPafYi9Z1RldRMvUXhP8U7s+OuhRwprdw2ivvOFrnWyz9lL2
The code for the program is bellow . Any help is wellcomed :)
package com;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Scanner;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.crypto.Cipher;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
public class Main {
public final static int BUFFER_SIZE = 117;
public static void decrypt(String originalZipFileName, String newZipFileName, String privateKeyFileName) throws Exception {
byte[] buffer = new byte[128];
ZipFile originalZipFile = new ZipFile(originalZipFileName);
ZipOutputStream newZipFile = new ZipOutputStream(new FileOutputStream(newZipFileName));
Enumeration<? extends ZipEntry> zipEntries = originalZipFile.entries();
String privateKey = getKeyString(privateKeyFileName);
PrivateKey key = makePrivateKey(privateKey);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, key);
while(zipEntries.hasMoreElements()){
ZipEntry entry = zipEntries.nextElement();
ZipEntry copy = new ZipEntry(entry.getName());
newZipFile.putNextEntry(copy);
InputStream inputEntry = originalZipFile.getInputStream(entry);
while(inputEntry.read(buffer) != -1){
newZipFile.write(cipher.doFinal(buffer));
}
newZipFile.closeEntry();
inputEntry.close();
}
newZipFile.close();
originalZipFile.close();
}
public static void encrypt(String originalZipFileName, String newZipFileName, String publicKeyFileName) throws Exception{
byte[] buffer = new byte[BUFFER_SIZE];
ZipFile originalZipFile = new ZipFile(originalZipFileName);
ZipOutputStream newZipFile = new ZipOutputStream(new FileOutputStream(newZipFileName));
Enumeration<? extends ZipEntry> zipEntries = originalZipFile.entries();
String publicKey = getKeyString(publicKeyFileName);
PublicKey key = makePublicKey(publicKey);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, key);
while(zipEntries.hasMoreElements()){
ZipEntry entry = zipEntries.nextElement();
ZipEntry copy = new ZipEntry(entry.getName());
newZipFile.putNextEntry(copy);
InputStream inputEntry = originalZipFile.getInputStream(entry);
while(inputEntry.read(buffer) != -1){
newZipFile.write(cipher.doFinal(buffer));
}
newZipFile.closeEntry();
inputEntry.close();
}
newZipFile.close();
originalZipFile.close();
}
public static String getKeyString(String fileName){
String key = new String();
try {
BufferedReader buf = new BufferedReader(new FileReader(fileName));
key = buf.readLine();
} catch ( IOException e) {
e.printStackTrace();
}
return key.trim();
}
public static PublicKey makePublicKey(String stored) throws GeneralSecurityException {
byte[] data = Base64.getDecoder().decode(stored);
X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
KeyFactory fact = KeyFactory.getInstance("RSA");
return fact.generatePublic(spec);
}
public static PrivateKey makePrivateKey(String stored) throws GeneralSecurityException, Exception {
/*byte[] data = Base64.getDecoder().decode(stored);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(data);
KeyFactory fact = KeyFactory.getInstance("RSA");
return fact.generatePrivate(spec);*/
byte[] data = Base64.getDecoder().decode(stored);
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(new ASN1Integer(0));
ASN1EncodableVector v2 = new ASN1EncodableVector();
v2.add(new ASN1ObjectIdentifier(PKCSObjectIdentifiers.rsaEncryption.getId()));
v2.add(DERNull.INSTANCE);
v.add(new DERSequence(v2));
v.add(new DEROctetString(data));
ASN1Sequence seq = new DERSequence(v);
byte[] privKey = seq.getEncoded("DER");
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privKey);
KeyFactory fact = KeyFactory.getInstance("RSA");
PrivateKey key = fact.generatePrivate(spec);
return key;
}
public static void main(String[] args) throws Exception {
Scanner scan = new Scanner(System.in);
System.out.println("Enter type of operation:");
String line = scan.nextLine();
if(line.equals("encrypt")){
System.out.println("Enter name of original ZIP file:");
String originalZipFileName = scan.nextLine();
System.out.println("Enter name of new ZIP file:");
String newZipFileName = scan.nextLine();
System.out.println("Enter name of file containg public key:");
String publicKeyFileName = scan.nextLine();
encrypt(originalZipFileName, newZipFileName, publicKeyFileName);
}
if(line.equals("decrypt")){
System.out.println("Enter name of original ZIP file:");
String originalZipFileName = scan.nextLine();
System.out.println("Enter name of new ZIP file:");
String newZipFileName = scan.nextLine();
System.out.println("Enter name of file containg private key:");
String privateKeyFileName = scan.nextLine();
decrypt(originalZipFileName, newZipFileName, privateKeyFileName);
}
}
}
PS: Updated decrypt method. Still gives same error.
public static void decrypt(String originalZipFileName, String newZipFileName, String privateKeyFileName) throws Exception {
byte[] buffer = new byte[128];
ZipFile originalZipFile = new ZipFile(originalZipFileName);
ZipOutputStream newZipFile = new ZipOutputStream(new FileOutputStream(newZipFileName));
Enumeration<? extends ZipEntry> zipEntries = originalZipFile.entries();
String privateKey = getKeyString(privateKeyFileName);
PrivateKey key = makePrivateKey(privateKey);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, key);
while(zipEntries.hasMoreElements()){
ZipEntry entry = zipEntries.nextElement();
ZipEntry copy = new ZipEntry(entry.getName());
newZipFile.putNextEntry(copy);
InputStream inputEntry = originalZipFile.getInputStream(entry);
while(inputEntry.read(buffer) != -1){
newZipFile.write(cipher.doFinal(buffer));
}
newZipFile.closeEntry();
inputEntry.close();
}
newZipFile.close();
originalZipFile.close();
}
Jozef is right.
When you create cipher with default parameters, it defaults to "RSA/ECB/PKCS1Padding". You should specify padding explicitly, if you don't like nasty surprises. Because other security providers might have different default parameters. And you never know in advance which security settings each specific JRE has.
So PKCS1 padding adds 11 bytes to your original data increasing it from 117 bytes to 128 bytes. You should take into account that these numbers are specific to 1024 bit RSA keys (which are marginally secure) and will be different for longer keys. Since you are loading the key from a file consider checking its length.
#Test
public void testPadding() throws Exception {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024, random);
KeyPair keyPair = keyGen.generateKeyPair();
/* constant 117 is a public key size - 11 */
byte[] plaintext = new byte[117];
random.nextBytes(plaintext);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] ciphertext = cipher.doFinal(plaintext);
System.out.println(plaintext.length + " becomes " + ciphertext.length);
}
This prints
117 becomes 128
And finally, consider using AES instead of RSA for file encryption.
So to fix the problem you need to use buffer of size public key length - 11 (117) for encryption and public key size (128) for decryption.
Change
outputFile.write(cipher.doFinal(buffer), 0, read);
to
outputFile.write(cipher.doFinal(buffer));
because buffer read is 117 bytes and size of doFinal result is 128 bytes.
Also you need to buffer input streams. When you are reading from file, it can be slow sometimes and then InputStream will read less data than buffer may contain. By using BufferedInputStream one ensures that there is enough data before read call returns. However, for decryption it's crucial to have the full block of data
InputStream inputEntry = new BufferedInputStream(originalZipFile.getInputStream(entry));
while((read = inputEntry.read(buffer)) != -1){
outputFile.write(cipher.doFinal(buffer), 0, read);
}
You have a problem here. read is the size of the plaintext that was read, not the ciphertext. You should remove the 2nd and 3rd parameters altogether.
It is also a waste of time and space to write the ciphertext to an intermediate file. Just write it straight to the zip stream.
The decrypt method's byte array should be 256 bytes in length as it is the default output size of the algorithm (The extra bytes result in this length). Change byte[] buffer = new byte[128]; to byte[] buffer = new byte[256];.

Decrypting a string with RSA returns additional zeros at the beginning

I'm trying to generate an AES key, encrypt it and decrypt it using RSA.
It kind of works, except that after decrypting the data and encoding with Base64 I get a pile of "A" letters before my actual string(the base64-encoded AES key). I guess these were zeros in byte.
The "RSA/ECB/NoPadding" parameters are mandatory. What am I doing wrong ? I need it to return the original string/bytes.
package szyfrator;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.tools.bzip2.CBZip2OutputStream;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.sun.org.apache.xml.internal.security.utils.Base64;
public class Cryptography {
private static byte[] aesKey;
private static String base64AESKey;
private static byte[] encryptedAESKey;
private static String base64AESEncryptedKey;
private static byte[] aesKeyTransformed;
public static void main(String args[]){
Cryptography.generateAESkey();
Cryptography.encryptAESKey(new File("G:\\HASHBABYHASH\\public.txt"));
Cryptography.decryptAESKey(new File("G:\\HASHBABYHASH\\private.txt"));
System.out.println("String: " + Base64.encode(Cryptography.getAesKey()) + "\r\n");
System.out.println("Encrypted string: " + Cryptography.getBase64EncryptedKey() + "\r\n");
System.out.println("Decrypted String: " + Base64.encode(Cryptography.getAesKeyTransformed()) + "\r\n");
}
public static void generateAESkey(){
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey secretKey = keyGen.generateKey();
byte[] keyBytes = secretKey.getEncoded();
base64AESKey = Base64.encode(keyBytes);
aesKey = keyBytes;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public static void encryptAESKey(File publicKeyFile){
try {
FileInputStream input = new FileInputStream(publicKeyFile);
byte[] decoded = Base64.decode(IOUtils.toByteArray(input));
X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(publicSpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
encryptedAESKey = cipher.doFinal(aesKey);
base64AESEncryptedKey = Base64.encode(encryptedAESKey);
input.close();
}catch (Exception e) {
e.printStackTrace();
}
}
public static void decryptAESKey(File privateKeyFile){
try {
FileInputStream input = new FileInputStream(privateKeyFile);
byte[] decoded = Base64.decode(IOUtils.toByteArray(input));
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
aesKeyTransformed = cipher.doFinal(encryptedAESKey);
input.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
Here is the result:
String: xVwH7Nbz84emVoH0J31sRHC+B669T9wCUVlTDhYgXiI=
Encrypted string: INTA8rx46hX6bZbDIl4iiWsUGO4ywCW0Aee1reqQ3wR5X7He5ztLHvyZoa0WZmUGYbYwprNGffRI
OVJFxczMHkxUfHU1WWCTzcfNylD+sWObIYrbyc13aZi9OL/r1GXuaGtkIgTJyqv0QPHfIri7iaH3
Lr/F4EIcyphJM3E2reQ=
Decrypted String: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxVwH7Nbz84emVoH0J31sRHC+
B669T9wCUVlTDhYgXiI=
In RSA some data is encoded into a large number and calculated upon. NoPadding (unpadded or textbook RSA) means that you're fully responsible for the proper encoding of the message. All of the calculations are done against a large modulus (should be at least 2048 bit nowadays). Since Java assumes big-endian numbers, your message is encoded into the least significant bytes automatically, but the decryption returns the decoded message in the same size of the modulus, because it cannot know whether the leading zero-bytes where intentional or not.
In order to make this calculation correct and secure it is necessary to apply padding. The old-style PKCS#1 v1.5 padding is not considered secure nowadays, but it only has 11 bytes of overhead (only 2048/8-11=245 bytes can be encrypted with a key of 2048 bit). The newer PKCS#1 v2.1 padding (OAEP) is considered secure and should be used here. It does have an overhead of 42 bytes if SHA-1 is used.
The "RSA/ECB/NoPadding" parameters are mandatory.
This is really bad, because it is very insecure: Which attacks are possible against raw/textbook RSA?
If you're not willing to simply change the cipher string to Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");, you will have to remove the leading zeros yourself. The problem is of course that this "zero-padding" mode is ambiguous and if the plaintext begins with a 0x00 byte, you will not be able to distinguish it from a padding byte and will have to remove it, thus breaking your plaintext. If the plaintext is an AES key as in your case, there is a 0.3% chance that it begins with a 0x00 byte and thus breaks the key. You will have to make sure that the key is actually correct and fill up with zero bytes if it has not the correct length.
Here is how you can remove leading zero bytes:
byte[] unpadZeros(byte[] in) {
int i = 0;
while(in[i] == 0) i++;
return Arrays.copyOfRange(in, i, in.length);
}
If you know that you're decrypting an AES key, then it's possible to make the unpadding no produce wrong data:
byte[] unpadZerosToGetAesKey(byte[] in) {
int i = 0;
while(in[i] == 0) i++;
int len = in.length - i;
if (len <= 16) len = 16;
else if (len <= 24) len = 24;
else len = 32;
return Arrays.copyOfRange(in, in.length - len, in.length);
}

Encrypt message for Web Push API in 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 = {...};

Determining that decryption has occurred

I am developing a GUI based encryptor/decryptor based on AES-128 bit symmetric encryption.
My problem is that how to determine that decryption has not occurred and show a dialog box "Decryption Failed". The code I have written would always generate a file without a .enc extension regardless of the fact that it is still encrypted !
Hoping to get a answer as always from Stack Overflow's top notch programmers :)
Do note that the decryption process doesn't fail or throws exception ! It's just the fact that it generates a file that's still not decrypted. That we have to stop and that's what I meant !
Code here: (Sorry for bad indentation !)
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.MessageDigest;
public class FileEncryptor{
private String algo;
private String path;
private String password;
public FileEncryptor(String algo,String path, String password) {
this.algo = algo; //setting algo
this.path = path;//setting file path
this.password = password;
}
public void encrypt() throws Exception{
SecureRandom padding = new SecureRandom();
byte[] salt = new byte[16];
padding.nextBytes(salt);
//generating key
byte k[] = password.getBytes();
MessageDigest sha = MessageDigest.getInstance("SHA-1");
k = sha.digest(k);
k = Arrays.copyOf(k, 16);
SecretKeySpec key = new SecretKeySpec(k,algo);
//creating and initialising cipher and cipher streams
Cipher encrypt = Cipher.getInstance(algo);
encrypt.init(Cipher.ENCRYPT_MODE, key);
//opening streams
FileOutputStream fos =new FileOutputStream(path+".enc");
try(FileInputStream fis =new FileInputStream(path)){
try(CipherOutputStream cout=new CipherOutputStream(fos, encrypt)){
copy(fis,cout);
}
}
}
public void decrypt() throws Exception{
SecureRandom padding = new SecureRandom();
byte[] salt = new byte[16];
padding.nextBytes(salt);
//generating same key
byte k[] = password.getBytes();
MessageDigest sha = MessageDigest.getInstance("SHA-1");
k = sha.digest(k);
k = Arrays.copyOf(k, 16);
SecretKeySpec key = new SecretKeySpec(k,algo);
//creating and initialising cipher and cipher streams
Cipher decrypt = Cipher.getInstance(algo);
decrypt.init(Cipher.DECRYPT_MODE, key);
//opening streams
FileInputStream fis = new FileInputStream(path);
try(CipherInputStream cin=new CipherInputStream(fis, decrypt)){
try(FileOutputStream fos =new FileOutputStream(path.substring(0,path.lastIndexOf(".")))){
copy(cin,fos);
}
}
}
private void copy(InputStream is,OutputStream os) throws Exception{
byte buf[] = new byte[4096]; //4K buffer set
int read = 0;
while((read = is.read(buf)) != -1) //reading
os.write(buf,0,read); //writing
}
public static void main (String[] args)throws Exception {
System.out.println("Enter Password: ");
new FileEncryptor("AES","sample.txt",new java.util.Scanner(System.in).nextLine()).encrypt();
new FileEncryptor("AES","sample.txt.enc",new java.util.Scanner(System.in).nextLine()).decrypt();
}
}
Without looking at the API calls, the decrypt methods should throw an exception if an error occurs. In your exception handler, you can set a flag that will allow you to display an error message. You can also delay the decrypted file creation till after successful decryption (or at least till after the first block has been successfully decrypted). If decryption then fails further along the line, you can delete the (essentially temporary) decrypted output file and display the error message.
[edit]
I slightly misunderstood the original post, so some suggestions to check for failed decryption (note that these are higher level than AES, so it might be specific to your application only):
Add a checksum to the plaintext data before encryption
Append other metadata (file size, user, date, etc) to the plaintext, and check for these when decrypting
Usually, a padding exception would occur on decryption - check for these (and any other giveaways)
Use PKI (public key infrastructure) functionality such as signatures (this is outside the scope of this answer, and possibly outside the scope of the problem you're trying to solve)
I suggest appending a constant, rather than a checksum, to your data before encryption, and verifying it after encryption.
And the encryption algorithm should use chaining, that means avoid ECB (see here why: http://bobnalice.wordpress.com/2009/01/28/friends-don%E2%80%99t-let-friends-use-ecb-mode-encryption).
Using a constant with chaining, is nearly as good as a checksum and much simpler.

Categories