What algorithm to use when creating an encrypting log4j appender - java

I've created a RollingFileAppender which encrypts the output to a log4j log file.
Currently it uses AES/ECB/NoPadding, and it works fine.
Here is how we create the cipher
public static Cipher getCipher(boolean encrypt) throws Exception {
//https://en.wikipedia.org/wiki/Stream_cipher
byte[] key = ("sometestkey").getBytes("UTF-8");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 16); // use only first 128 bit
Key k = new SecretKeySpec(key,"AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
if (encrypt) {
cipher.init(Cipher.ENCRYPT_MODE, k);
} else {
cipher.init(Cipher.DECRYPT_MODE, k);
}
return cipher;
}
Here's how we create the appender :
public class EncryptingRollingFileAppender extends RollingFileAppender {
private CipherOutputStream s;
private Cipher cipher;
public EncryptingRollingFileAppender() {super();}
public EncryptingRollingFileAppender(Layout layout, String filename, boolean append) throws IOException {super(layout, filename, append);}
public EncryptingRollingFileAppender(Layout layout, String filename) throws IOException {super(layout, filename);}
#Override
protected OutputStreamWriter createWriter(OutputStream outputStream) {
if (cipher==null) {
try {
cipher = DecryptionTools.getCipher(true);
s = new CipherOutputStream(outputStream, cipher);
} catch (Throwable t) {
throw new RuntimeException("failed to initialise encrypting file appender",t);
}
}
OutputStreamWriter out = super.createWriter(s);
return out;
}
}
We can decrypt the file by using
getCipher(false)
to create an appropriate decryption stream.
The issue is that our security team are haggling about key management.
They don't like the use symetric key encryption, and would prefer us to use a key pair rather than a simple password that we have to manage in some way.
Does anyone know of a non-padding ECB encryption technique that would use a key pair, and would be appropriate for this kind of stream encryption and decryption?

The comments suggesting using hybrid encryption and PGP are right.
PGP is the defacto standard for hybrid encryption of files, and it is a much more robust solution that ECB mode AES.
Due to the nature of PGP it is going to work slightly differently to your existing solution.
PGP messages have a header and a footer, so you will want each file to be individually encrypted (you wont just be able to decrypt individual blocks like you can with plain ECB mode encryption).
It looks like you are using log4j 1.2, I have created a working implementation of a PGP encrypting RollingFileAppender.
To use the example, generate an armour encoded PGP public key, run the main class then decrypt the file with any PGP tool (I used GnuPG for creating the keys and decrypting).
The example was built against log4j:log4j:1.2.17 and org.bouncycastle:bcpg-jdk15on:1.56

