1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package social.grain.support
import android.security.keystore.KeyGenParameterSpec
import java.io.InputStream
import java.io.OutputStream
import java.security.Key
import java.security.KeyStore
import java.security.KeyStoreSpi
import java.security.Provider
import java.security.SecureRandom
import java.security.Security
import java.security.cert.Certificate
import java.security.spec.AlgorithmParameterSpec
import java.util.Collections
import java.util.Date
import java.util.Enumeration
import javax.crypto.KeyGenerator
import javax.crypto.KeyGeneratorSpi
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
/**
* A stand-in for `AndroidKeyStore`, which no JVM sandbox provides.
*
* [social.grain.data.auth.SecureStore] seals every credential under a
* non-exportable Keystore key, and that provider simply doesn't exist off a
* device — so without this, anything that touches a session can only be tested
* on an emulator. The keys here are ordinary in-memory AES keys: the point is
* to exercise the encrypt/decrypt/rotate paths that sit *above* the Keystore,
* not to reproduce hardware backing, which a test could not verify anyway.
*
* Held for the life of the JVM, matching the device: a second `SecureStore`
* built in the same test finds the master key the first one created, and a
* value written by one is readable by the other.
*/
object FakeAndroidKeyStore {
internal val keys = Collections.synchronizedMap(mutableMapOf<String, SecretKey>())
fun install() {
if (Security.getProvider(NAME) == null) Security.addProvider(FakeKeyStoreProvider())
}
/** Forget every key, as clearing the app's data would. */
fun reset() = keys.clear()
internal const val NAME = "AndroidKeyStore"
}
/** The JCA provider entry points `SecureStore` asks for by name. */
class FakeKeyStoreProvider : Provider(FakeAndroidKeyStore.NAME, 1.0, "In-memory test keystore") {
init {
put("KeyStore.${FakeAndroidKeyStore.NAME}", FakeKeyStoreSpi::class.java.name)
put("KeyGenerator.AES", FakeKeyGeneratorSpi::class.java.name)
}
}
/** Just enough of a keystore to hold secret keys under an alias. */
class FakeKeyStoreSpi : KeyStoreSpi() {
override fun engineGetKey(alias: String, password: CharArray?): Key? =
FakeAndroidKeyStore.keys[alias]
override fun engineGetEntry(
alias: String,
protParam: KeyStore.ProtectionParameter?,
): KeyStore.Entry? =
FakeAndroidKeyStore.keys[alias]?.let { KeyStore.SecretKeyEntry(it) }
override fun engineSetEntry(
alias: String,
entry: KeyStore.Entry,
protParam: KeyStore.ProtectionParameter?,
) {
(entry as? KeyStore.SecretKeyEntry)?.let { FakeAndroidKeyStore.keys[alias] = it.secretKey }
}
override fun engineSetKeyEntry(
alias: String,
key: Key,
password: CharArray?,
chain: Array<out Certificate>?,
) {
(key as? SecretKey)?.let { FakeAndroidKeyStore.keys[alias] = it }
}
override fun engineSetKeyEntry(alias: String, key: ByteArray?, chain: Array<out Certificate>?) =
throw UnsupportedOperationException()
override fun engineDeleteEntry(alias: String) {
FakeAndroidKeyStore.keys.remove(alias)
}
override fun engineContainsAlias(alias: String): Boolean =
FakeAndroidKeyStore.keys.containsKey(alias)
override fun engineAliases(): Enumeration<String> =
Collections.enumeration(FakeAndroidKeyStore.keys.keys.toList())
override fun engineSize(): Int = FakeAndroidKeyStore.keys.size
override fun engineIsKeyEntry(alias: String): Boolean = engineContainsAlias(alias)
override fun engineIsCertificateEntry(alias: String): Boolean = false
override fun engineGetCertificate(alias: String): Certificate? = null
override fun engineGetCertificateChain(alias: String): Array<Certificate>? = null
override fun engineGetCertificateAlias(cert: Certificate?): String? = null
override fun engineSetCertificateEntry(alias: String, cert: Certificate?) =
throw UnsupportedOperationException()
override fun engineGetCreationDate(alias: String): Date = Date(0)
override fun engineLoad(stream: InputStream?, password: CharArray?) = Unit
override fun engineStore(stream: OutputStream?, password: CharArray?) = Unit
}
/**
* Generates an ordinary AES key and files it under the alias the
* [KeyGenParameterSpec] names — which is what the real provider does, and what
* makes a key created once findable on the next launch.
*/
class FakeKeyGeneratorSpi : KeyGeneratorSpi() {
private var alias: String? = null
private var sizeBits = 256
override fun engineInit(random: SecureRandom?) = Unit
override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?) {
val spec = params as? KeyGenParameterSpec ?: return
alias = spec.keystoreAlias
sizeBits = spec.keySize.takeIf { it > 0 } ?: 256
}
override fun engineInit(keysize: Int, random: SecureRandom?) {
sizeBits = keysize
}
override fun engineGenerateKey(): SecretKey {
val key = KeyGenerator.getInstance("AES").apply { init(sizeBits) }.generateKey()
.let { SecretKeySpec(it.encoded, "AES") }
alias?.let { FakeAndroidKeyStore.keys[it] = key }
return key
}
}