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