OXYGENHUB
# c53e328b2db4 oxygenhub-android / app / src / main / java / io / oxygenhub / android / alerts / AlertKey.kt
3.7 KiB · last changed Alerts from the hub: the phone registers its Cloud Messaging token and an X25519 key kept by the Keystore, and opens and shows what the hub seals to it with the Rust core; Firebase from google-services.json (the release job's firebase-config secret); pins OxygenFlow main (refs #16) 8 days ago source blame history raw
1
package io.oxygenhub.android.alerts
2
3
import android.content.Context
4
import android.security.keystore.KeyGenParameterSpec
5
import android.security.keystore.KeyProperties
6
import android.security.keystore.StrongBoxUnavailableException
7
import android.util.Base64
8
import androidx.core.content.edit
9
import java.security.GeneralSecurityException
10
import java.security.KeyStore
11
import javax.crypto.Cipher
12
import javax.crypto.KeyGenerator
13
import javax.crypto.SecretKey
14
import javax.crypto.spec.GCMParameterSpec
15
import uniffi.rust_core.alertNewSecret
16
import uniffi.rust_core.alertPublicKey
17
18
/**
19
 * The phone's alert key (#16): an X25519 secret, made by the Rust core,
20
 * that the hub seals each alert to. Kept in app-private preferences only as
21
 * ciphertext under an AES-GCM key in the Android Keystore (StrongBox where
22
 * the phone has one), as the session's token is; out of backups, since a
23
 * restored copy could not be opened anyway. A lost key just means a new one
24
 * and registering again.
25
 */
26
object AlertKey {
27
    private const val PREFS = "alerts"
28
    private const val SEALED = "secret.sealed"
29
    private const val KEYSTORE = "AndroidKeyStore"
30
    private const val ALIAS = "io.oxygenhub.android.alerts"
31
    private const val TRANSFORMATION = "AES/GCM/NoPadding"
32
33
    /** The secret, made and kept the first time it is asked for. */
34
    @Synchronized
35
    fun secret(context: Context): ByteArray {
36
        val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
37
        prefs.getString(SEALED, null)?.let { sealed ->
38
            try {
39
                return open(sealed)
40
            } catch (_: GeneralSecurityException) {
41
                // The Keystore key is gone: a new secret, registered again.
42
            } catch (_: IllegalArgumentException) {
43
            }
44
        }
45
        val fresh = alertNewSecret()
46
        prefs.edit { putString(SEALED, seal(fresh)) }
47
        return fresh
48
    }
49
50
    /** The public key, hex, for the hub. */
51
    fun publicKeyHex(context: Context): String = alertPublicKey(secret(context)).joinToString("") { "%02x".format(it) }
52
53
    private fun seal(plain: ByteArray): String {
54
        val cipher = Cipher.getInstance(TRANSFORMATION)
55
        cipher.init(Cipher.ENCRYPT_MODE, key())
56
        return b64(cipher.iv) + ":" + b64(cipher.doFinal(plain))
57
    }
58
59
    private fun open(sealed: String): ByteArray {
60
        val parts = sealed.split(':', limit = 2)
61
        require(parts.size == 2) { "not a sealed secret" }
62
        val key = existingKey() ?: throw GeneralSecurityException("no key")
63
        val cipher = Cipher.getInstance(TRANSFORMATION)
64
        cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, Base64.decode(parts[0], Base64.NO_WRAP)))
65
        return cipher.doFinal(Base64.decode(parts[1], Base64.NO_WRAP))
66
    }
67
68
    private fun b64(bytes: ByteArray): String = Base64.encodeToString(bytes, Base64.NO_WRAP)
69
70
    private fun existingKey(): SecretKey? {
71
        val ks = KeyStore.getInstance(KEYSTORE).apply { load(null) }
72
        return (ks.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey
73
    }
74
75
    private fun key(): SecretKey = existingKey() ?: try {
76
        generate(strongBox = true)
77
    } catch (_: StrongBoxUnavailableException) {
78
        generate(strongBox = false)
79
    }
80
81
    private fun generate(strongBox: Boolean): SecretKey {
82
        val spec = KeyGenParameterSpec.Builder(ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
83
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
84
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
85
            .setKeySize(256)
86
            .setIsStrongBoxBacked(strongBox)
87
            .build()
88
        return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE).apply { init(spec) }.generateKey()
89
    }
90
}