You are pretty close to what you want to achieve. Using Log4j 1.2 (because you cannot subclass RollingFileAppender directly in Log4j 2) you can generate a password on the fly for each log file, encrypt the password with RSA and store next to it. Then use the password to produce an AES CipherOutputStream for the log appender.
public class EncryptingRollingFileAppender extends RollingFileAppender {
private CipherOutputStream s;
private Cipher cipher;
private byte[] secretKey;
public EncryptingRollingFileAppender(Layout layout, String filename, boolean append) throws IOException {
super(layout, filename, append);
writeKeyFile(filename);
}
public EncryptingRollingFileAppender(Layout layout, String filename) throws IOException {
super(layout, filename);
writeKeyFile(filename);
}
private void writeKeyFile(final String logfilename) throws IOException {
final int dot = logfilename.lastIndexOf('.');
final String keyfilename = (dot == -1 ? logfilename : logfilename.substring(0, dot)) + ".key";
try (FileOutputStream out = new FileOutputStream(keyfilename)) {
out.write(DecryptionTools.encryptPasswordBase64(secretKey).getBytes(ISO_8859_1));
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | KeyStoreException
| CertificateException e) {
}
}
#Override
protected OutputStreamWriter createWriter(OutputStream outputStream) {
System.out.println("createWriter()");
if (cipher == null) {
secretKey = DecryptionTools.generateRandomKey(16).getBytes(ISO_8859_1);
try {
cipher = DecryptionTools.getCipher(true, secretKey);
} catch (InvalidKeyException e) {
System.out.println("InvalidKeyException");
}
s = new CipherOutputStream(outputStream, cipher);
}
OutputStreamWriter out = super.createWriter(s);
return out;
}
}
You´ll need several helper functions for reading the private key from a file or from a Java key store which can be found here.
The test file TestEncryptingRollingFileAppender shows how to write an encrypted log and read it back.
import static com.acme.DecryptionTools.getCipher;
import static com.acme.DecryptionTools.decryptPasswordBase64;
public class TestEncryptingRollingFileAppender {
#SuppressWarnings("deprecation")
#Test
public void testAppender() throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, KeyStoreException, CertificateException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException {
final File logfile = File.createTempFile("testlog_", ".log");
final String logfilename = logfile.getAbsolutePath();
final Logger lggr = LogManager.getLogger(TestEncryptingRollingFileAppender.class);
final EncryptingRollingFileAppender appender = new EncryptingRollingFileAppender(new SimpleLayout(), logfilename, true);
appender.append(new LoggingEvent(lggr.getClass().getName(), lggr, Priority.INFO, "Test Log Line #1", null));
appender.append(new LoggingEvent(lggr.getClass().getName(), lggr, Priority.INFO, "Test Log Line #1", null));
final int dot = logfilename.lastIndexOf('.');
byte[] key = decryptPasswordBase64(new String(Files.readAllBytes(Paths.get(logfilename.substring(0, dot)+".key"))));
StringBuilder logContent = new StringBuilder();
try (FileInputStream instrm = new FileInputStream(logfilename);
CipherInputStream cistrm = new CipherInputStream(instrm, getCipher(false, key))) {
int c;
while ((c=cistrm.read())!=-1)
logContent.append((char) c);
}
assertEquals("INFO - Test Log Line #1\r\nINFO - Test Log Line #1", logContent.toString());
}
}

Related

How do I resolve an IllegalBlockSizeException?

