Safer password handling (#1146)

* no longer convert passwords to string

* also clear backing array of outputStream

* use fill and small refactor
This commit is contained in:
Shamicen 2024-04-15 01:48:32 +02:00 committed by GitHub
parent 206d824ed2
commit 596a8d002f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -16,6 +16,8 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.security.KeyStore import java.security.KeyStore
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
@ -33,28 +35,28 @@ object CbzCrypto {
const val DATABASE_NAME = "tachiyomiEncrypted.db" const val DATABASE_NAME = "tachiyomiEncrypted.db"
const val DEFAULT_COVER_NAME = "cover.jpg" const val DEFAULT_COVER_NAME = "cover.jpg"
private val securityPreferences: SecurityPreferences by injectLazy() private val securityPreferences: SecurityPreferences by injectLazy()
private val keyStore = KeyStore.getInstance(KEYSTORE).apply { private val keyStore = KeyStore.getInstance(Keystore).apply {
load(null) load(null)
} }
private val encryptionCipherCbz private val encryptionCipherCbz
get() = Cipher.getInstance(CRYPTO_SETTINGS).apply { get() = Cipher.getInstance(CryptoSettings).apply {
init( init(
Cipher.ENCRYPT_MODE, Cipher.ENCRYPT_MODE,
getKey(ALIAS_CBZ), getKey(AliasCbz),
) )
} }
private val encryptionCipherSql private val encryptionCipherSql
get() = Cipher.getInstance(CRYPTO_SETTINGS).apply { get() = Cipher.getInstance(CryptoSettings).apply {
init( init(
Cipher.ENCRYPT_MODE, Cipher.ENCRYPT_MODE,
getKey(ALIAS_SQL), getKey(AliasSql),
) )
} }
private fun getDecryptCipher(iv: ByteArray, alias: String): Cipher { private fun getDecryptCipher(iv: ByteArray, alias: String): Cipher {
return Cipher.getInstance(CRYPTO_SETTINGS).apply { return Cipher.getInstance(CryptoSettings).apply {
init( init(
Cipher.DECRYPT_MODE, Cipher.DECRYPT_MODE,
getKey(alias), getKey(alias),
@ -69,12 +71,12 @@ object CbzCrypto {
} }
private fun generateKey(alias: String): SecretKey { private fun generateKey(alias: String): SecretKey {
return KeyGenerator.getInstance(ALGORITHM).apply { return KeyGenerator.getInstance(Algorithm).apply {
init( init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setKeySize(KEY_SIZE) .setKeySize(KeySize)
.setBlockModes(BLOCK_MODE) .setBlockModes(BlockMode)
.setEncryptionPaddings(PADDING) .setEncryptionPaddings(Padding)
.setRandomizedEncryptionRequired(true) .setRandomizedEncryptionRequired(true)
.setUserAuthenticationRequired(false) .setUserAuthenticationRequired(false)
.build(), .build(),
@ -82,13 +84,13 @@ object CbzCrypto {
}.generateKey() }.generateKey()
} }
private fun encrypt(password: String, cipher: Cipher): String { private fun encrypt(password: ByteArray, cipher: Cipher): String {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
outputStream.use { output -> outputStream.use { output ->
output.write(cipher.iv) output.write(cipher.iv)
ByteArrayInputStream(password.toByteArray()).use { input -> ByteArrayInputStream(password).use { input ->
val buffer = ByteArray(BUFFER_SIZE) val buffer = ByteArray(BufferSize)
while (input.available() > BUFFER_SIZE) { while (input.available() > BufferSize) {
input.read(buffer) input.read(buffer)
output.write(cipher.update(buffer)) output.write(cipher.update(buffer))
} }
@ -98,51 +100,66 @@ object CbzCrypto {
return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT) return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT)
} }
private fun decrypt(encryptedPassword: String, alias: String): String { private fun decrypt(encryptedPassword: String, alias: String): ByteArray {
val inputStream = Base64.decode(encryptedPassword, Base64.DEFAULT).inputStream() val inputStream = Base64.decode(encryptedPassword, Base64.DEFAULT).inputStream()
return inputStream.use { input -> return inputStream.use { input ->
val iv = ByteArray(IV_SIZE) val iv = ByteArray(IvSize)
input.read(iv) input.read(iv)
val cipher = getDecryptCipher(iv, alias) val cipher = getDecryptCipher(iv, alias)
ByteArrayOutputStream().use { output -> ByteArrayOutputStreamPassword().use { output ->
val buffer = ByteArray(BUFFER_SIZE) val buffer = ByteArray(BufferSize)
while (inputStream.available() > BUFFER_SIZE) { while (inputStream.available() > BufferSize) {
inputStream.read(buffer) inputStream.read(buffer)
output.write(cipher.update(buffer)) output.write(cipher.update(buffer))
} }
output.write(cipher.doFinal(inputStream.readBytes())) output.write(cipher.doFinal(inputStream.readBytes()))
output.toString() output.toByteArray().also {
output.clear()
}
} }
} }
} }
fun deleteKeyCbz() { fun deleteKeyCbz() {
keyStore.deleteEntry(ALIAS_CBZ) keyStore.deleteEntry(AliasCbz)
generateKey(ALIAS_CBZ) generateKey(AliasCbz)
} }
fun encryptCbz(password: String): String { fun encryptCbz(password: String): String {
return encrypt(password, encryptionCipherCbz) return encrypt(password.toByteArray(), encryptionCipherCbz)
} }
fun getDecryptedPasswordCbz(): CharArray { fun getDecryptedPasswordCbz(): CharArray {
val encryptedPassword = securityPreferences.cbzPassword().get() val encryptedPassword = securityPreferences.cbzPassword().get()
if (encryptedPassword.isBlank()) error("This archive is encrypted please set a password") if (encryptedPassword.isBlank()) error("This archive is encrypted please set a password")
return decrypt(encryptedPassword, ALIAS_CBZ).toCharArray() val cbzBytes = decrypt(encryptedPassword, AliasCbz)
return Charsets.UTF_8.decode(ByteBuffer.wrap(cbzBytes)).array()
.also {
cbzBytes.fill('#'.code.toByte())
}
} }
private fun generateAndEncryptSqlPw() { private fun generateAndEncryptSqlPw() {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val password = (1..SQL_PASSWORD_LENGTH).map { val passwordArray = CharArray(SqlPasswordLength)
charPool[SecureRandom().nextInt(charPool.size)] for (i in 0..<SqlPasswordLength) {
}.joinToString("", transform = { it.toString() }) passwordArray[i] = charPool[SecureRandom().nextInt(charPool.size)]
securityPreferences.sqlPassword().set(encrypt(password, encryptionCipherSql)) }
val passwordBuffer = Charsets.UTF_8.encode(CharBuffer.wrap(passwordArray))
val passwordBytes = ByteArray(passwordBuffer.limit())
passwordBuffer.get(passwordBytes)
securityPreferences.sqlPassword().set(encrypt(passwordBytes, encryptionCipherSql))
.also {
passwordArray.fill('#')
passwordBuffer.array().fill('#'.code.toByte())
passwordBytes.fill('#'.code.toByte())
}
} }
fun getDecryptedPasswordSql(): ByteArray { fun getDecryptedPasswordSql(): ByteArray {
if (securityPreferences.sqlPassword().get().isBlank()) generateAndEncryptSqlPw() if (securityPreferences.sqlPassword().get().isBlank()) generateAndEncryptSqlPw()
return decrypt(securityPreferences.sqlPassword().get(), ALIAS_SQL).toByteArray() return decrypt(securityPreferences.sqlPassword().get(), AliasSql)
} }
fun isPasswordSet(): Boolean { fun isPasswordSet(): Boolean {
@ -202,18 +219,24 @@ object CbzCrypto {
} }
} }
private const val BUFFER_SIZE = 2048 private const val BufferSize = 2048
private const val KEY_SIZE = 256 private const val KeySize = 256
private const val IV_SIZE = 16 private const val IvSize = 16
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val Algorithm = KeyProperties.KEY_ALGORITHM_AES
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC private const val BlockMode = KeyProperties.BLOCK_MODE_CBC
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val Padding = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING" private const val CryptoSettings = "$Algorithm/$BlockMode/$Padding"
private const val KEYSTORE = "AndroidKeyStore" private const val Keystore = "AndroidKeyStore"
private const val ALIAS_CBZ = "cbzPw" private const val AliasCbz = "cbzPw"
private const val ALIAS_SQL = "sqlPw" private const val AliasSql = "sqlPw"
private const val SQL_PASSWORD_LENGTH = 32 private const val SqlPasswordLength = 32
private class ByteArrayOutputStreamPassword : ByteArrayOutputStream() {
fun clear() {
this.buf.fill('#'.code.toByte())
}
}
// SY <-- // SY <--