Skip to content

Commit

Permalink
Switch to Android encryption library for API keys storage encryption.
Browse files Browse the repository at this point in the history
  • Loading branch information
kukabi committed Jan 22, 2024
1 parent fb045c8 commit 47297a0
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 117 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ information.
```gradle
dependencies {
// ...
implementation 'com.github.helikon-labs:subvt-data-android:0.22.2'
implementation 'com.github.helikon-labs:subvt-data-android:0.22.3'
// ...
}
```
Expand Down
7 changes: 5 additions & 2 deletions subvt-data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
defaultConfig {
minSdk 24
targetSdk 34
versionCode 222
versionName "0.22.2"
versionCode 223
versionName "0.22.3"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -82,6 +82,9 @@ dependencies {
// log
implementation "com.orhanobut:logger:2.2.0"

// keystore
implementation "androidx.security:security-crypto:1.0.0"

// spongycastle
implementation "com.madgag.spongycastle:bcpkix-jdk15on:$spongyCastleVersion"
implementation "com.madgag.spongycastle:bcpg-jdk15on:$spongyCastleVersion"
Expand Down
164 changes: 50 additions & 114 deletions subvt-data/src/main/java/io/helikon/subvt/data/service/auth/AuthUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,13 @@
package io.helikon.subvt.data.service.auth

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKeys
import okhttp3.internal.toHexString
import org.web3j.crypto.ECKeyPair
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.util.Calendar
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.security.auth.x500.X500Principal

const val COMMS_KEY_ALIAS = "SubVTCommsKey"

internal fun Int.toPaddedHexString(): String {
return toHexString().run {
Expand All @@ -47,29 +37,8 @@ internal fun clearKeys(context: Context) {
File(getPublicKeyEncryptedFilePath(context)).delete()
}

internal fun generateEncryptionKeyPair() {
val notBefore = Calendar.getInstance()
val notAfter = Calendar.getInstance()
notAfter.add(Calendar.YEAR, 1)
val spec =
KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_RSA,
"AndroidKeyStore",
)
spec.initialize(
KeyGenParameterSpec.Builder(
COMMS_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setKeySize(2048)
.setKeyValidityStart(notBefore.time)
.setKeyValidityEnd(notAfter.time)
.setCertificateSubject(X500Principal("CN=subvt"))
.setCertificateSerialNumber(BigInteger(System.currentTimeMillis().toString()))
.build(),
)
spec.generateKeyPair()
private fun getMasterKeyAlias(): String {
return MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
}

private fun getPrivateKeyEncryptedFilePath(context: Context): String {
Expand All @@ -82,102 +51,69 @@ private fun getPublicKeyEncryptedFilePath(context: Context): String {
return filesDir + File.separator + "subvt_comms_pub"
}

private fun getCipherInstance() =
Cipher.getInstance(
// "RSA/ECB/OAEPWithSHA-256AndMGF1Padding",
"AES/CBC/PKCS5Padding",
)

internal fun storeKeyPair(
context: Context,
keyPair: ECKeyPair,
) {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
if (!keyStore.containsAlias(COMMS_KEY_ALIAS)) {
generateEncryptionKeyPair()
}
val entry = keyStore.getEntry(COMMS_KEY_ALIAS, null) as KeyStore.PrivateKeyEntry
val inCipher = getCipherInstance()
inCipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
var cipherOutputStream =
CipherOutputStream(
FileOutputStream(getPrivateKeyEncryptedFilePath(context)),
inCipher,
)
cipherOutputStream.write(keyPair.privateKey.toByteArray())
cipherOutputStream.close()
cipherOutputStream =
CipherOutputStream(
FileOutputStream(getPublicKeyEncryptedFilePath(context)),
inCipher,
)
cipherOutputStream.write(keyPair.publicKey.toByteArray())
cipherOutputStream.close()

/*
val privateKeyStream = FileOutputStream(getPrivateKeyEncryptedFilePath(context))
privateKeyStream.write(keyPair.privateKey.toByteArray())
privateKeyStream.flush()
privateKeyStream.close()
val publicKeyStream = FileOutputStream(getPublicKeyEncryptedFilePath(context))
publicKeyStream.write(keyPair.publicKey.toByteArray())
publicKeyStream.flush()
publicKeyStream.close()
*/
}
val privateKeyFile =
EncryptedFile.Builder(
File(getPrivateKeyEncryptedFilePath(context)),
context,
getMasterKeyAlias(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
).build()
privateKeyFile
.openFileOutput()
.run {
write(keyPair.privateKey.toByteArray())
flush()
close()
}

private fun getFileBytes(path: String): ByteArray {
val inputStream = FileInputStream(path)
val byteList = mutableListOf<Byte>()
while (true) {
val nextByte = inputStream.read().toByte()
if (nextByte == (-1).toByte()) {
break
val publicKeyFile =
EncryptedFile.Builder(
File(getPublicKeyEncryptedFilePath(context)),
context,
getMasterKeyAlias(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
).build()
publicKeyFile
.openFileOutput()
.run {
write(keyPair.publicKey.toByteArray())
flush()
close()
}
byteList.add(nextByte)
}
return byteList.toByteArray()
}

private fun getEncryptedFileBytes(
context: Context,
path: String,
outCipher: Cipher,
): ByteArray {
val cipherInputStream =
CipherInputStream(
FileInputStream(path),
outCipher,
)
val byteList = mutableListOf<Byte>()
while (true) {
val nextByte = cipherInputStream.read().toByte()
if (nextByte == (-1).toByte()) {
break
val encryptedFile =
EncryptedFile.Builder(
File(path),
context,
getMasterKeyAlias(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
).build()
val byteArrayOutputStream = ByteArrayOutputStream()
encryptedFile.openFileInput().run {
var nextByte: Int = read()
while (nextByte != -1) {
byteArrayOutputStream.write(nextByte)
nextByte = read()
}
byteList.add(nextByte)
close()
}
return byteList.toByteArray()
return byteArrayOutputStream.toByteArray()
}

internal fun getKeyPair(context: Context): ECKeyPair? {
if (!File(getPrivateKeyEncryptedFilePath(context)).exists()) {
return null
}
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val entry = keyStore.getEntry(COMMS_KEY_ALIAS, null) as KeyStore.PrivateKeyEntry
val outCipher1 = getCipherInstance()
outCipher1.init(Cipher.DECRYPT_MODE, entry.privateKey)
val privateKeyBytes = getEncryptedFileBytes(getPrivateKeyEncryptedFilePath(context), outCipher1)
val outCipher2 = getCipherInstance()
outCipher2.init(Cipher.DECRYPT_MODE, entry.privateKey)
val publicKeyBytes = getEncryptedFileBytes(getPublicKeyEncryptedFilePath(context), outCipher2)
return ECKeyPair(BigInteger(privateKeyBytes), BigInteger(publicKeyBytes))

/*
val privateKeyBytes = getFileBytes(getPrivateKeyEncryptedFilePath(context))
val publicKeyBytes = getFileBytes(getPublicKeyEncryptedFilePath(context))
val privateKeyBytes = getEncryptedFileBytes(context, getPrivateKeyEncryptedFilePath(context))
val publicKeyBytes = getEncryptedFileBytes(context, getPublicKeyEncryptedFilePath(context))
return ECKeyPair(BigInteger(privateKeyBytes), BigInteger(publicKeyBytes))
*/
}

0 comments on commit 47297a0

Please sign in to comment.