I'm currently attempting to implement an encryption and decryption function for a basic Java program, using the following methods:
private static String encryptString(PublicKey publicKey, String message) throws Exception {
Cipher encrypt = Cipher.getInstance("RSA/ECB/PKCS1Padding");
encrypt.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = encrypt.doFinal(message.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
private static String decryptString(PrivateKey privateKey, String message) throws Exception {
Cipher decrypt = Cipher.getInstance("RSA/ECB/PKCS1Padding");
decrypt.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = decrypt.doFinal(message.getBytes());
return Base64.getEncoder().encodeToString(decrypted);
}
Encoding the message using the public key appears to work correctly. However, when attempting to decrypt it using the private key, javax.crypto throws an IllegalBlockSizeException saying data must not be longer than 128 bytes.

Should I use MessageDigest to verify a digital signature that signed in C#?

I'm new in Java and want to sign some data in c# and verify in Java with RSA and SHA512.
C#:
static string SignData(string message, RSAParameters privateKey)
{
byte[] signedBytes;
using (var rsa = new RSACryptoServiceProvider())
{
var encoder = new UTF8Encoding();
byte[] originalData = encoder.GetBytes(message);
rsa.ImportParameters(privateKey);
signedBytes = rsa.SignData(originalData, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
rsa.PersistKeyInCsp = false;
}
return Convert.ToBase64String(signedBytes);
}
Java:
static boolean verifySignature512(String message, String sign, PublicKey publicKey) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] contentDigest = digest.digest(message.getBytes(Charset.forName("UTF-8")));
Signature signature = Signature.getInstance("Sha512withRSA");
signature.initVerify(publicKey);
signature.update(contentDigest);
return signature.verify(Base64.getDecoder().decode(sign));
}
I used RSA and public and private keys. Java function always return false with no error or exception.
If I remove MessageDigest like below it starts working, Is it ok and secure?
static boolean verifySignature512(String message, String sign, PublicKey publicKey) throws Exception {
// MessageDigest digest = MessageDigest.getInstance("SHA-512");
// byte[] contentDigest = digest.digest(message.getBytes(Charset.forName("UTF-8")));
Signature signature = Signature.getInstance("Sha512withRSA");
signature.initVerify(publicKey);
signature.update(message.getBytes(Charset.forName("UTF-8")));
return signature.verify(Base64.getDecoder().decode(sign));
}
The signature verification on Java-side has to fail as you are using different hashing algorithms on both sides.
In C# you are using SHA1 ('HashAlgorithmName.SHA1') and the Java-part is using SHA512 ('Signature signature = Signature.getInstance("Sha512withRSA");').
The following code is using SHA1 as hash algorithm but you can easily change this (on all codelines :-) to SHA256 or SHA512.
The C#-code verifies the signature with the public key, the same public key (encoded as PEM) is used in the Java-code
for verification only.
Security warning: My example codes are using UNSECURE 512-bit RSA-keys that should not used in production.
There is not proper exception handling and you are using the padding 'RSASignaturePadding.Pkcs1' which should
NOT used anymore as well.
This is the output of my C#-code:
Should I use MessageDigest to verify a digital signature that signed in C#?
signedData: mU2bcCMEhG13xG9sKwhaA//dnw2+rbLkwz2737cNU5kb2EBenJIEJ+bA596XccCVKUKPanxMUFoVw2fl8HhCNw==
The data was verified.
That's the Java-output:
RSA instance: SHA1withRSA
The data was verified.
C#-code:
using System;
using System.Security.Cryptography;
using System.Text;
class RSACSPSample {
static void Main() {
try {
Console.WriteLine("Should I use MessageDigest to verify a digital signature that signed in C#?");
// Create a UnicodeEncoder to convert between byte array and string.
ASCIIEncoding ByteConverter = new ASCIIEncoding();
string message = "this is the important message to sign";
// get private and public key ### SAMPLE and UNSECURE 512 bit RSA keypair
var privateKey = "<RSAKeyValue><Modulus>mfgthqgvK5P6kP00ojzA68+tGMwjEacduojFSukazKPXrZ8Q5XjzfqgJmDQ3wcWe3hWK92O3z/tmAuN47KA0ZQ==</Modulus><Exponent>AQAB</Exponent><P>8VCRao0hZmIv5gVGFLqOD/7n6TQKlekA96U1QVzimKM=</P><Q>o1bchWA5ddDd59FED37QcrakoTXNoxRspFtsLDKEp1c=</Q><DP>ugF0VUE7wYNlkFP4VPoHjuTZNbRbhHn5uOmrRxqlvyk=</DP><DQ>XoGggC9Hr8pUyo9DIPAP7X+Ny5TU0Vm87m/TK9Ni+2s=</DQ><InverseQ>YqOHEP8dgCvz5Q8nhpQgdrKfdlmjkNAFxKF7j3pm09I=</InverseQ><D>mCpGy/rxS08e5iXn26LRQvvm5UfyLKMNZWmAGk2QF8cRGFB7dds/SI9wGTC9xyOoF4N2kWzYdLx+dYbR9lqwbQ==</D></RSAKeyValue>";
var publicKey = "<RSAKeyValue><Modulus>mfgthqgvK5P6kP00ojzA68+tGMwjEacduojFSukazKPXrZ8Q5XjzfqgJmDQ3wcWe3hWK92O3z/tmAuN47KA0ZQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
// Create a new instance of the RSACryptoServiceProvider class
RSACryptoServiceProvider RSAalg = new RSACryptoServiceProvider(512);
RSAalg.PersistKeyInCsp = false;
RSAalg.FromXmlString(privateKey);
RSAParameters rsaParameters = RSAalg.ExportParameters(true);
String signedData = SignData(message, rsaParameters);
Console.WriteLine("signedData: " + signedData);
// verify with xml-public key
RSAalg.FromXmlString(publicKey);
rsaParameters = RSAalg.ExportParameters(false);
bool verifiedData = VerifyData(message, signedData, rsaParameters);
// Verify the data and display the result to the
// console.
if (VerifyData(message, signedData, rsaParameters)) {
Console.WriteLine("The data was verified.");
}
else {
Console.WriteLine("The data does not match the signature.");
}
}
catch(ArgumentNullException) {
Console.WriteLine("The data was not signed or verified");
}
}
static string SignData(string message, RSAParameters privateKey)
{
byte[] signedBytes;
using (var rsa = new RSACryptoServiceProvider())
{
var encoder = new UTF8Encoding();
byte[] originalData = encoder.GetBytes(message);
rsa.ImportParameters(privateKey);
signedBytes = rsa.SignData(originalData, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
rsa.PersistKeyInCsp = false;
}
return Convert.ToBase64String(signedBytes);
}
public static bool VerifyData(string message, string signedData, RSAParameters rsaParameters)
{
byte[] messageBytes;
byte[] signedBytes;
using (var rsa = new RSACryptoServiceProvider())
try
{
var encoder = new UTF8Encoding();
messageBytes = encoder.GetBytes(message);
signedBytes = Convert.FromBase64String(signedData);
rsa.ImportParameters(rsaParameters);
return rsa.VerifyData(messageBytes, new SHA1CryptoServiceProvider(), signedBytes);
}
catch(CryptographicException e) {
Console.WriteLine(e.Message);
return false;
}
}
}
Java-code:
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class MainSha1 {
public static void main(String[] args) throws GeneralSecurityException {
System.out.println("Should I use MessageDigest to verify a digital signature that signed in C#?");
String message = "this is the important message to sign";
// this is a SAMPLE and UNSECURE RSA 512 bit key
String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJn4LYaoLyuT+pD9NKI8wOvPrRjMIxGn\n" +
"HbqIxUrpGsyj162fEOV4836oCZg0N8HFnt4Vivdjt8/7ZgLjeOygNGUCAwEAAQ==\n" +
"-----END PUBLIC KEY-----";
String signedData = "HS4qvrXpqu97me7yDt9lWXp+QLjKMO8FY4kiUiGhMhi6KmXQXCtmcUWSbg0i+LXv7u5ueRiQNeBnu6UCbPhZLg==";
String rsaInstanceString = "SHA1withRSA";
System.out.println("RSA instance: " + rsaInstanceString);
PublicKey publicKey = getPublicKeyFromString(publicKeyPem);
boolean verifyData = verifyRsa(publicKey, rsaInstanceString, message.getBytes(StandardCharsets.UTF_8), Base64.getDecoder().decode(signedData));
if (verifyData = true) {
System.out.println("The data was verified.");
} else {
System.out.println("The data could NOT get verified.");
}
}
public static PublicKey getPublicKeyFromString(String key) throws GeneralSecurityException {
String publicKeyPEM = key;
publicKeyPEM = publicKeyPEM.replace("-----BEGIN PUBLIC KEY-----", "");
publicKeyPEM = publicKeyPEM.replace("-----END PUBLIC KEY-----", "");
publicKeyPEM = publicKeyPEM.replaceAll("[\\r\\n]+", "");
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey pubKey = (PublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded));
return pubKey;
}
public static Boolean verifyRsa(PublicKey publicKey, String rsaInstanceString, byte[] messageByte,
byte[] signatureByte) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
Signature publicSignature = Signature.getInstance(rsaInstanceString);
publicSignature.initVerify(publicKey);
publicSignature.update(messageByte);
return publicSignature.verify(signatureByte);
}
}

Encrypting and Decrypting password in java

Im Trying to do my homework to create a class called Password that implements the Encryptable interface.
Im trying using RSA Algorythm.
I use some RSA code references from the Google and resulting my code below.
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Key;
import java.util.Arrays;
import javax.crypto.Cipher;
import java.util.Scanner;
public class Password
{
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
String password = sc.nextLine();
KeyPair keyPair = RSAKeyPair.keyPairRSA();
Key publicKey = keyPair.getPublic();
Key privateKey = keyPair.getPrivate();
System.out.println("Original: " + password);
byte[] encrypted = RSAEncryptDecrypt.encrypt(password, privateKey);
System.out.println("Encrypted: " + new String(encrypted));
byte[] decrypted = RSAEncryptDecrypt.decrypt(encrypted, publicKey);
System.out.println("Decrypted: " + new String(decrypted));
}
}
final class RSAConstants {
private RSAConstants() {
}
public static final String ALGORITHM = "RSA";
public static final int ALGORITHM_BITS = 2048;
}
class RSAKeyPair {
public static KeyPair keyPairRSA() {
KeyPairGenerator generator = null;
try {
generator = KeyPairGenerator.getInstance(RSAConstants.ALGORITHM);
} catch (Exception e) {
e.printStackTrace();
}
if (generator != null) {
generator.initialize(RSAConstants.ALGORITHM_BITS);
KeyPair keyPair = generator.genKeyPair();
return keyPair;
}
return null;
}
}
class RSAEncryptDecrypt {
public static byte[] encrypt(String original, Key privateKey) {
if (original != null && privateKey != null) {
byte[] bs = original.getBytes();
byte[] encData = convert(bs, privateKey, Cipher.ENCRYPT_MODE);
return encData;
}
return null;
}
public static byte[] decrypt(byte[] encrypted, Key publicKey) {
if (encrypted != null && publicKey != null) {
byte[] decData = convert(encrypted, publicKey, Cipher.DECRYPT_MODE);
return decData;
}
return null;
}
private static byte[] convert(byte[] data, Key key, int mode) {
try {
Cipher cipher = Cipher.getInstance(RSAConstants.ALGORITHM);
cipher.init(mode, key);
byte[] newData = cipher.doFinal(data);
return newData;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
My Input is:
InterstellarGalactica
All goes smooth except for the result of Encrypted Password
Resulting below
Original: InterstellarGalactica
Encrypted: Sªë/H?ù,X?U4??A???ìñáQ
÷? *?7*??d?'å?Ñ¡w °??? Pè???«{?D÷??cB???'É »???qªîÉDë??~hb??z8?çÿ?hí?{mè?{*îèGê??WÅ{x??ï.5¼?úü;e??G?-F?shèn?FI
áh`UƒIàB!?åäô+D<&"?)?????ß!??3ä?¬???â???<?¬Ü?{ #ó12B?òt?ƒòÆr²Ä·oHQ?ë?«ú?°?î??Äy?:X^<?
&:ryb\?¼
Decrypted: InterstellarGalactica
Why do it is became a meaningless character?
Is there anything wrong with my code?
Can you explain how to do it in proper way(if there is)?
Thanks in advance.
You are using RSA in the wrong way:
In RSA you use the public key for encryption and the private key for decryption.
You however use the private key for encryption and the public key for decryption:
byte[] encrypted = RSAEncryptDecrypt.encrypt(password, privateKey);
byte[] decrypted = RSAEncryptDecrypt.decrypt(encrypted, publicKey);
Additionally please never convert a byte[] that contains binary data to String. If you want to print binary data convert it for example to a hexadecimal or base64 String instead. Or if you want to print it as a number use BigInteger.
// output Base64 encoded
System.out.println(java.util.Base64.getEncoder().encode(encrypted));
// out hexadecimal (uses Apache commons codec library
System.out.println(org.apache.commons.codec.binary.Hex.encodeHexString(encrypted));
// out hexadecimal without external library)
System.out.println(new java.math.BigInteger(1, encrypted).toString(16))
// Output as large number (useful for manual RSA calculations)
System.out.println(new java.math.BigInteger(1, encrypted));

generate public and private Keys in Hexadecimal in Java

I want to generate the public and the private key in Hexadecimal.
The current output is written in Chinese language.
I want the output of public and private key written in Hexadecimal.
// Class
public class GenerateKeys {
private final KeyPairGenerator keyGen;
private KeyPair pair;
private PrivateKey privateKey;
private PublicKey publicKey;
// Constructor
public GenerateKeys(int keylength) throws NoSuchAlgorithmException, NoSuchProviderException {
this.keyGen = KeyPairGenerator.getInstance("RSA"); // Algorithm
this.keyGen.initialize(keylength);
}
public void createKeys() {
this.pair = this.keyGen.generateKeyPair();
this.privateKey = pair.getPrivate();
this.publicKey = pair.getPublic();
}
public PrivateKey getPrivateKey() {
return this.privateKey;
}
public PublicKey getPublicKey() {
return this.publicKey;
}
public void writeToFile(String path, byte[] key) throws IOException {
File f = new File(path);
f.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(key);
fos.flush();
}
}
// Main
public static void main(String[] args)
{
GenerateKeys gk;
try {
gk = new GenerateKeys(1024);
gk.createKeys();
gk.writeToFile("MyKeys/publicKey",gk.getPublicKey().getEncoded());
gk.writeToFile("MyKeys/privateKey",gk.getPrivateKey().getEncoded());
} catch (NoSuchAlgorithmException | NoSuchProviderException | IOException e) {
System.err.println(e.getMessage());
}
}
}
It doesn't seem like you are actually creating a file written in the Chinese language. What you seem to be doing is creating what's called a "binary" file. These are files that your computer can understand, but when you open them in a text editor, you can't read them because they don't make any sense. Symbols from other languages will often appear.
Writing byte[] arrays with FileOutputStream will always make a binary file.
To create a file that's readable by humans and displays your keys in hexadecimal you can replace your writeToFile() method with this code.
public void writeToFile(String path, byte[] key) throws IOException {
File f = new File(path);
f.getParentFile().mkdirs();
StringBuilder sb = new StringBuilder();
for(byte b: key) {
sb.append(String.format("%02X", b) + " ");
}
try (FileWriter fos = new FileWriter(f)) {
fos.write(sb.toString());
fos.flush();
}
}
This should generate a text file with each key value in your byte[] array converted to hexadecimal.

got AEADBadTagException while trying to decrypt message in GCM mode

I'm writing an app which got a lot of security constraints:
It needs to store files securely encrypted, and must be able to decrypt them. Also, an Operator needs to be able to decrypt the file without the app.
To archive this, I generated a KeyPair on my PC, put the public part in my app, generate an AES SecretKey Key inside the app, encrypt and save it with my public key (for operator purposes), and put the unencrypted key in AndroidKeyStore.
To Encrypt a message, I receive the SecretKey from KeyStore, encrypt my message, get the IV I used as well as the encryptedSecretKey, and write them in a defined order to a byte array (iv->encryptedSecretKey->encryptedMessage).
To Decrypt I try the same in reverse: get the byte array, read the iv and encryptedSecretKey, and pass the rest (encryptedMessage) to my cypher to decrypt.
The problem is, that cipher.doFinal(encryptedMessage) is throwing an
javax.crypto.AEADBadTagExceptionwhich is caused by android.security.KeyStoreException: Signature/MAC verification failed.
I already checked that the encrypted message and the one I want to decrypt are exactly the same. I'm having no idea what I am doing wrong.
The class I use is the following:
package my.company.domain;
import android.content.Context;
import android.content.SharedPreferences;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoHelper {
public static final String TAG = CryptoHelper.class.getSimpleName();
private static final String KEY_ALIAS = "OI1lTI1lLI1l0";
private static final char[] KEY_PASSWORD = "Il0VELI1lO".toCharArray();
private static final String PREF_NAME = "CryptoPrefs";
private static final String KEY_ENCRYPTED_SECRET = "encryptedSecret";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final int IV_SIZE = 12;
private static final int IV_BIT_LEN = IV_SIZE * 8;
//generate 128 bit key (16), other possible values 192(24), 256(32)
private static final int AES_KEY_SIZE = 16;
private static final String AES = KeyProperties.KEY_ALGORITHM_AES;
private static final String AES_MODE = AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE;
private static final String RSA = KeyProperties.KEY_ALGORITHM_RSA;
private static final String RSA_MODE = KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + KeyProperties.ENCRYPTION_PADDING_NONE;
private static final String RSA_PROVIDER = "AndroidOpenSSL";
private final Context mContext;
private final SharedPreferences mPrefs;
private SecureRandom mSecureRandom;
private KeyStore mAndroidKeyStore;
private PublicKey mPublicKey;
private byte[] mEncryptedSecretKey;
public CryptoHelper(Context context) {
mContext = context;
mSecureRandom = new SecureRandom();
mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
try {
mAndroidKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
mAndroidKeyStore.load(null);
} catch (KeyStoreException e) {
Log.wtf(TAG, "Could not get AndroidKeyStore!", e);
} catch (Exception e) {
Log.wtf(TAG, "Could not load AndroidKeyStore!", e);
}
}
public void reset() throws KeyStoreException {
mAndroidKeyStore.deleteEntry(KEY_ALIAS);
}
public byte[] encrypt(byte[] message){
SecretKey secretKey = getSecretKey();
try {
Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
byte[] cryptedBytes = cipher.doFinal(message);
byte[] iv = cipher.getIV();
byte[] encryptedSecretKey = getEncryptedSecretKey();
ByteBuffer buffer = ByteBuffer.allocate(IV_BIT_LEN + encryptedSecretKey.length + cryptedBytes.length);
buffer
.put(iv)
.put(encryptedSecretKey)
.put(cryptedBytes);
return buffer.array();
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
return null;
}
public byte[] encrypt(String message){
return encrypt(message.getBytes(StandardCharsets.UTF_8));
}
public byte[] decrypt(byte[] bytes){
ByteBuffer buffer = ByteBuffer.wrap(bytes);
byte[] iv = new byte[IV_SIZE];
buffer.get(iv);
byte[] unused = getEncryptedSecretKey();
buffer.get(unused);
byte[] encryptedMessage = new byte[bytes.length - IV_SIZE - unused.length];
buffer.get(encryptedMessage);
try {
Cipher cipher = Cipher.getInstance(AES_MODE);
GCMParameterSpec parameterSpec = new GCMParameterSpec(IV_BIT_LEN, iv);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), parameterSpec);
byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
return decryptedMessage;
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
return null;
}
public String decryptToString(byte[] bytes){
return new String(decrypt(bytes), StandardCharsets.UTF_8);
}
public byte[] decrypt(FileInputStream fileToDecrypt){
byte[] buffer = null;
try {
buffer = new byte[fileToDecrypt.available()];
fileToDecrypt.read(buffer);
buffer = decrypt(buffer);
} catch (IOException e) {
e.printStackTrace();
}
return buffer;
}
public PublicKey getPublicKey() {
if (null == mPublicKey) {
mPublicKey = readPublicKey();
}
return mPublicKey;
}
public byte[] getEncryptedSecretKey() {
if (null == mEncryptedSecretKey){
mEncryptedSecretKey = Base64.decode(mPrefs.getString(KEY_ENCRYPTED_SECRET, null), Base64.NO_WRAP);
}
return mEncryptedSecretKey;
}
private void saveEncryptedSecretKey(byte[] encryptedSecretKey){
String base64EncryptedKey = Base64.encodeToString(encryptedSecretKey, Base64.NO_WRAP);
mPrefs.edit().putString(KEY_ENCRYPTED_SECRET, base64EncryptedKey).apply();
}
protected SecretKey getSecretKey(){
SecretKey secretKey = null;
try {
if (!mAndroidKeyStore.containsAlias(KEY_ALIAS)){
generateAndStoreSecureKey();
}
secretKey = (SecretKey) mAndroidKeyStore.getKey(KEY_ALIAS, KEY_PASSWORD);
} catch (KeyStoreException e) {
Log.wtf(TAG, "Could not check AndroidKeyStore alias!", e);
secretKey = null;
} catch (GeneralSecurityException e) {
e.printStackTrace();
secretKey = null;
}
return secretKey;
}
private void generateAndStoreSecureKey()
throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, KeyStoreException, BadPaddingException, IllegalBlockSizeException {
SecretKey secretKey = generateSecureRandomKey();
PublicKey publicKey = getPublicKey();
Cipher keyCipher = Cipher.getInstance(RSA_MODE, RSA_PROVIDER);
keyCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedSecretKeyBytes = keyCipher.doFinal(secretKey.getEncoded());
saveEncryptedSecretKey(encryptedSecretKeyBytes);
KeyProtection keyProtection = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_VERIFY)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build();
mAndroidKeyStore.setEntry(KEY_ALIAS, new KeyStore.SecretKeyEntry(secretKey), keyProtection);
}
protected PublicKey readPublicKey() {
DataInputStream dis = null;
PublicKey key = null;
try {
dis = new DataInputStream(mContext.getResources().getAssets().open("public_key.der"));
byte[] keyBytes = new byte[dis.available()];
dis.readFully(keyBytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory facotory = KeyFactory.getInstance(RSA);
key = facotory.generatePublic(spec);
} catch (Exception e) {
key = null;
} finally {
if (null != dis) {
try {
dis.close();
} catch (IOException e) {
Log.wtf(TAG, "Cannot Close Stream!", e);
}
}
}
return key;
}
#NonNull
protected SecretKey generateSecureRandomKey() {
return new SecretKeySpec(generateSecureRandomBytes(AES_KEY_SIZE), AES);
}
#NonNull
protected byte[] generateSecureRandomBytes(int byteCount) {
byte[] keyBytes = new byte[byteCount];
mSecureRandom.nextBytes(keyBytes);
return keyBytes;
}
}
And I Test it like this:
#Test
public void testCrypto() throws Exception {
CryptoHelper crypto = new CryptoHelper(InstrumentationRegistry.getTargetContext());
crypto.reset();
String verySecretOpinion = "we're all doomed";
byte[] encrypt = crypto.encrypt(verySecretOpinion);
Assert.assertNotNull("Encrypted secret is Null!", encrypt);
Assert.assertFalse("encrypted Bytes are the same as Input!", new String(encrypt, StandardCharsets.UTF_8).equals(verySecretOpinion));
String decryptedString = crypto.decryptToString(encrypt);
Assert.assertNotNull("Decrypted String must be Non-Null!", decryptedString);
Assert.assertEquals("Decrypted String doesn't equal encryption input!", verySecretOpinion, decryptedString);
}
By the way minSdkVersion is 25, so higher than Marshmallow
UPDATE:
Fixed Cipher.DECRYPT_MODE to ENCRYPT_MODE on saving the SecretKey thx to James K Polk's comment
it works If I switch from BlockMode GCM to BlockMode CBC (and changing the GCMParameterSpec to IvParamterSpec but loose the verification of the GCM Mode.
There are at least two problems with the Operator interface. First, you RSA encrypt the secret key using the wrong Cipher mode: you used DECRYPT mode when you should have used encrypt. Secondly, you are using RSA without any padding. You need to use a real padding mode, one of the OEAP padding modes is recommended.
An error in the encryption side occurs when sizing the buffer used to hold the result:
ByteBuffer buffer = ByteBuffer.allocate(IV_BIT_LEN + encryptedSecretKey.length + cryptedBytes.length);
allocates too much space. IV_BIT_LEN should probably be changed to IV_SIZE to get the correctly sized ByteBuffer.
The last mistake is the failure to account for the GCM authentication tag length when setting the GCMParameterSpec on the decrypt side. You initialized the tag length in this line
GCMParameterSpec parameterSpec = new GCMParameterSpec(IV_BIT_LEN, iv);
but that's incorrect, the tag length is unrelated to the IV. Since you did not explicitly set the GCMParameterSpec on the encrypt side you got the default tag length, which happens to be 128.
You can retrieve the tag length on the encrypt side by calling
cipher.getParameters().getParameterSpec(GCMParameterSpec.class) to get the parameter spec. From this you can retrieve both the tag length and the iv. You should probably consider the tag length, 16 bytes = 128 bits, to be a hard-coded constant and not transmit it. The receiver should similar assume the tag length is 128 bits.

Categories