Skip to content

Commit

Permalink
Merge pull request #249 from openziti/store-identity-in-file
Browse files Browse the repository at this point in the history
store identity config in file
  • Loading branch information
ekoby authored Dec 13, 2024
2 parents c2e2485 + 04e49e6 commit caaa17a
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 36 deletions.
107 changes: 77 additions & 30 deletions app/src/main/java/org/openziti/mobile/TunnelModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class TunnelModel(
val context: () -> Context
): ViewModel() {
val Context.prefs: DataStore<Preferences> by preferencesDataStore("tunnel")
val identitiesDir = context().getDir("identities", Context.MODE_PRIVATE)

val NAMESERVER = stringPreferencesKey("nameserver")
val zitiDNS = context().prefs.data.map {
Expand Down Expand Up @@ -86,6 +87,7 @@ class TunnelModel(

class TunnelIdentity(
val id: String,
val cfg: ZitiConfig,
private val tunnel: TunnelModel,
enable: Boolean = true
): ViewModel() {
Expand Down Expand Up @@ -137,7 +139,7 @@ class TunnelModel(

fun delete() {
setEnabled(false)
tunnel.deleteIdentity(id)
tunnel.deleteIdentity(id, cfg.id.key?.removePrefix("keychain:"))
}

internal fun processServiceUpdate(ev: ServiceEvent) {
Expand Down Expand Up @@ -171,35 +173,65 @@ class TunnelModel(
}
}

val aliases = Keychain.store.aliases().toList()
val configs = mutableMapOf<String, ZitiConfig>()

aliases.filter { it.startsWith("ziti://") }
.map { Pair(it, Keychain.store.getEntry(it, null)) }
.filter { it.second is PrivateKeyEntry }
.map {
val uri = URI(it.first)
val ctrl = "https://${uri.host}:${uri.port}"
val id = uri.userInfo ?: uri.path.removePrefix("/")
val idFiles = identitiesDir.listFiles() ?: emptyArray()
idFiles.forEach {
Log.i(TAG, "loading identity from file[$it]")
val cfg = Json.decodeFromString<ZitiConfig>(it.readText())
configs[cfg.identifier] = cfg
}

val loadedKeys = configs.mapNotNull { it.value.id.key?.removePrefix("keychain:") }

val idCerts = Keychain.store.getCertificateChain(it.first)
val pem = idCerts.map { it as X509Certificate }
.joinToString(transform = X509Certificate::toPEM, separator = "")
val caCerts = aliases.filter { it.startsWith("ziti:$id/") }
.map { Keychain.store.getCertificate(it) as X509Certificate}
.joinToString(transform = X509Certificate::toPEM, separator = "")
it.first to ZitiConfig(
controller = ctrl,
controllers = listOf(ctrl),
id = ZitiID(cert = pem, key = "keychain:${it.first}", ca = caCerts)
)
}.forEach {
loadConfig(it.first, it.second)
val aliases = Keychain.store.aliases().toList().filter { !loadedKeys.contains(it) }

for (alias in aliases) {
loadConfigFromKeyStore(alias)?.let { cfg ->
Log.i(TAG, "migrating identity from keychain[$alias]")
val uri = URI(alias)
val id = uri.userInfo ?: uri.path.removePrefix("/")
val json = Json.encodeToString(ZitiConfig.serializer(), cfg)
identitiesDir.resolve(cfg.identifier).outputStream().use {
it.write(json.toByteArray())
}
configs[id] = cfg
}
}

for (it in configs) {
loadIdentity(it.key, it.value)
}
}

private fun loadConfigFromKeyStore(alias: String): ZitiConfig? {
if (!Keychain.store.containsAlias(alias)) return null
if (!alias.startsWith("ziti://")) return null

val entry = Keychain.store.getEntry(alias, null)
if (entry !is PrivateKeyEntry) return null

val uri = URI(alias)
val ctrl = "https://${uri.host}:${uri.port}"
val id = uri.userInfo ?: uri.path.removePrefix("/")

val idCerts = Keychain.store.getCertificateChain(alias)
val pem = idCerts.map { it as X509Certificate }
.joinToString(transform = X509Certificate::toPEM, separator = "")
val caCerts = Keychain.store.aliases().toList().filter { it.startsWith("ziti:$id/") }
.map { Keychain.store.getCertificate(it) as X509Certificate}
.joinToString(transform = X509Certificate::toPEM, separator = "")

return ZitiConfig(
controller = ctrl,
controllers = listOf(ctrl),
id = ZitiID(cert = pem, key = "keychain:${alias}", ca = caCerts)
)
}

private fun disabledKey(id: String) = booleanPreferencesKey("$id.disabled")

private fun loadConfig(id: String, cfg: ZitiConfig) {
private fun loadIdentity(id: String, cfg: ZitiConfig) {
val disabled = runBlocking {
context().prefs.data.map {
it[disabledKey(id)] ?: false
Expand All @@ -211,7 +243,7 @@ class TunnelModel(
if (ex != null) {
Log.w("model", "failed to execute", ex)
} else {
identities[id] = TunnelIdentity(id, this, !disabled)
identities[id] = TunnelIdentity(id, cfg, this, !disabled)
identitiesData.postValue(identities.values.toList())
Log.i("model", "load result[$id]: $json")
}
Expand Down Expand Up @@ -258,9 +290,12 @@ class TunnelModel(
}

future.thenApply { cfg ->
val keyAlias = cfg.id.key.removePrefix("keychain:")
Keychain.updateKeyEntry(keyAlias, cfg.id.cert, cfg.id.ca)
loadConfig(keyAlias, cfg)
val cfgJson = Json.encodeToString(ZitiConfig.serializer(), cfg)
identitiesDir.resolve(cfg.identifier).outputStream().use {
it.write(cfgJson.toByteArray())
}

loadIdentity(cfg.identifier, cfg)
}.exceptionally {
Log.e("model", "enrollment failed", it)
}
Expand Down Expand Up @@ -289,17 +324,22 @@ class TunnelModel(
return tunnel.processCmd(OnOffCommand(id, on)).thenApply {}
}

private fun deleteIdentity(identifier: String) {
private fun deleteIdentity(identifier: String, key: String?) {
identities.remove(identifier)
identitiesData.postValue(identities.values.toList())

val uri = URI(identifier)
val id = uri.userInfo ?: uri.path.removePrefix("/")

key?.let {
runCatching { Keychain.store.deleteEntry(key) }
.onFailure { Log.w(TAG, "failed to remove entry", it) }
}

runCatching {
Keychain.store.deleteEntry(identifier)
identitiesDir.resolve(id).delete()
}.onFailure {
Log.w(TAG, "failed to remove entry", it)
Log.w(TAG, "failed to remove config", it)
}

val caCerts = Keychain.store.aliases().toList().filter { it.startsWith("ziti:$id/") }
Expand All @@ -309,6 +349,13 @@ class TunnelModel(
}
}

runBlocking {
context().prefs.edit {
val prefKey = disabledKey(identifier)
if (it.contains(prefKey))
it.remove(prefKey)
}
}
}

companion object {
Expand Down
4 changes: 2 additions & 2 deletions tunnel/src/main/java/org/openziti/tunnel/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ sealed class Event {
data class ContextEvent(
override val identifier: String,
val status: String,
val name: String,
val controller: String): Event()
val name: String?,
val controller: String?): Event()

@Serializable @SerialName("APIEvent")
data class APIEvent(
Expand Down
14 changes: 10 additions & 4 deletions tunnel/src/main/java/org/openziti/tunnel/TunnelCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import java.net.URI

enum class CMD {
ZitiDump,
Expand Down Expand Up @@ -41,16 +42,21 @@ enum class CMD {
}

@Serializable data class ZitiID (
val cert: String,
val key: String,
val cert: String?,
val key: String?,
val ca: String
)

@Serializable data class ZitiConfig(
@SerialName("ztAPI") val controller: String? = null,
@SerialName("ztAPI") val controller: String,
@SerialName("ztAPIs") val controllers: List<String>? = null,
val id: ZitiID,
)
) {
val identifier: String = if (id.key != null)
URI(id.key.removePrefix("keychain:")).let { it.userInfo ?: it.path.removePrefix("/") }
else
URI(controller).host.replace(".", "_")
}

@Serializable sealed class TunnelCommand(@Transient val cmd: CMD = CMD.Status)
@Serializable data object ListIdentities: TunnelCommand(CMD.ListIdentities)
Expand Down

0 comments on commit caaa17a

Please sign in to comment.