From ba86685935da90f31ef4f3f7113565f4e2a47fd4 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Sun, 8 Nov 2026 21:52:24 -0800 Subject: [PATCH] A certificate can be downloaded from any link and converted, and fetched again later A .p12, with or without a password, or a certificate and key as PEM or DER, in one file or two. Gearslip ships no address of its own; the driver pastes one, and it's kept for Download again. --- .../seb3thehacker/gearslip/CertFromLink.kt | 175 ++++++++++++++++++ .../gearslip/ui/SettingsScreen.kt | 97 ++++++++++ gearslip/src/main/res/values-ar/strings.xml | 13 ++ gearslip/src/main/res/values-cs/strings.xml | 13 ++ gearslip/src/main/res/values-de/strings.xml | 13 ++ gearslip/src/main/res/values-es/strings.xml | 13 ++ gearslip/src/main/res/values-fr/strings.xml | 13 ++ gearslip/src/main/res/values-it/strings.xml | 13 ++ gearslip/src/main/res/values-ja/strings.xml | 13 ++ gearslip/src/main/res/values-ko/strings.xml | 13 ++ gearslip/src/main/res/values-nl/strings.xml | 13 ++ gearslip/src/main/res/values-pl/strings.xml | 13 ++ .../src/main/res/values-pt-rBR/strings.xml | 13 ++ gearslip/src/main/res/values-ro/strings.xml | 13 ++ gearslip/src/main/res/values-ru/strings.xml | 13 ++ gearslip/src/main/res/values-sv/strings.xml | 13 ++ gearslip/src/main/res/values-tr/strings.xml | 13 ++ gearslip/src/main/res/values-uk/strings.xml | 13 ++ .../src/main/res/values-zh-rCN/strings.xml | 13 ++ gearslip/src/main/res/values/strings.xml | 13 ++ .../gearslip/CertFromLinkTest.kt | 56 ++++++ 21 files changed, 562 insertions(+) create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/CertFromLink.kt create mode 100644 gearslip/src/test/java/app/seb3thehacker/gearslip/CertFromLinkTest.kt diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/CertFromLink.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/CertFromLink.kt new file mode 100644 index 0000000..38c5e82 --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/CertFromLink.kt @@ -0,0 +1,175 @@ +package app.seb3thehacker.gearslip + +import android.content.Context +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.security.KeyFactory +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 + +/** + * A certificate fetched from a link the driver pastes (issue #15), in whatever form it comes: + * a PKCS#12 file, with or without a password, or a certificate and its private key as PEM or + * DER, together in one file or in two. Whatever arrives is converted to the PKCS#12 identity + * Gearslip presents to the car, and stored as an imported certificate. + * + * Gearslip ships no address of its own. The links are kept on the phone so the same files can + * be fetched again from Settings, and they're only ever fetched when the driver asks. + */ +object CertFromLink { + + sealed interface Outcome { + /** Imported, and in use from the next connection. */ + data object Done : Outcome + /** A PKCS#12 file that needs its password. */ + data object NeedsPassword : Outcome + /** A certificate on its own: its private key is at another address. */ + data object NeedsKey : Outcome + } + + private const val KEY_CERT_LINK = "cert_link" + private const val KEY_KEY_LINK = "cert_key_link" + private const val MAX_BYTES = 1L * 1024 * 1024 + /** The password the converted file is stored under; it never leaves the app's storage. */ + private const val STORE_PASSWORD = "gearslip" + + /** The link used last, for Download again, or null when none was ever used. */ + fun savedLink(context: Context): Pair? { + val cert = AppSettings.getString(context, KEY_CERT_LINK, "").ifEmpty { return null } + return cert to AppSettings.getString(context, KEY_KEY_LINK, "").ifEmpty { null } + } + + /** Runs on a background thread. Throws with a message a driver can read when it can't be used. */ + fun import(context: Context, certLink: String, keyLink: String?, password: String?): Outcome { + val first = download(certLink) + // The key's link is fetched only when the first file turns out to hold no key. + var second: ByteArray? = null + var converted = convert(first, null, password) + if (converted is Converted.NeedsKey && !keyLink.isNullOrBlank()) { + second = download(keyLink) + converted = convert(first, second, password) + } + return when (converted) { + Converted.NeedsKey -> Outcome.NeedsKey + Converted.NeedsPassword -> Outcome.NeedsPassword + is Converted.Ready -> { + CertProvider.importFromBytes(context, converted.p12, converted.password) + remember(context, certLink, keyLink.takeIf { second != null }) + Outcome.Done + } + } + } + + internal sealed interface Converted { + class Ready(val p12: ByteArray, val password: String) : Converted + data object NeedsPassword : Converted + data object NeedsKey : Converted + } + + /** + * Turns what was downloaded into a PKCS#12 file with its password. [first] is the file at + * the first link; [second] the key from the second, once asked for. + */ + internal fun convert(first: ByteArray, second: ByteArray?, password: String?): Converted { + // A PKCS#12 file is DER too, so the certificate parser gets the first try. + val certs = certificates(first) + if (certs.isEmpty()) { + val pass = password.orEmpty() + val loaded = runCatching { KeyStore.getInstance("PKCS12").apply { load(ByteArrayInputStream(first), pass.toCharArray()) } } + if (loaded.isFailure) { + if (password == null) return Converted.NeedsPassword + error("That password doesn't open the file, or it isn't a certificate.") + } + val store = loaded.getOrThrow() + require(store.aliases().asSequence().any { store.isKeyEntry(it) }) { "The file holds no private key, so the car can't be shown it." } + return Converted.Ready(first, pass) + } + + val key = privateKey(first) ?: second?.let { + privateKey(it) ?: error("The second link isn't a private key in a form Gearslip reads.") + } ?: return Converted.NeedsKey + // The certificate that goes with the key comes first, and any issuers after it. + val leaf = certs.firstOrNull { belongs(key, it) } ?: error("The private key doesn't belong to this certificate.") + val chain = listOf(leaf) + certs.filter { it !== leaf } + val store = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setKeyEntry("phone", key, STORE_PASSWORD.toCharArray(), chain.toTypedArray()) + } + val bytes = ByteArrayOutputStream().use { store.store(it, STORE_PASSWORD.toCharArray()); it.toByteArray() } + GearslipLog.i("certificate from a link: ${leaf.subjectX500Principal}") + return Converted.Ready(bytes, STORE_PASSWORD) + } + + private fun remember(context: Context, cert: String, key: String?) { + AppSettings.putString(context, KEY_CERT_LINK, cert) + AppSettings.putString(context, KEY_KEY_LINK, key.orEmpty()) + } + + private fun download(link: String): ByteArray { + val url = link.trim() + require(url.startsWith("https://") || url.startsWith("http://")) { "That isn't a web address." } + val connection = (URL(url).openConnection() as HttpURLConnection).apply { + connectTimeout = 15_000 + readTimeout = 15_000 + instanceFollowRedirects = true + } + try { + if (connection.responseCode !in 200..299) error("The server answered ${connection.responseCode}.") + val out = ByteArrayOutputStream() + connection.inputStream.use { input -> + val buffer = ByteArray(16 * 1024) + while (true) { + val read = input.read(buffer) + if (read < 0) break + out.write(buffer, 0, read) + require(out.size() <= MAX_BYTES) { "That file is far too big to be a certificate." } + } + } + return out.toByteArray() + } finally { + connection.disconnect() + } + } + + /** Every certificate in PEM text, or the one certificate a DER file holds. */ + private fun certificates(bytes: ByteArray): List { + val factory = CertificateFactory.getInstance("X.509") + val pem = pemBlocks(bytes, "CERTIFICATE") + if (pem.isNotEmpty()) return pem.map { factory.generateCertificate(ByteArrayInputStream(it)) as X509Certificate } + return listOfNotNull(runCatching { factory.generateCertificate(ByteArrayInputStream(bytes)) as X509Certificate }.getOrNull()) + } + + /** A PKCS#8 or PKCS#1 key, as PEM or as PKCS#8 DER; null when there's none. */ + private fun privateKey(bytes: ByteArray): PrivateKey? { + require(pemBlocks(bytes, "ENCRYPTED PRIVATE KEY").isEmpty()) { "The private key is encrypted. Gearslip needs one without a passphrase, or a .p12 file." } + pemBlocks(bytes, "PRIVATE KEY").firstOrNull()?.let { return pkcs8(it) } + if (pemBlocks(bytes, "RSA PRIVATE KEY").isNotEmpty()) return PemKeyParser.parsePrivateKey(String(bytes)).getPrivate() + if (String(bytes).contains("-----BEGIN")) return null + return runCatching { pkcs8(bytes) }.getOrNull() + } + + private fun pkcs8(der: ByteArray): PrivateKey = runCatching { + KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(der)) + }.getOrElse { KeyFactory.getInstance("EC").generatePrivate(PKCS8EncodedKeySpec(der)) } + + /** + * Whether [key] is [cert]'s. RSA keys are matched by modulus, which every Android provider + * exposes, even one that hides the rest of a PKCS#8 key. Any other kind is taken on trust. + */ + private fun belongs(key: PrivateKey, cert: X509Certificate): Boolean { + val public = cert.publicKey + return if (key is java.security.interfaces.RSAKey && public is java.security.interfaces.RSAKey) key.modulus == public.modulus else key.algorithm == public.algorithm + } + + private fun pemBlocks(bytes: ByteArray, type: String): List { + val text = runCatching { String(bytes, Charsets.US_ASCII) }.getOrDefault("") + return Regex("-----BEGIN $type-----([A-Za-z0-9+/=\\s]+)-----END $type-----").findAll(text) + .map { Base64.getMimeDecoder().decode(it.groupValues[1]) }.toList() + } +} diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt index 896bdc2..0f6222f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt @@ -1,5 +1,7 @@ package app.seb3thehacker.gearslip.ui +import androidx.compose.material.icons.filled.Create + import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import app.seb3thehacker.gearslip.R @@ -356,6 +358,8 @@ private fun CertificateSection() { var notice by remember { mutableStateOf(null) } var downloading by remember { mutableStateOf(false) } var detailsOpen by remember { mutableStateOf(false) } + var linkDialog by remember { mutableStateOf(null) } + val savedLink = remember(refresh) { app.seb3thehacker.gearslip.CertFromLink.savedLink(context) } val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> if (uri != null) pendingUri = uri } @@ -413,6 +417,22 @@ private fun CertificateSection() { icon = Icons.Filled.Refresh, ) SettingsDivider() + SettingsRow( + stringResource(R.string.cert_link), + Modifier.clickable { linkDialog = LinkRequest() }, + subtitle = stringResource(R.string.cert_link_sub), + icon = Icons.Filled.Create, + ) + savedLink?.let { (cert, key) -> + SettingsDivider() + SettingsRow( + stringResource(R.string.cert_link_again), + Modifier.clickable { linkDialog = LinkRequest(cert, key, start = true) }, + subtitle = cert.substringAfter("://").substringBefore('/'), + icon = Icons.Filled.Refresh, + ) + } + SettingsDivider() SettingsRow( stringResource(R.string.cert_import), Modifier.clickable { picker.launch(arrayOf("*/*")) }, @@ -440,6 +460,14 @@ private fun CertificateSection() { } } + linkDialog?.let { request -> + LinkDialog(request, onDismiss = { linkDialog = null }) { + linkDialog = null + notice = context.getString(R.string.cert_link_done) + refresh++ + } + } + pendingUri?.let { uri -> PasswordDialog( onDismiss = { pendingUri = null }, @@ -461,6 +489,75 @@ private fun CertificateSection() { } } +/** What the link dialog opens with: blank, or the saved link to fetch again straight away. */ +private class LinkRequest(val cert: String = "https://", val key: String? = null, val start: Boolean = false) + +/** + * Fetches a certificate from a link (issue #15). It asks for more only when the file needs it: + * the password of a .p12, or the link to a key that came separately. + */ +@Composable +private fun LinkDialog(request: LinkRequest, onDismiss: () -> Unit, onDone: () -> Unit) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var cert by remember { mutableStateOf(request.cert) } + var key by remember { mutableStateOf(request.key) } + var password by remember { mutableStateOf(null) } + var error by remember { mutableStateOf(null) } + var busy by remember { mutableStateOf(false) } + fun go() { + busy = true + error = null + scope.launch { + val result = withContext(Dispatchers.IO) { + runCatching { app.seb3thehacker.gearslip.CertFromLink.import(context, cert, key?.takeIf { it.isNotBlank() }, password) } + } + busy = false + result.onSuccess { outcome -> + when (outcome) { + app.seb3thehacker.gearslip.CertFromLink.Outcome.Done -> onDone() + app.seb3thehacker.gearslip.CertFromLink.Outcome.NeedsKey -> if (key == null) key = "https://" else error = context.getString(R.string.cert_link_key_note) + app.seb3thehacker.gearslip.CertFromLink.Outcome.NeedsPassword -> if (password == null) password = "" else error = context.getString(R.string.cert_import_failed) + } + }.onFailure { error = context.getString(R.string.cert_link_failed, it.message.orEmpty()) } + } + } + LaunchedEffect(request) { if (request.start) go() } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.cert_link_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.cert_link_note)) + OutlinedTextField( + cert, { cert = it }, label = { Text(stringResource(R.string.cert_link_label)) }, singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + ) + key?.let { k -> + Text(stringResource(R.string.cert_link_key_note), style = MaterialTheme.typography.bodySmall) + OutlinedTextField( + k, { key = it }, label = { Text(stringResource(R.string.cert_link_key)) }, singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + ) + } + password?.let { p -> + Text(stringResource(R.string.cert_link_password_note), style = MaterialTheme.typography.bodySmall) + OutlinedTextField( + p, { password = it }, label = { Text(stringResource(R.string.cert_link_password)) }, singleLine = true, + visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + ) + } + error?.let { Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) } + } + }, + confirmButton = { + TextButton(::go, enabled = !busy) { Text(stringResource(if (busy) R.string.cert_downloading else R.string.cert_link_download)) } + }, + dismissButton = { TextButton(onDismiss) { Text(stringResource(R.string.cancel)) } }, + ) +} + @Composable private fun Detail(label: String, value: String) { Column { diff --git a/gearslip/src/main/res/values-ar/strings.xml b/gearslip/src/main/res/values-ar/strings.xml index b2edf18..1dfede4 100644 --- a/gearslip/src/main/res/values-ar/strings.xml +++ b/gearslip/src/main/res/values-ar/strings.xml @@ -992,4 +992,17 @@ إشعاراته، تُقرأ بصوت عالٍ، مع الردود. أضفته أنت إزالة %1$s + تنزيل من رابط + ملف شهادة تثق به، من عنوان تلصقه + شهادة من رابط + الصق عنوان ملف ‎.p12، أو عنوان شهادة ومفتاحها بصيغة PEM أو DER. يحوّلها Gearslip إلى الصيغة التي تحتاجها السيارة. + رابط الشهادة أو ملف ‎.p12 + رابط المفتاح الخاص + هذه شهادة بلا مفتاح. الصق رابط المفتاح أيضًا. + كلمة المرور + لهذا الملف كلمة مرور. + تنزيل + التنزيل مجددًا + نُزّلت وحُوّلت. تُستخدم من الاتصال التالي. + تعذّر استخدامها. %1$s diff --git a/gearslip/src/main/res/values-cs/strings.xml b/gearslip/src/main/res/values-cs/strings.xml index 1f0e8a7..ede0ae7 100644 --- a/gearslip/src/main/res/values-cs/strings.xml +++ b/gearslip/src/main/res/values-cs/strings.xml @@ -972,4 +972,17 @@ Její oznámení, předčítaná nahlas, s odpověďmi. Přidáno vámi Odebrat %1$s + Stáhnout z odkazu + Soubor certifikátu, kterému věříte, z adresy, kterou vložíte + Certifikát z odkazu + Vložte adresu souboru .p12 nebo certifikátu a klíče ve formátu PEM či DER. Gearslip ho převede do podoby, kterou auto potřebuje. + Odkaz na certifikát nebo .p12 + Odkaz na soukromý klíč + Je to certifikát bez klíče. Vložte i odkaz na klíč. + Heslo + Tento soubor má heslo. + Stáhnout + Stáhnout znovu + Staženo a převedeno. Použije se od dalšího připojení. + Nelze použít. %1$s diff --git a/gearslip/src/main/res/values-de/strings.xml b/gearslip/src/main/res/values-de/strings.xml index 06df565..efcaf4d 100644 --- a/gearslip/src/main/res/values-de/strings.xml +++ b/gearslip/src/main/res/values-de/strings.xml @@ -834,4 +834,17 @@ Ihre Benachrichtigungen, vorgelesen, mit Antworten. Von dir hinzugefügt %1$s entfernen + Über einen Link herunterladen + Eine Zertifikatsdatei, der du vertraust, unter einer Adresse, die du einfügst + Zertifikat über einen Link + Füge die Adresse einer .p12-Datei ein oder die eines Zertifikats mit Schlüssel als PEM oder DER. Gearslip wandelt sie in die Form um, die das Auto braucht. + Link zum Zertifikat oder zur .p12-Datei + Link zum privaten Schlüssel + Das ist ein Zertifikat ohne Schlüssel. Füge auch den Link zum Schlüssel ein. + Passwort + Diese Datei hat ein Passwort. + Herunterladen + Erneut herunterladen + Heruntergeladen und umgewandelt. Es gilt ab der nächsten Verbindung. + Nicht verwendbar. %1$s diff --git a/gearslip/src/main/res/values-es/strings.xml b/gearslip/src/main/res/values-es/strings.xml index 5c468c9..327baea 100644 --- a/gearslip/src/main/res/values-es/strings.xml +++ b/gearslip/src/main/res/values-es/strings.xml @@ -836,4 +836,17 @@ Sus notificaciones, leídas en voz alta, con respuestas. Añadida por ti Quitar %1$s + Descargar desde un enlace + Un archivo de certificado de confianza, en una dirección que pegues + Certificado desde un enlace + Pega la dirección de un archivo .p12, o de un certificado y su clave en PEM o DER. Gearslip lo convierte al formato que necesita el coche. + Enlace del certificado o del .p12 + Enlace de la clave privada + Es un certificado sin su clave. Pega también el enlace de la clave. + Contraseña + Este archivo tiene contraseña. + Descargar + Descargar de nuevo + Descargado y convertido. Se usará en la próxima conexión. + No se pudo usar. %1$s diff --git a/gearslip/src/main/res/values-fr/strings.xml b/gearslip/src/main/res/values-fr/strings.xml index df7e48b..4a2fbe8 100644 --- a/gearslip/src/main/res/values-fr/strings.xml +++ b/gearslip/src/main/res/values-fr/strings.xml @@ -962,4 +962,17 @@ Ses notifications, lues à voix haute, avec réponses. Ajoutée par vous Retirer %1$s + Télécharger depuis un lien + Un fichier de certificat de confiance, à une adresse que vous collez + Certificat depuis un lien + Collez l’adresse d’un fichier .p12, ou d’un certificat et de sa clé en PEM ou DER. Gearslip le convertit au format dont la voiture a besoin. + Lien du certificat ou du .p12 + Lien de la clé privée + C’est un certificat sans sa clé. Collez aussi le lien de la clé. + Mot de passe + Ce fichier a un mot de passe. + Télécharger + Télécharger à nouveau + Téléchargé et converti. Il sera utilisé à la prochaine connexion. + Impossible de l’utiliser. %1$s diff --git a/gearslip/src/main/res/values-it/strings.xml b/gearslip/src/main/res/values-it/strings.xml index 2f85ccc..447d95f 100644 --- a/gearslip/src/main/res/values-it/strings.xml +++ b/gearslip/src/main/res/values-it/strings.xml @@ -962,4 +962,17 @@ Le sue notifiche, lette ad alta voce, con risposte. Aggiunta da te Rimuovi %1$s + Scarica da un link + Un file di certificato di cui ti fidi, a un indirizzo che incolli + Certificato da un link + Incolla l’indirizzo di un file .p12, o di un certificato e della sua chiave in PEM o DER. Gearslip lo converte nel formato che serve all’auto. + Link del certificato o del .p12 + Link della chiave privata + È un certificato senza la sua chiave. Incolla anche il link della chiave. + Password + Questo file ha una password. + Scarica + Scarica di nuovo + Scaricato e convertito. Verrà usato dalla prossima connessione. + Impossibile usarlo. %1$s diff --git a/gearslip/src/main/res/values-ja/strings.xml b/gearslip/src/main/res/values-ja/strings.xml index 0e305e7..593a519 100644 --- a/gearslip/src/main/res/values-ja/strings.xml +++ b/gearslip/src/main/res/values-ja/strings.xml @@ -942,4 +942,17 @@ 通知を読み上げ、返信できます。 自分で追加 %1$s を削除 + リンクからダウンロード + 信頼できる証明書ファイルを、貼り付けたアドレスから + リンクから証明書 + .p12 ファイル、または PEM か DER の証明書と鍵のアドレスを貼り付けてください。Gearslip が車に必要な形式に変換します。 + 証明書または .p12 のリンク + 秘密鍵のリンク + 鍵のない証明書です。鍵のリンクも貼り付けてください。 + パスワード + このファイルにはパスワードがあります。 + ダウンロード + もう一度ダウンロード + ダウンロードして変換しました。次の接続から使われます。 + 使用できませんでした。%1$s diff --git a/gearslip/src/main/res/values-ko/strings.xml b/gearslip/src/main/res/values-ko/strings.xml index 8ba877c..374d751 100644 --- a/gearslip/src/main/res/values-ko/strings.xml +++ b/gearslip/src/main/res/values-ko/strings.xml @@ -942,4 +942,17 @@ 알림을 소리 내어 읽고 답장할 수 있습니다. 직접 추가함 %1$s 삭제 + 링크에서 다운로드 + 신뢰하는 인증서 파일을 붙여넣은 주소에서 + 링크로 인증서 가져오기 + .p12 파일 또는 PEM이나 DER 형식의 인증서와 키 주소를 붙여넣으세요. Gearslip이 차량에 필요한 형식으로 변환합니다. + 인증서 또는 .p12 링크 + 개인 키 링크 + 키가 없는 인증서입니다. 키 링크도 붙여넣으세요. + 비밀번호 + 이 파일에는 비밀번호가 있습니다. + 다운로드 + 다시 다운로드 + 다운로드하고 변환했습니다. 다음 연결부터 사용됩니다. + 사용할 수 없습니다. %1$s diff --git a/gearslip/src/main/res/values-nl/strings.xml b/gearslip/src/main/res/values-nl/strings.xml index 8fa839a..f66e695 100644 --- a/gearslip/src/main/res/values-nl/strings.xml +++ b/gearslip/src/main/res/values-nl/strings.xml @@ -952,4 +952,17 @@ De meldingen, voorgelezen, met antwoorden. Door jou toegevoegd %1$s verwijderen + Downloaden via een link + Een certificaatbestand dat je vertrouwt, op een adres dat je plakt + Certificaat via een link + Plak het adres van een .p12-bestand, of van een certificaat en sleutel in PEM of DER. Gearslip zet het om naar de vorm die de auto nodig heeft. + Link naar certificaat of .p12 + Link naar de privésleutel + Dat is een certificaat zonder sleutel. Plak ook de link naar de sleutel. + Wachtwoord + Dit bestand heeft een wachtwoord. + Downloaden + Opnieuw downloaden + Gedownload en omgezet. Het wordt gebruikt vanaf de volgende verbinding. + Kon het niet gebruiken. %1$s diff --git a/gearslip/src/main/res/values-pl/strings.xml b/gearslip/src/main/res/values-pl/strings.xml index aa90e82..8489bab 100644 --- a/gearslip/src/main/res/values-pl/strings.xml +++ b/gearslip/src/main/res/values-pl/strings.xml @@ -972,4 +972,17 @@ Jej powiadomienia, czytane na głos, z odpowiedziami. Dodana przez Ciebie Usuń %1$s + Pobierz z linku + Plik certyfikatu, któremu ufasz, spod wklejonego adresu + Certyfikat z linku + Wklej adres pliku .p12 albo certyfikatu i klucza w PEM lub DER. Gearslip przekształci go do postaci, której potrzebuje samochód. + Link do certyfikatu lub pliku .p12 + Link do klucza prywatnego + To certyfikat bez klucza. Wklej też link do klucza. + Hasło + Ten plik ma hasło. + Pobierz + Pobierz ponownie + Pobrano i przekształcono. Zostanie użyty od następnego połączenia. + Nie można go użyć. %1$s diff --git a/gearslip/src/main/res/values-pt-rBR/strings.xml b/gearslip/src/main/res/values-pt-rBR/strings.xml index 6dfccd4..e2433f8 100644 --- a/gearslip/src/main/res/values-pt-rBR/strings.xml +++ b/gearslip/src/main/res/values-pt-rBR/strings.xml @@ -962,4 +962,17 @@ As notificações dele, lidas em voz alta, com respostas. Adicionado por você Remover %1$s + Baixar de um link + Um arquivo de certificado confiável, em um endereço que você cola + Certificado de um link + Cole o endereço de um arquivo .p12, ou de um certificado e sua chave em PEM ou DER. O Gearslip converte para o formato que o carro precisa. + Link do certificado ou do .p12 + Link da chave privada + É um certificado sem a chave. Cole também o link da chave. + Senha + Este arquivo tem senha. + Baixar + Baixar de novo + Baixado e convertido. Será usado a partir da próxima conexão. + Não foi possível usar. %1$s diff --git a/gearslip/src/main/res/values-ro/strings.xml b/gearslip/src/main/res/values-ro/strings.xml index 94c0d8c..d7823d9 100644 --- a/gearslip/src/main/res/values-ro/strings.xml +++ b/gearslip/src/main/res/values-ro/strings.xml @@ -844,4 +844,17 @@ Notificările ei, citite cu voce tare, cu răspunsuri. Adăugată de tine Elimină %1$s + Descarcă dintr-un link + Un fișier de certificat de încredere, de la o adresă pe care o lipești + Certificat dintr-un link + Lipește adresa unui fișier .p12 sau a unui certificat și a cheii lui în PEM ori DER. Gearslip îl convertește în forma de care are nevoie mașina. + Linkul certificatului sau al .p12 + Linkul cheii private + Este un certificat fără cheie. Lipește și linkul cheii. + Parolă + Fișierul are o parolă. + Descarcă + Descarcă din nou + Descărcat și convertit. Se folosește de la următoarea conectare. + Nu a putut fi folosit. %1$s diff --git a/gearslip/src/main/res/values-ru/strings.xml b/gearslip/src/main/res/values-ru/strings.xml index 9a3ec2f..d0183de 100644 --- a/gearslip/src/main/res/values-ru/strings.xml +++ b/gearslip/src/main/res/values-ru/strings.xml @@ -972,4 +972,17 @@ Его уведомления, прочитанные вслух, с ответами. Добавлено вами Удалить %1$s + Скачать по ссылке + Файл сертификата, которому вы доверяете, по адресу, который вы вставите + Сертификат по ссылке + Вставьте адрес файла .p12 или сертификата с ключом в формате PEM или DER. Gearslip преобразует его в нужный машине вид. + Ссылка на сертификат или .p12 + Ссылка на закрытый ключ + Это сертификат без ключа. Вставьте также ссылку на ключ. + Пароль + У этого файла есть пароль. + Скачать + Скачать снова + Скачано и преобразовано. Будет использоваться со следующего подключения. + Не удалось использовать. %1$s diff --git a/gearslip/src/main/res/values-sv/strings.xml b/gearslip/src/main/res/values-sv/strings.xml index 8682d6b..a5f9eef 100644 --- a/gearslip/src/main/res/values-sv/strings.xml +++ b/gearslip/src/main/res/values-sv/strings.xml @@ -952,4 +952,17 @@ Dess aviseringar, upplästa, med svar. Tillagd av dig Ta bort %1$s + Ladda ner från en länk + En certifikatfil du litar på, från en adress du klistrar in + Certifikat från en länk + Klistra in adressen till en .p12-fil, eller till ett certifikat och en nyckel i PEM eller DER. Gearslip gör om den till den form bilen behöver. + Länk till certifikat eller .p12 + Länk till den privata nyckeln + Det är ett certifikat utan nyckel. Klistra in nyckelns länk också. + Lösenord + Filen har ett lösenord. + Ladda ner + Ladda ner igen + Nedladdat och omgjort. Det används från nästa anslutning. + Det gick inte att använda. %1$s diff --git a/gearslip/src/main/res/values-tr/strings.xml b/gearslip/src/main/res/values-tr/strings.xml index c897e67..466e6ca 100644 --- a/gearslip/src/main/res/values-tr/strings.xml +++ b/gearslip/src/main/res/values-tr/strings.xml @@ -952,4 +952,17 @@ Bildirimleri, sesli okunur ve yanıtlanabilir. Sizin eklediğiniz %1$s kaldır + Bir bağlantıdan indir + Güvendiğiniz bir sertifika dosyası, yapıştırdığınız bir adreste + Bağlantıdan sertifika + Bir .p12 dosyasının ya da PEM veya DER biçiminde bir sertifika ve anahtarın adresini yapıştırın. Gearslip bunu aracın istediği biçime dönüştürür. + Sertifika veya .p12 bağlantısı + Özel anahtarın bağlantısı + Bu, anahtarı olmayan bir sertifika. Anahtarın bağlantısını da yapıştırın. + Parola + Bu dosyanın parolası var. + İndir + Yeniden indir + İndirildi ve dönüştürüldü. Bir sonraki bağlantıdan itibaren kullanılır. + Kullanılamadı. %1$s diff --git a/gearslip/src/main/res/values-uk/strings.xml b/gearslip/src/main/res/values-uk/strings.xml index 9ad3ae2..7e7cca0 100644 --- a/gearslip/src/main/res/values-uk/strings.xml +++ b/gearslip/src/main/res/values-uk/strings.xml @@ -972,4 +972,17 @@ Його сповіщення, прочитані вголос, із відповідями. Додано вами Видалити %1$s + Завантажити за посиланням + Файл сертифіката, якому ви довіряєте, за адресою, яку ви вставите + Сертифікат за посиланням + Вставте адресу файлу .p12 або сертифіката з ключем у форматі PEM чи DER. Gearslip перетворить його на потрібний авто вигляд. + Посилання на сертифікат або .p12 + Посилання на закритий ключ + Це сертифікат без ключа. Вставте також посилання на ключ. + Пароль + Цей файл має пароль. + Завантажити + Завантажити знову + Завантажено й перетворено. Використовуватиметься з наступного підключення. + Не вдалося використати. %1$s diff --git a/gearslip/src/main/res/values-zh-rCN/strings.xml b/gearslip/src/main/res/values-zh-rCN/strings.xml index 7029074..eaa8fb9 100644 --- a/gearslip/src/main/res/values-zh-rCN/strings.xml +++ b/gearslip/src/main/res/values-zh-rCN/strings.xml @@ -942,4 +942,17 @@ 朗读其通知,并可回复。 由你添加 移除 %1$s + 从链接下载 + 从你粘贴的地址下载你信任的证书文件 + 从链接获取证书 + 粘贴 .p12 文件的地址,或 PEM、DER 格式的证书和密钥的地址。Gearslip 会把它转换成汽车需要的格式。 + 证书或 .p12 链接 + 私钥链接 + 这是没有密钥的证书。请同时粘贴密钥的链接。 + 密码 + 此文件有密码。 + 下载 + 重新下载 + 已下载并转换。将在下次连接时使用。 + 无法使用。%1$s diff --git a/gearslip/src/main/res/values/strings.xml b/gearslip/src/main/res/values/strings.xml index 782a194..e7bbec1 100644 --- a/gearslip/src/main/res/values/strings.xml +++ b/gearslip/src/main/res/values/strings.xml @@ -1009,4 +1009,17 @@ Its notifications, read aloud, with replies. Added by you Remove %1$s + Download from a link + A certificate file you trust, at an address you paste + Certificate from a link + Paste the address of a .p12 file, or of a certificate and key in PEM or DER. Gearslip converts it to the form the car needs. + Certificate or .p12 link + Link to the private key + That\'s a certificate without its key. Paste the key\'s link too. + Password + This file has a password. + Download + Download again + Downloaded and converted. It\'s used from the next connection. + Couldn\'t use it. %1$s diff --git a/gearslip/src/test/java/app/seb3thehacker/gearslip/CertFromLinkTest.kt b/gearslip/src/test/java/app/seb3thehacker/gearslip/CertFromLinkTest.kt new file mode 100644 index 0000000..8a52722 --- /dev/null +++ b/gearslip/src/test/java/app/seb3thehacker/gearslip/CertFromLinkTest.kt @@ -0,0 +1,56 @@ +package app.seb3thehacker.gearslip + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.security.KeyStore +import java.security.PrivateKey +import java.util.Base64 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +/** The forms a certificate can arrive in from a link (issue #15), each converted to a usable PKCS#12. */ +class CertFromLinkTest { + + private val generated = SelfSignedCert.generate() + private val key = generated.keyStore.getKey(SelfSignedCert.ALIAS, SelfSignedCert.PASSWORD) as PrivateKey + private val certPem = pem("CERTIFICATE", generated.certificate.encoded) + private val keyPem = pem("PRIVATE KEY", key.encoded) + + private fun pem(type: String, der: ByteArray) = + "-----BEGIN $type-----\n" + Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(der) + "\n-----END $type-----\n" + + private fun assertUsable(result: CertFromLink.Converted) { + assertTrue("expected a ready file, got $result", result is CertFromLink.Converted.Ready) + val ready = result as CertFromLink.Converted.Ready + val store = KeyStore.getInstance("PKCS12").apply { load(ByteArrayInputStream(ready.p12), ready.password.toCharArray()) } + val alias = store.aliases().asSequence().first { store.isKeyEntry(it) } + assertEquals(generated.certificate, store.getCertificate(alias)) + } + + @Test fun certificateAndKeyInOnePem() = assertUsable(CertFromLink.convert((certPem + keyPem).toByteArray(), null, null)) + + @Test fun certificateAloneAsksForTheKey() = + assertEquals(CertFromLink.Converted.NeedsKey, CertFromLink.convert(certPem.toByteArray(), null, null)) + + @Test fun certificateAndKeyInTwoFiles() = assertUsable(CertFromLink.convert(certPem.toByteArray(), keyPem.toByteArray(), null)) + + @Test fun derCertificateAndDerKey() = assertUsable(CertFromLink.convert(generated.certificate.encoded, key.encoded, null)) + + @Test fun p12WithPassword() { + val bytes = ByteArrayOutputStream().use { generated.keyStore.store(it, "secret".toCharArray()); it.toByteArray() } + assertEquals(CertFromLink.Converted.NeedsPassword, CertFromLink.convert(bytes, null, null)) + assertUsable(CertFromLink.convert(bytes, null, "secret")) + } + + @Test fun keyFromAnotherCertificateIsRefused() { + val other = SelfSignedCert.generate() + val otherKey = other.keyStore.getKey(SelfSignedCert.ALIAS, SelfSignedCert.PASSWORD) as PrivateKey + try { + CertFromLink.convert(certPem.toByteArray(), pem("PRIVATE KEY", otherKey.encoded).toByteArray(), null) + fail("a key from another certificate was accepted") + } catch (_: IllegalStateException) { + } + } +}