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.
This commit is contained in:
Seb3thehacker committed 2026-11-08 21:52:24 -08:00
1 parent 85c8133442
commit ba86685935
21 files changed
+562

No files matched your search

@@ -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<String, String?>? {
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<X509Certificate> {
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<ByteArray> {
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()
}
}
@@ -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<String?>(null) }
var downloading by remember { mutableStateOf(false) }
var detailsOpen by remember { mutableStateOf(false) }
var linkDialog by remember { mutableStateOf<LinkRequest?>(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<String?>(null) }
var error by remember { mutableStateOf<String?>(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 {
@@ -992,4 +992,17 @@
<string name="car_apps_as_messaging_note">إشعاراته، تُقرأ بصوت عالٍ، مع الردود.</string>
<string name="car_apps_added_by_you">أضفته أنت</string>
<string name="car_apps_remove">إزالة %1$s</string>
<string name="cert_link">تنزيل من رابط</string>
<string name="cert_link_sub">ملف شهادة تثق به، من عنوان تلصقه</string>
<string name="cert_link_title">شهادة من رابط</string>
<string name="cert_link_note">الصق عنوان ملف ‎.p12، أو عنوان شهادة ومفتاحها بصيغة PEM أو DER. يحوّلها Gearslip إلى الصيغة التي تحتاجها السيارة.</string>
<string name="cert_link_label">رابط الشهادة أو ملف ‎.p12</string>
<string name="cert_link_key">رابط المفتاح الخاص</string>
<string name="cert_link_key_note">هذه شهادة بلا مفتاح. الصق رابط المفتاح أيضًا.</string>
<string name="cert_link_password">كلمة المرور</string>
<string name="cert_link_password_note">لهذا الملف كلمة مرور.</string>
<string name="cert_link_download">تنزيل</string>
<string name="cert_link_again">التنزيل مجددًا</string>
<string name="cert_link_done">نُزّلت وحُوّلت. تُستخدم من الاتصال التالي.</string>
<string name="cert_link_failed">تعذّر استخدامها. %1$s</string>
</resources>
@@ -972,4 +972,17 @@
<string name="car_apps_as_messaging_note">Její oznámení, předčítaná nahlas, s odpověďmi.</string>
<string name="car_apps_added_by_you">Přidáno vámi</string>
<string name="car_apps_remove">Odebrat %1$s</string>
<string name="cert_link">Stáhnout z odkazu</string>
<string name="cert_link_sub">Soubor certifikátu, kterému věříte, z adresy, kterou vložíte</string>
<string name="cert_link_title">Certifikát z odkazu</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Odkaz na certifikát nebo .p12</string>
<string name="cert_link_key">Odkaz na soukromý klíč</string>
<string name="cert_link_key_note">Je to certifikát bez klíče. Vložte i odkaz na klíč.</string>
<string name="cert_link_password">Heslo</string>
<string name="cert_link_password_note">Tento soubor má heslo.</string>
<string name="cert_link_download">Stáhnout</string>
<string name="cert_link_again">Stáhnout znovu</string>
<string name="cert_link_done">Staženo a převedeno. Použije se od dalšího připojení.</string>
<string name="cert_link_failed">Nelze použít. %1$s</string>
</resources>
@@ -834,4 +834,17 @@
<string name="car_apps_as_messaging_note">Ihre Benachrichtigungen, vorgelesen, mit Antworten.</string>
<string name="car_apps_added_by_you">Von dir hinzugefügt</string>
<string name="car_apps_remove">%1$s entfernen</string>
<string name="cert_link">Über einen Link herunterladen</string>
<string name="cert_link_sub">Eine Zertifikatsdatei, der du vertraust, unter einer Adresse, die du einfügst</string>
<string name="cert_link_title">Zertifikat über einen Link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Link zum Zertifikat oder zur .p12-Datei</string>
<string name="cert_link_key">Link zum privaten Schlüssel</string>
<string name="cert_link_key_note">Das ist ein Zertifikat ohne Schlüssel. Füge auch den Link zum Schlüssel ein.</string>
<string name="cert_link_password">Passwort</string>
<string name="cert_link_password_note">Diese Datei hat ein Passwort.</string>
<string name="cert_link_download">Herunterladen</string>
<string name="cert_link_again">Erneut herunterladen</string>
<string name="cert_link_done">Heruntergeladen und umgewandelt. Es gilt ab der nächsten Verbindung.</string>
<string name="cert_link_failed">Nicht verwendbar. %1$s</string>
</resources>
@@ -836,4 +836,17 @@
<string name="car_apps_as_messaging_note">Sus notificaciones, leídas en voz alta, con respuestas.</string>
<string name="car_apps_added_by_you">Añadida por ti</string>
<string name="car_apps_remove">Quitar %1$s</string>
<string name="cert_link">Descargar desde un enlace</string>
<string name="cert_link_sub">Un archivo de certificado de confianza, en una dirección que pegues</string>
<string name="cert_link_title">Certificado desde un enlace</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Enlace del certificado o del .p12</string>
<string name="cert_link_key">Enlace de la clave privada</string>
<string name="cert_link_key_note">Es un certificado sin su clave. Pega también el enlace de la clave.</string>
<string name="cert_link_password">Contraseña</string>
<string name="cert_link_password_note">Este archivo tiene contraseña.</string>
<string name="cert_link_download">Descargar</string>
<string name="cert_link_again">Descargar de nuevo</string>
<string name="cert_link_done">Descargado y convertido. Se usará en la próxima conexión.</string>
<string name="cert_link_failed">No se pudo usar. %1$s</string>
</resources>
@@ -962,4 +962,17 @@
<string name="car_apps_as_messaging_note">Ses notifications, lues à voix haute, avec réponses.</string>
<string name="car_apps_added_by_you">Ajoutée par vous</string>
<string name="car_apps_remove">Retirer %1$s</string>
<string name="cert_link">Télécharger depuis un lien</string>
<string name="cert_link_sub">Un fichier de certificat de confiance, à une adresse que vous collez</string>
<string name="cert_link_title">Certificat depuis un lien</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Lien du certificat ou du .p12</string>
<string name="cert_link_key">Lien de la clé privée</string>
<string name="cert_link_key_note">C’est un certificat sans sa clé. Collez aussi le lien de la clé.</string>
<string name="cert_link_password">Mot de passe</string>
<string name="cert_link_password_note">Ce fichier a un mot de passe.</string>
<string name="cert_link_download">Télécharger</string>
<string name="cert_link_again">Télécharger à nouveau</string>
<string name="cert_link_done">Téléchargé et converti. Il sera utilisé à la prochaine connexion.</string>
<string name="cert_link_failed">Impossible de l’utiliser. %1$s</string>
</resources>
@@ -962,4 +962,17 @@
<string name="car_apps_as_messaging_note">Le sue notifiche, lette ad alta voce, con risposte.</string>
<string name="car_apps_added_by_you">Aggiunta da te</string>
<string name="car_apps_remove">Rimuovi %1$s</string>
<string name="cert_link">Scarica da un link</string>
<string name="cert_link_sub">Un file di certificato di cui ti fidi, a un indirizzo che incolli</string>
<string name="cert_link_title">Certificato da un link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Link del certificato o del .p12</string>
<string name="cert_link_key">Link della chiave privata</string>
<string name="cert_link_key_note">È un certificato senza la sua chiave. Incolla anche il link della chiave.</string>
<string name="cert_link_password">Password</string>
<string name="cert_link_password_note">Questo file ha una password.</string>
<string name="cert_link_download">Scarica</string>
<string name="cert_link_again">Scarica di nuovo</string>
<string name="cert_link_done">Scaricato e convertito. Verrà usato dalla prossima connessione.</string>
<string name="cert_link_failed">Impossibile usarlo. %1$s</string>
</resources>
@@ -942,4 +942,17 @@
<string name="car_apps_as_messaging_note">通知を読み上げ、返信できます。</string>
<string name="car_apps_added_by_you">自分で追加</string>
<string name="car_apps_remove">%1$s を削除</string>
<string name="cert_link">リンクからダウンロード</string>
<string name="cert_link_sub">信頼できる証明書ファイルを、貼り付けたアドレスから</string>
<string name="cert_link_title">リンクから証明書</string>
<string name="cert_link_note">.p12 ファイル、または PEM か DER の証明書と鍵のアドレスを貼り付けてください。Gearslip が車に必要な形式に変換します。</string>
<string name="cert_link_label">証明書または .p12 のリンク</string>
<string name="cert_link_key">秘密鍵のリンク</string>
<string name="cert_link_key_note">鍵のない証明書です。鍵のリンクも貼り付けてください。</string>
<string name="cert_link_password">パスワード</string>
<string name="cert_link_password_note">このファイルにはパスワードがあります。</string>
<string name="cert_link_download">ダウンロード</string>
<string name="cert_link_again">もう一度ダウンロード</string>
<string name="cert_link_done">ダウンロードして変換しました。次の接続から使われます。</string>
<string name="cert_link_failed">使用できませんでした。%1$s</string>
</resources>
@@ -942,4 +942,17 @@
<string name="car_apps_as_messaging_note">알림을 소리 내어 읽고 답장할 수 있습니다.</string>
<string name="car_apps_added_by_you">직접 추가함</string>
<string name="car_apps_remove">%1$s 삭제</string>
<string name="cert_link">링크에서 다운로드</string>
<string name="cert_link_sub">신뢰하는 인증서 파일을 붙여넣은 주소에서</string>
<string name="cert_link_title">링크로 인증서 가져오기</string>
<string name="cert_link_note">.p12 파일 또는 PEM이나 DER 형식의 인증서와 키 주소를 붙여넣으세요. Gearslip이 차량에 필요한 형식으로 변환합니다.</string>
<string name="cert_link_label">인증서 또는 .p12 링크</string>
<string name="cert_link_key">개인 키 링크</string>
<string name="cert_link_key_note">키가 없는 인증서입니다. 키 링크도 붙여넣으세요.</string>
<string name="cert_link_password">비밀번호</string>
<string name="cert_link_password_note">이 파일에는 비밀번호가 있습니다.</string>
<string name="cert_link_download">다운로드</string>
<string name="cert_link_again">다시 다운로드</string>
<string name="cert_link_done">다운로드하고 변환했습니다. 다음 연결부터 사용됩니다.</string>
<string name="cert_link_failed">사용할 수 없습니다. %1$s</string>
</resources>
@@ -952,4 +952,17 @@
<string name="car_apps_as_messaging_note">De meldingen, voorgelezen, met antwoorden.</string>
<string name="car_apps_added_by_you">Door jou toegevoegd</string>
<string name="car_apps_remove">%1$s verwijderen</string>
<string name="cert_link">Downloaden via een link</string>
<string name="cert_link_sub">Een certificaatbestand dat je vertrouwt, op een adres dat je plakt</string>
<string name="cert_link_title">Certificaat via een link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Link naar certificaat of .p12</string>
<string name="cert_link_key">Link naar de privésleutel</string>
<string name="cert_link_key_note">Dat is een certificaat zonder sleutel. Plak ook de link naar de sleutel.</string>
<string name="cert_link_password">Wachtwoord</string>
<string name="cert_link_password_note">Dit bestand heeft een wachtwoord.</string>
<string name="cert_link_download">Downloaden</string>
<string name="cert_link_again">Opnieuw downloaden</string>
<string name="cert_link_done">Gedownload en omgezet. Het wordt gebruikt vanaf de volgende verbinding.</string>
<string name="cert_link_failed">Kon het niet gebruiken. %1$s</string>
</resources>
@@ -972,4 +972,17 @@
<string name="car_apps_as_messaging_note">Jej powiadomienia, czytane na głos, z odpowiedziami.</string>
<string name="car_apps_added_by_you">Dodana przez Ciebie</string>
<string name="car_apps_remove">Usuń %1$s</string>
<string name="cert_link">Pobierz z linku</string>
<string name="cert_link_sub">Plik certyfikatu, któremu ufasz, spod wklejonego adresu</string>
<string name="cert_link_title">Certyfikat z linku</string>
<string name="cert_link_note">Wklej adres pliku .p12 albo certyfikatu i klucza w PEM lub DER. Gearslip przekształci go do postaci, której potrzebuje samochód.</string>
<string name="cert_link_label">Link do certyfikatu lub pliku .p12</string>
<string name="cert_link_key">Link do klucza prywatnego</string>
<string name="cert_link_key_note">To certyfikat bez klucza. Wklej też link do klucza.</string>
<string name="cert_link_password">Hasło</string>
<string name="cert_link_password_note">Ten plik ma hasło.</string>
<string name="cert_link_download">Pobierz</string>
<string name="cert_link_again">Pobierz ponownie</string>
<string name="cert_link_done">Pobrano i przekształcono. Zostanie użyty od następnego połączenia.</string>
<string name="cert_link_failed">Nie można go użyć. %1$s</string>
</resources>
@@ -962,4 +962,17 @@
<string name="car_apps_as_messaging_note">As notificações dele, lidas em voz alta, com respostas.</string>
<string name="car_apps_added_by_you">Adicionado por você</string>
<string name="car_apps_remove">Remover %1$s</string>
<string name="cert_link">Baixar de um link</string>
<string name="cert_link_sub">Um arquivo de certificado confiável, em um endereço que você cola</string>
<string name="cert_link_title">Certificado de um link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Link do certificado ou do .p12</string>
<string name="cert_link_key">Link da chave privada</string>
<string name="cert_link_key_note">É um certificado sem a chave. Cole também o link da chave.</string>
<string name="cert_link_password">Senha</string>
<string name="cert_link_password_note">Este arquivo tem senha.</string>
<string name="cert_link_download">Baixar</string>
<string name="cert_link_again">Baixar de novo</string>
<string name="cert_link_done">Baixado e convertido. Será usado a partir da próxima conexão.</string>
<string name="cert_link_failed">Não foi possível usar. %1$s</string>
</resources>
@@ -844,4 +844,17 @@
<string name="car_apps_as_messaging_note">Notificările ei, citite cu voce tare, cu răspunsuri.</string>
<string name="car_apps_added_by_you">Adăugată de tine</string>
<string name="car_apps_remove">Elimină %1$s</string>
<string name="cert_link">Descarcă dintr-un link</string>
<string name="cert_link_sub">Un fișier de certificat de încredere, de la o adresă pe care o lipești</string>
<string name="cert_link_title">Certificat dintr-un link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Linkul certificatului sau al .p12</string>
<string name="cert_link_key">Linkul cheii private</string>
<string name="cert_link_key_note">Este un certificat fără cheie. Lipește și linkul cheii.</string>
<string name="cert_link_password">Parolă</string>
<string name="cert_link_password_note">Fișierul are o parolă.</string>
<string name="cert_link_download">Descarcă</string>
<string name="cert_link_again">Descarcă din nou</string>
<string name="cert_link_done">Descărcat și convertit. Se folosește de la următoarea conectare.</string>
<string name="cert_link_failed">Nu a putut fi folosit. %1$s</string>
</resources>
@@ -972,4 +972,17 @@
<string name="car_apps_as_messaging_note">Его уведомления, прочитанные вслух, с ответами.</string>
<string name="car_apps_added_by_you">Добавлено вами</string>
<string name="car_apps_remove">Удалить %1$s</string>
<string name="cert_link">Скачать по ссылке</string>
<string name="cert_link_sub">Файл сертификата, которому вы доверяете, по адресу, который вы вставите</string>
<string name="cert_link_title">Сертификат по ссылке</string>
<string name="cert_link_note">Вставьте адрес файла .p12 или сертификата с ключом в формате PEM или DER. Gearslip преобразует его в нужный машине вид.</string>
<string name="cert_link_label">Ссылка на сертификат или .p12</string>
<string name="cert_link_key">Ссылка на закрытый ключ</string>
<string name="cert_link_key_note">Это сертификат без ключа. Вставьте также ссылку на ключ.</string>
<string name="cert_link_password">Пароль</string>
<string name="cert_link_password_note">У этого файла есть пароль.</string>
<string name="cert_link_download">Скачать</string>
<string name="cert_link_again">Скачать снова</string>
<string name="cert_link_done">Скачано и преобразовано. Будет использоваться со следующего подключения.</string>
<string name="cert_link_failed">Не удалось использовать. %1$s</string>
</resources>
@@ -952,4 +952,17 @@
<string name="car_apps_as_messaging_note">Dess aviseringar, upplästa, med svar.</string>
<string name="car_apps_added_by_you">Tillagd av dig</string>
<string name="car_apps_remove">Ta bort %1$s</string>
<string name="cert_link">Ladda ner från en länk</string>
<string name="cert_link_sub">En certifikatfil du litar på, från en adress du klistrar in</string>
<string name="cert_link_title">Certifikat från en länk</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Länk till certifikat eller .p12</string>
<string name="cert_link_key">Länk till den privata nyckeln</string>
<string name="cert_link_key_note">Det är ett certifikat utan nyckel. Klistra in nyckelns länk också.</string>
<string name="cert_link_password">Lösenord</string>
<string name="cert_link_password_note">Filen har ett lösenord.</string>
<string name="cert_link_download">Ladda ner</string>
<string name="cert_link_again">Ladda ner igen</string>
<string name="cert_link_done">Nedladdat och omgjort. Det används från nästa anslutning.</string>
<string name="cert_link_failed">Det gick inte att använda. %1$s</string>
</resources>
@@ -952,4 +952,17 @@
<string name="car_apps_as_messaging_note">Bildirimleri, sesli okunur ve yanıtlanabilir.</string>
<string name="car_apps_added_by_you">Sizin eklediğiniz</string>
<string name="car_apps_remove">%1$s kaldır</string>
<string name="cert_link">Bir bağlantıdan indir</string>
<string name="cert_link_sub">Güvendiğiniz bir sertifika dosyası, yapıştırdığınız bir adreste</string>
<string name="cert_link_title">Bağlantıdan sertifika</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Sertifika veya .p12 bağlantısı</string>
<string name="cert_link_key">Özel anahtarın bağlantısı</string>
<string name="cert_link_key_note">Bu, anahtarı olmayan bir sertifika. Anahtarın bağlantısını da yapıştırın.</string>
<string name="cert_link_password">Parola</string>
<string name="cert_link_password_note">Bu dosyanın parolası var.</string>
<string name="cert_link_download">İndir</string>
<string name="cert_link_again">Yeniden indir</string>
<string name="cert_link_done">İndirildi ve dönüştürüldü. Bir sonraki bağlantıdan itibaren kullanılır.</string>
<string name="cert_link_failed">Kullanılamadı. %1$s</string>
</resources>
@@ -972,4 +972,17 @@
<string name="car_apps_as_messaging_note">Його сповіщення, прочитані вголос, із відповідями.</string>
<string name="car_apps_added_by_you">Додано вами</string>
<string name="car_apps_remove">Видалити %1$s</string>
<string name="cert_link">Завантажити за посиланням</string>
<string name="cert_link_sub">Файл сертифіката, якому ви довіряєте, за адресою, яку ви вставите</string>
<string name="cert_link_title">Сертифікат за посиланням</string>
<string name="cert_link_note">Вставте адресу файлу .p12 або сертифіката з ключем у форматі PEM чи DER. Gearslip перетворить його на потрібний авто вигляд.</string>
<string name="cert_link_label">Посилання на сертифікат або .p12</string>
<string name="cert_link_key">Посилання на закритий ключ</string>
<string name="cert_link_key_note">Це сертифікат без ключа. Вставте також посилання на ключ.</string>
<string name="cert_link_password">Пароль</string>
<string name="cert_link_password_note">Цей файл має пароль.</string>
<string name="cert_link_download">Завантажити</string>
<string name="cert_link_again">Завантажити знову</string>
<string name="cert_link_done">Завантажено й перетворено. Використовуватиметься з наступного підключення.</string>
<string name="cert_link_failed">Не вдалося використати. %1$s</string>
</resources>
@@ -942,4 +942,17 @@
<string name="car_apps_as_messaging_note">朗读其通知,并可回复。</string>
<string name="car_apps_added_by_you">由你添加</string>
<string name="car_apps_remove">移除 %1$s</string>
<string name="cert_link">从链接下载</string>
<string name="cert_link_sub">从你粘贴的地址下载你信任的证书文件</string>
<string name="cert_link_title">从链接获取证书</string>
<string name="cert_link_note">粘贴 .p12 文件的地址,或 PEM、DER 格式的证书和密钥的地址。Gearslip 会把它转换成汽车需要的格式。</string>
<string name="cert_link_label">证书或 .p12 链接</string>
<string name="cert_link_key">私钥链接</string>
<string name="cert_link_key_note">这是没有密钥的证书。请同时粘贴密钥的链接。</string>
<string name="cert_link_password">密码</string>
<string name="cert_link_password_note">此文件有密码。</string>
<string name="cert_link_download">下载</string>
<string name="cert_link_again">重新下载</string>
<string name="cert_link_done">已下载并转换。将在下次连接时使用。</string>
<string name="cert_link_failed">无法使用。%1$s</string>
</resources>
+13
View File
@@ -1009,4 +1009,17 @@
<string name="car_apps_as_messaging_note">Its notifications, read aloud, with replies.</string>
<string name="car_apps_added_by_you">Added by you</string>
<string name="car_apps_remove">Remove %1$s</string>
<string name="cert_link">Download from a link</string>
<string name="cert_link_sub">A certificate file you trust, at an address you paste</string>
<string name="cert_link_title">Certificate from a link</string>
<string name="cert_link_note">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.</string>
<string name="cert_link_label">Certificate or .p12 link</string>
<string name="cert_link_key">Link to the private key</string>
<string name="cert_link_key_note">That\'s a certificate without its key. Paste the key\'s link too.</string>
<string name="cert_link_password">Password</string>
<string name="cert_link_password_note">This file has a password.</string>
<string name="cert_link_download">Download</string>
<string name="cert_link_again">Download again</string>
<string name="cert_link_done">Downloaded and converted. It\'s used from the next connection.</string>
<string name="cert_link_failed">Couldn\'t use it. %1$s</string>
</resources>
@@ -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) {
}
}
}