I'm developing an offline auth process for users to access their accounts. My idea was to store their hashed & encrypted email and passwords and let them introduce their email and password to auth , i'm using a Room database to store their credentials . The problem is when i try to decrypt their passwords and check if those match the decryption function throws an exception (-1) ,(I'm using the Keystore Sistem , Cipher , tha salt changes constantly with AES-256 algorithm). Any other suggestion to make an offline auth?
fun encryptText(bytes:ByteArray, outputStream: OutputStream ):ByteArray{
val encryptedBytes = encryptCipher.doFinal(bytes)
outputStream.use {
it.write(encryptCipher.iv.size)
it.write(encryptCipher.iv)
it.write(encryptedBytes.size)
it.write(encryptedBytes)
}
return encryptedBytes
}
fun decryptText(inputStream:InputStream):ByteArray{
Log.d("Test" ,"input decryptor = " +inputStream.readBytes().decodeToString())
return inputStream.use {
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
it.read(iv)
val encryptedBytesSize = inputStream.read()
val encryptedBytes = ByteArray(encryptedBytesSize)
it.read(encryptedBytes)
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
}
}
#Entity(tableName = "account_credentials")
data class AccountCredentials(#PrimaryKey
val uuid:String,
val firebaseId:String?,
#ColumnInfo(name = "password_bytes",typeAffinity = ColumnInfo.BLOB) val hashedPassword:ByteArray,
#ColumnInfo(name = "email_bytes",typeAffinity = ColumnInfo.BLOB) val email:ByteArray
)
private fun encryptCredentials(credentials: AccountCredentials):AccountCredentials{
val passwordStream = ByteArrayOutputStream()
val emailStream = ByteArrayOutputStream()
CryptographicManager().encryptText(credentials.email , emailStream)
CryptographicManager().encryptText(credentials.hashedPassword , passwordStream)
return credentials.copy(hashedPassword = passwordStream.toByteArray() , email =emailStream.toByteArray() )
}
I have tried to change the data class field to string instead of ByteArray but the same result .
Related
Description
I'm attempting to encrypt a token along with its IV to a pair of ByteArrays, serialize it, then write it to a Room database. The steps are obviously reversed when attempting to decrypt and read it.
When repeating the encryption/serialization/deserialization/decryption steps, but without writing it to a database, the given ByteArray decrypts just fine. Writing it gives me the following error on decryption:
java.io.StreamCorruptedException: invalid stream header
I'm struggling to understand why this happens, and I'd appreciate the help.
Code
ByteArray Functions
#Suppress("UNCHECKED_CAST")
fun <T : Serializable> fromByteArray(byteArray: ByteArray): T {
val inputStream = ByteArrayInputStream(byteArray)
val objectInput = ObjectInputStream(inputStream)
val result = objectInput.readObject() as T
objectInput.close()
inputStream.close()
return result
}
fun Serializable.toByteArray(): ByteArray {
val outputStream = ByteArrayOutputStream()
val objectOutput = ObjectOutputStream(outputStream)
objectOutput.writeObject(this)
objectOutput.flush()
val result = outputStream.toByteArray()
outputStream.close()
objectOutput.close()
return result
}
Encryption Functions
override fun <T : Serializable> encryptData(data: T): Pair<ByteArray, ByteArray> {
var temp = data.toByteArray()
if (temp.size % 16 != 0) {
temp = temp.copyOf(
(temp.size + 16) - (temp.size % 16)
)
}
cipher.init(Cipher.ENCRYPT_MODE, getKey())
val ivBytes = cipher.iv
val encryptedArray = cipher.doFinal(temp)
return Pair(ivBytes, encryptedArray)
}
#Suppress("UNCHECKED_CAST")
override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
val ivSpec = IvParameterSpec(ivBytes)
cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
val tempArray: ByteArray = cipher.doFinal(data)
return fromByteArray(tempArray) as T
}
Room Data Class
data class UserData(
val profilePictureId: Long?,
val savedTimestamp: Long = System.currentTimeMillis(),
#PrimaryKey
val username: String = "",
val userToken: Pair<ByteArray, ByteArray>?
)
Database Class
#Database(entities = [UserData::class], version = 1)
#TypeConverters(UserDataConverters::class)
abstract class UserDataDatabase : RoomDatabase() {
abstract val userDataDao: UserDataDao
companion object {
const val DB_NAME = "user_data_db"
}
}
Database DAO
#Dao
interface UserDataDao {
#Query("SELECT * FROM UserData")
fun loadUserData(): Flow<UserData>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateUserData(userData: UserData)
}
Database Type Converters
class UserDataConverters {
#TypeConverter
fun fromTokenPair(pair: Pair<ByteArray, ByteArray>): String {
return Json.encodeToString(pair)
}
#TypeConverter
fun toTokenPair(serializedPair: String): Pair<ByteArray, ByteArray> {
return Json.decodeFromString(serializedPair)
}
}
So this isn't actually related to Room. My mistake.
I didn't realise that serializing objects to ByteArrays with ObjectInputStream also writes a header for later serialization with ObjectOutputStream.
When encrypting the serialized data, I was using CDC block mode, which requires padding to a block size divisble by 16. That extra padding caused the aforementioned header to become invalid for accompanying data.
Removing the padding raises issues with detecting when padding stops and content starts (copyOf adds zeroes). With that in mind, and after later finding out that CBC is less secure than GCM (which requires no padding), I changed the block mode to GCM.
See below for resultant code (irrelevent blocks removed):
private val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"TroupetentKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
Also, when decrypting, the previous spec ivParameterSpec() couldn't be used, as GCM requires a tag length. That was also changed:
#Suppress("UNCHECKED_CAST")
override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
val ivSpec = GCMParameterSpec(128, ivBytes)
cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
val decryptedArray: ByteArray = cipher.doFinal(data)
return fromByteArray(decryptedArray) as T
}
I am trying to implement encryption in amazon with custom keys, I am providing these 3 values in the header as per the documentation mention
objectMetadata.setHeader("x-amz-server-side-encryption-customer-algorithm", ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION)
objectMetadata.setHeader("x-amz-server-side-encryption-customer-key", key2)
objectMetadata.setHeader("x-amz-server-side-encryption-customer-key-MD5", md5)
These 3 values are required in the header but it's not encrypting files at amazon server, I am generating the customer-key and md5 key through this code
#Throws(Exception::class)
fun encrypt(
plaintext: ByteArray?,
password: CharArray,
key: SecretKey,
IV: ByteArray?,
salt: ByteArray
): ByteArray? {
val cipher = Cipher.getInstance("AES")
val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
val keySpec = SecretKeySpec(keyBytes, "AES")
val ivSpec = IvParameterSpec(IV)
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
return cipher.doFinal(plaintext)
}
val key2 = Base64.getEncoder().encodeToString(encrypt(utf8, chr, key, iv, salt)) // This is how am calling the function and making base64 customer key
Then for md5 am using this code to create md5 key
val md = MessageDigest.getInstance("MD5")
Files.newInputStream(Paths.get(files[j].path)).use { `is` ->
DigestInputStream(`is`, md).use { }
}
val digest: ByteArray = md.digest(files[j].path.encodeToByteArray())
val md5 = Base64.getEncoder().encodeToString(digest)
The file is successfully uploading to the aws server but the file is not encrypted, I can't seem to figure out what's the issue
Are you adding these 3 keys in your header correctly?
objectMetadata.setHeader("x-amz-server-side-encryption-customer-algorithm", ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION)
objectMetadata.setHeader("x-amz-server-side-encryption-customer-key", key2)
objectMetadata.setHeader("x-amz-server-side-encryption-customer-key-MD5", md5)
The Android docs give the following snippet for how to encrypt a message in AES:
val plaintext: ByteArray = ...
val keygen = KeyGenerator.getInstance("AES")
keygen.init(256)
val key: SecretKey = keygen.generateKey()
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
cipher.init(Cipher.ENCRYPT_MODE, key)
val ciphertext: ByteArray = cipher.doFinal(plaintext)
val iv: ByteArray = cipher.iv
I get this error when implementing this method:
Unresolved reference: Cipher
So it appears the 'Cipher' object isn't native, however I have no way of knowing how to import it by following the Android docs. How do I set up my project to be able to use 'Cipher'?
javax.crypto.Cipher is part of the JCE and should be available. Does an import javax.crypto.Cipher not work? Then maybe something is wrong with your environment.
I'm not sure if using that Cipher is necessary, and if the solution I'm providing is the best approach, but I was able to use AES for encryption and decryption using the following code for a text input, means a String:
ENCRYPTION
// text
val aesEncrypt: AESEncrypt = AESEncrypt()
val encryptedByteArray = aesEncrypt.encrypt(text)
val baos_text = ByteArrayOutputStream()
val oosText = ObjectOutputStream(baos_text)
oosText.writeObject(encryptedByteArray)
val encryptedText = String(android.util.Base64.encode(baos_text.toByteArray(), android.util.Base64.DEFAULT))
// key
val key = aesEncrypt.mySecretKey
val baos = ByteArrayOutputStream()
val oos = ObjectOutputStream(baos)
oos.writeObject(key)
val keyAsString = String(android.util.Base64.encode(baos.toByteArray(), android.util.Base64.DEFAULT))
// initialisation vector
val iv = aesEncrypt.myInitializationVector
val baosIV = ByteArrayOutputStream()
val oosIV = ObjectOutputStream(baosIV)
oosIV.writeObject(iv)
val initialisationVector = String(android.util.Base64.encode(baosIV.toByteArray(), android.util.Base64.DEFAULT))
DECRYPTION
You must save the key, initialisation vector, and the encrypted text in order to decrypt it back.
val initialisationVector = ... // get from wherever you saved it, local db, firebase...
val bytesIV = android.util.Base64.decode(iv, android.util.Base64.DEFAULT)
val oisIV = ObjectInputStream(ByteArrayInputStream(bytesIV))
val initializationVectorIV = oisIV.readObject() as ByteArray
val encryptedText = ... // get
val bytesText = android.util.Base64.decode(encryptedText, android.util.Base64.DEFAULT)
val oisText = ObjectInputStream(ByteArrayInputStream(bytesText))
val textByteArray = oisText.readObject() as ByteArray
val key = ... // get your key
val bytesKey = android.util.Base64.decode(key, android.util.Base64.DEFAULT)
val oisKey = ObjectInputStream(ByteArrayInputStream(bytesKey))
val secretKeyObj = oisKey.readObject() as SecretKey
val aesDecrypt = AESDecrypt(secretKeyObj,initializationVectorIV)
val decryptedByteArray = aesDecrypt.decrypt(textByteArray)
val textAfterDecryption = decryptedByteArray.toString(charset("UTF-8"))
EDIT
AES helper class:
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
class AESEncrypt {
var mySecretKey: SecretKey? = null
var myInitializationVector: ByteArray? = null
fun encrypt(strToEncrypt: String): ByteArray {
val plainText = strToEncrypt.toByteArray(Charsets.UTF_8)
val keygen = KeyGenerator.getInstance("AES")
keygen.init(256)
val key = keygen.generateKey()
mySecretKey = key
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
cipher.init(Cipher.ENCRYPT_MODE, key)
val cipherText = cipher.doFinal(plainText)
myInitializationVector = cipher.iv
return cipherText
}
}
AES decrypt helper
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
class AESDecrypt(private val mySecretKey: SecretKey?, private val initializationVector: ByteArray?) {
fun decrypt(dataToDecrypt: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
val ivSpec = IvParameterSpec(initializationVector)
cipher.init(Cipher.DECRYPT_MODE, mySecretKey, ivSpec)
val cipherText = cipher.doFinal(dataToDecrypt)
return cipherText
}
}
Do tell if you still need any help :)
I am trying to create a simple Kotlin object that wraps access to the app's shared preferences by encrypting content before saving it.
Encrypting seems to work OK but when I try to decrypt, I get an javax.crypto.AEADBadTagException which stems from an android.security.KeyStoreException with a message of "Signature/MAC verification failed".
I have tried debugging to see what's the underlying issue but I can't find anything. No search has given me any clue. I seem to follow a few guides to the letter without success.
private val context: Context?
get() = this.application?.applicationContext
private var application: Application? = null
private val transformation = "AES/GCM/NoPadding"
private val androidKeyStore = "AndroidKeyStore"
private val ivPrefix = "_iv"
private val keyStore by lazy { this.createKeyStore() }
private fun createKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance(this.androidKeyStore)
keyStore.load(null)
return keyStore
}
private fun createSecretKey(alias: String): SecretKey {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, this.androidKeyStore)
keyGenerator.init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
return keyGenerator.generateKey()
}
private fun getSecretKey(alias: String): SecretKey {
return if (this.keyStore.containsAlias(alias)) {
(this.keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
} else {
this.createSecretKey(alias)
}
}
private fun removeSecretKey(alias: String) {
this.keyStore.deleteEntry(alias)
}
private fun encryptText(alias: String, textToEncrypt: String): String {
val cipher = Cipher.getInstance(this.transformation)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(alias))
val ivString = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
this.storeInSharedPrefs(alias + this.ivPrefix, ivString)
val byteArray = cipher.doFinal(textToEncrypt.toByteArray(charset("UTF-8")))
return String(byteArray)
}
private fun decryptText(alias: String, textToDecrypt: String): String? {
val ivString = this.retrieveFromSharedPrefs(alias + this.ivPrefix) ?: return null
val iv = Base64.decode(ivString, Base64.DEFAULT)
val spec = GCMParameterSpec(iv.count() * 8, iv)
val cipher = Cipher.getInstance(this.transformation)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec)
try {
val byteArray = cipher.doFinal(textToDecrypt.toByteArray(charset("UTF-8")))
return String(byteArray)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun storeInSharedPrefs(key: String, value: String) {
this.context?.let {
PreferenceManager.getDefaultSharedPreferences(it).edit()?.putString(key, value)?.apply()
}
}
private fun retrieveFromSharedPrefs(key: String): String? {
val validContext = this.context ?: return null
return PreferenceManager.getDefaultSharedPreferences(validContext).getString(key, null)
}
Can anyone point me in the right direction ?
I had similar issue. It was all about android:allowBackup="true".
Issue
This issue will occur while uninstalling the app and then re-installing it again. KeyStore will get cleared on uninstall but the preferences not getting removed, so will end up trying to decrypt with a new key thus exception thrown.
Solution
Try disabling android:allowBackup as follows:
<application android:allowBackup="false" ... >
I encountered the same exception/issue 'android.security.KeyStoreException: Signature/MAC verification failed' on Cipher encryption 'AES/GCM/NoPadding'.
On my end, what helped to resolve this issue is to create a byte array holder first, with size that is obtained by calling Cipher.getOutputSize(int inputLen), then calling the doFinal overload Cipher.doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) to set the ciphertext in your byte array holder.
private var iv: ByteArray? = null
fun doEncryptionOperation() {
val keyStore = KeyStore.getInstance(PROVIDER_ANDROID_KEYSTORE).apply {
load(null)
}
// Assumption: key with alias 'secret_key' has already been stored
val entry = keyStore.getEntry("secret_key", null)
val secretKeyEntry = entry as KeyStore.SecretKeyEntry
val key secretKeyEntry.secretKey
val plainText = "Sample plain text"
val cipherText = encryptSymmetric(key, plainText.toByteArray())
val decrypted = decryptSymmetric(key, cipherText)
val decryptedStr = String(decrypted)
val same = decryptedStr == plainText
Log.d("SampleTag", "Is plaintext same from decrypted text? $same")
}
fun encryptSymmetric(key: SecretKey, plainText: ByteArray): ByteArray? {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
iv = cipher.iv
val len = plainText.size
val outLen = cipher.getOutputSize(len) // get expected cipher output size
val result = ByteArray(outLen) // create byte array with outLen
cipher.doFinal(plainText, 0, len, result,0) // doFinal passing plaintext data and result array
return result
}
fun decryptSymmetric(key: SecretKey, cipherText: ByteArray): ByteArray? {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val tagLen = 128 // default GCM tag length
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(tagLen,iv))
cipher.update(input.data)
val result = cipher.doFinal()
return result
}
Additionally, using AEAD, don't forget to call Cipher.updateAAD() in ENCRYPT_MODE, and set the same AEAD tag in the DECRYPT_MODE. Otherwise, you will encounter the same javax.crypto.AEADBadTagException.
When you change your authentiation tag length from iv.count() to 128 it will work.
I had a similar problem. I had an application where the admin and an ordinary user could log in and both of them had a remember me option. So, when the user previously pressed the remember me option, the program needs to fetch the encrypted password, decrypt it, and put it in the input field.
I was storing both encrypted passwords with their initialization vectors in the SharedPreferences file but when I was trying to decrypt them via Cipher (The secret key was stored in the AndroidKeyStore with the same alias for the secret key) it was decrypting one password but was giving me the same error as yours when I was decrypting another password.
Then, I used 2 different aliases for these 2 passwords when I was encrypting and decrypting them and the error is gone.
Github gist: Code example
Having generated the private key like this:
fun getKeyPair(): Pair<ByteArray, ByteArray> {
Security.addProvider(provider)
val generator = KeyPairGenerator.getInstance("ECDSA")
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1")
generator.initialize(ecSpec)
val keyPair = generator.generateKeyPair()
val publicKey = keyPair.public as ECPublicKey
val privateKey = keyPair.private
return Pair(publicKey.q.getEncoded(true), privateKey.getEncoded())
}
The public key can be reconstructed again like this:
Security.addProvider(...spongy castle provider)
val ecSpecs = ECNamedCurveTable.getParameterSpec("secp256r1")
val q = ecSpecs.curve.decodePoint(publicKeyEncoded)
val pubSpec = ECPublicKeySpec(q, ecSpecs)
val keyFactory = KeyFactory.getInstance("ECDSA")
val generatedPublic = keyFactory.generatePublic(pubSpec)
How it is possible to reconstruct private key from bytes also along with this?
UPDATE:
This code works well in actual app but it doesnt in JUnit testing:
val keyFactory = KeyFactory.getInstance("ECDSA")
val privSpec = PKCS8EncodedKeySpec(privateEncoded)
val generatedPrivate = keyFactory.generatePrivate(privSpec)
In JUnit test I am getting this error:
java.security.spec.InvalidKeySpecException: encoded key spec not recognised
My private key as encoded bytes has 150 bytes size.
Since the key is encoded using the standard Key.getEncoded(), the following standard solution should work:
val keyFactory = KeyFactory.getInstance("EC")
val privSpec = PKCS8EncodedKeySpec(privateEncoded)
val generatedPrivate = keyFactory.generatePrivate(privSpec)
The encoded key should contain all the required information to rebuild the private key without specifying additional parameters like you need to do for the reduced public key.