mirror of https://github.com/tasks/tasks
Convert KeyStoreEncryption to Kotlin
parent
9bf5216081
commit
d1ad84f281
@ -1,130 +0,0 @@
|
|||||||
package org.tasks.security;
|
|
||||||
|
|
||||||
import static org.tasks.Strings.isNullOrEmpty;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.security.keystore.KeyGenParameterSpec;
|
|
||||||
import android.security.keystore.KeyProperties;
|
|
||||||
import android.util.Base64;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStore.Entry;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.NoSuchProviderException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.UnrecoverableEntryException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.KeyGenerator;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.spec.GCMParameterSpec;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import org.tasks.injection.ApplicationScope;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@ApplicationScope
|
|
||||||
public class KeyStoreEncryption {
|
|
||||||
|
|
||||||
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
|
|
||||||
private static final String ALIAS = "passwords";
|
|
||||||
private static final Charset ENCODING = StandardCharsets.UTF_8;
|
|
||||||
private static final int GCM_IV_LENGTH = 12;
|
|
||||||
private static final int GCM_TAG_LENGTH = 16;
|
|
||||||
|
|
||||||
private KeyStore keyStore;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public KeyStoreEncryption() {
|
|
||||||
try {
|
|
||||||
keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
|
|
||||||
keyStore.load(null);
|
|
||||||
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String encrypt(String text) {
|
|
||||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
|
||||||
new SecureRandom().nextBytes(iv);
|
|
||||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, iv);
|
|
||||||
try {
|
|
||||||
byte[] output = cipher.doFinal(text.getBytes(ENCODING));
|
|
||||||
byte[] result = new byte[iv.length + output.length];
|
|
||||||
System.arraycopy(iv, 0, result, 0, iv.length);
|
|
||||||
System.arraycopy(output, 0, result, iv.length, output.length);
|
|
||||||
return Base64.encodeToString(result, Base64.DEFAULT);
|
|
||||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String decrypt(String text) {
|
|
||||||
if (isNullOrEmpty(text)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] decoded = Base64.decode(text, Base64.DEFAULT);
|
|
||||||
byte[] iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH);
|
|
||||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, iv);
|
|
||||||
try {
|
|
||||||
byte[] decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.length - GCM_IV_LENGTH);
|
|
||||||
return new String(decrypted, ENCODING);
|
|
||||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cipher getCipher(int cipherMode, byte[] iv) {
|
|
||||||
try {
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
||||||
cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv));
|
|
||||||
return cipher;
|
|
||||||
} catch (NoSuchAlgorithmException
|
|
||||||
| NoSuchPaddingException
|
|
||||||
| InvalidAlgorithmParameterException
|
|
||||||
| InvalidKeyException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SecretKey getSecretKey() {
|
|
||||||
try {
|
|
||||||
Entry entry = keyStore.getEntry(ALIAS, null);
|
|
||||||
return entry == null ? generateNewKey() : ((KeyStore.SecretKeyEntry) entry).getSecretKey();
|
|
||||||
} catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableEntryException e) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("TrulyRandom")
|
|
||||||
private SecretKey generateNewKey() {
|
|
||||||
try {
|
|
||||||
final KeyGenerator keyGenerator =
|
|
||||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
|
|
||||||
|
|
||||||
keyGenerator.init(
|
|
||||||
new KeyGenParameterSpec.Builder(
|
|
||||||
ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
||||||
.setRandomizedEncryptionRequired(false)
|
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
||||||
.build());
|
|
||||||
return keyGenerator.generateKey();
|
|
||||||
} catch (NoSuchAlgorithmException
|
|
||||||
| InvalidAlgorithmParameterException
|
|
||||||
| NoSuchProviderException e) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,97 @@
|
|||||||
|
package org.tasks.security
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import org.tasks.Strings.isNullOrEmpty
|
||||||
|
import org.tasks.injection.ApplicationScope
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.*
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ApplicationScope
|
||||||
|
class KeyStoreEncryption @Inject constructor() {
|
||||||
|
|
||||||
|
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||||
|
|
||||||
|
fun encrypt(text: String): String? {
|
||||||
|
val iv = ByteArray(GCM_IV_LENGTH)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
val cipher = getCipher(Cipher.ENCRYPT_MODE, iv)
|
||||||
|
return try {
|
||||||
|
val output = cipher.doFinal(text.toByteArray(ENCODING))
|
||||||
|
val result = ByteArray(iv.size + output.size)
|
||||||
|
System.arraycopy(iv, 0, result, 0, iv.size)
|
||||||
|
System.arraycopy(output, 0, result, iv.size, output.size)
|
||||||
|
Base64.encodeToString(result, Base64.DEFAULT)
|
||||||
|
} catch (e: IllegalBlockSizeException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
} catch (e: BadPaddingException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(text: String?): String? {
|
||||||
|
if (isNullOrEmpty(text)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val decoded = Base64.decode(text, Base64.DEFAULT)
|
||||||
|
val iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH)
|
||||||
|
val cipher = getCipher(Cipher.DECRYPT_MODE, iv)
|
||||||
|
return try {
|
||||||
|
val decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.size - GCM_IV_LENGTH)
|
||||||
|
String(decrypted, ENCODING)
|
||||||
|
} catch (e: IllegalBlockSizeException) {
|
||||||
|
Timber.e(e)
|
||||||
|
""
|
||||||
|
} catch (e: BadPaddingException) {
|
||||||
|
Timber.e(e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCipher(cipherMode: Int, iv: ByteArray): Cipher {
|
||||||
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
cipher.init(cipherMode, secretKey, GCMParameterSpec(GCM_TAG_LENGTH * java.lang.Byte.SIZE, iv))
|
||||||
|
return cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
private val secretKey: SecretKey
|
||||||
|
get() {
|
||||||
|
val entry: KeyStore.Entry? = keyStore.getEntry(ALIAS, null)
|
||||||
|
return (entry as KeyStore.SecretKeyEntry?)?.secretKey ?: generateNewKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private fun generateNewKey(): SecretKey {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
|
||||||
|
keyGenerator.init(
|
||||||
|
KeyGenParameterSpec.Builder(
|
||||||
|
ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
.setRandomizedEncryptionRequired(false)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
.build())
|
||||||
|
return keyGenerator.generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
keyStore.load(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||||
|
private const val ALIAS = "passwords"
|
||||||
|
private val ENCODING = StandardCharsets.UTF_8
|
||||||
|
private const val GCM_IV_LENGTH = 12
|
||||||
|
private const val GCM_TAG_LENGTH = 16
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue