mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix(lotw): support more PBES2 variants, classify import errors for users
- Pkcs12Reader: AES-192/AES-128/DES-EDE3-CBC ciphers, PBKDF2 SHA1/SHA256/SHA384/SHA512 PRFs, keyLength defaulting per cipher, error messages reduced to algorithm names - LoTWKeyMaterial: map BadPadding -> wrong password, IllegalState (unsupported algorithm) -> format error with algorithm name, IllegalArgumentException -> invalid file - Import dialog: user-facing messages with algorithm name embedded, no more raw OpenSSL/BoringSSL error strings - Tests: AES-192, DES-EDE3 fixtures, wrong-password BadPadding check
This commit is contained in:
1 parent
474762cbd3
commit
52b733a450
12 files changed
+152
-22
No files matched your search
@@ -29,8 +29,20 @@ internal data class LoTWKeyMaterial(val key: PrivateKey, val certificate: X509Ce
|
|||||||
try {
|
try {
|
||||||
val parsed = Pkcs12Reader.read(bytes, password)
|
val parsed = Pkcs12Reader.read(bytes, password)
|
||||||
parsed.first to parsed.second
|
parsed.first to parsed.second
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
fail(if (password.isNotEmpty()) LoTWProblem.CERTIFICATE_FORMAT else LoTWProblem.CERTIFICATE_PASSWORD)
|
// Classify by exception type so the user sees the right message:
|
||||||
|
// - BadPadding (BAD_DECRYPT) -> wrong password
|
||||||
|
// - IllegalStateException (error()) -> unsupported algorithm,
|
||||||
|
// message is the algorithm name
|
||||||
|
// - IllegalArgumentException (require()) -> structurally invalid file
|
||||||
|
fail(
|
||||||
|
when {
|
||||||
|
e is javax.crypto.BadPaddingException -> LoTWProblem.CERTIFICATE_PASSWORD
|
||||||
|
e is IllegalStateException && password.isNotEmpty() -> LoTWProblem.CERTIFICATE_FORMAT
|
||||||
|
else -> LoTWProblem.CERTIFICATE_INVALID
|
||||||
|
},
|
||||||
|
if (e is IllegalStateException) (e.message ?: "") else ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fail(LoTWProblem.CERTIFICATE_PASSWORD)
|
fail(LoTWProblem.CERTIFICATE_PASSWORD)
|
||||||
|
|||||||
@@ -51,9 +51,15 @@ internal object Pkcs12Reader {
|
|||||||
private val OID_CERT_BAG = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x03)
|
private val OID_CERT_BAG = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x03)
|
||||||
private val OID_X509_CERT = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x09, 0x16, 0x01)
|
private val OID_X509_CERT = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x09, 0x16, 0x01)
|
||||||
private val OID_PBES2 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x05, 0x0d)
|
private val OID_PBES2 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x05, 0x0d)
|
||||||
|
private val OID_PBKDF2 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x01, 0x05, 0x0c)
|
||||||
private val OID_HMAC_SHA1 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x07)
|
private val OID_HMAC_SHA1 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x07)
|
||||||
|
private val OID_HMAC_SHA256 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x09)
|
||||||
|
private val OID_HMAC_SHA384 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x0a)
|
||||||
|
private val OID_HMAC_SHA512 = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x02, 0x0b)
|
||||||
private val OID_AES_256_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a)
|
private val OID_AES_256_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a)
|
||||||
|
private val OID_AES_192_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x16)
|
||||||
private val OID_AES_128_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x02)
|
private val OID_AES_128_CBC = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x02)
|
||||||
|
private val OID_DES_EDE3_CBC = byteArrayOf(0x2a, 0x86.toByte(), 0x48, 0x86.toByte(), 0xf7.toByte(), 0x0d, 0x03, 0x07)
|
||||||
|
|
||||||
fun read(bytes: ByteArray, password: CharArray): Pair<PrivateKey, X509Certificate> {
|
fun read(bytes: ByteArray, password: CharArray): Pair<PrivateKey, X509Certificate> {
|
||||||
val pfx = DerReader.read(bytes)
|
val pfx = DerReader.read(bytes)
|
||||||
@@ -123,6 +129,11 @@ internal object Pkcs12Reader {
|
|||||||
// PBES2-params ::= SEQUENCE { kdf AlgorithmIdentifier, enc AlgorithmIdentifier }
|
// PBES2-params ::= SEQUENCE { kdf AlgorithmIdentifier, enc AlgorithmIdentifier }
|
||||||
val pbes2 = DerReader.children(algParts[1].content)
|
val pbes2 = DerReader.children(algParts[1].content)
|
||||||
val kdf = DerReader.children(pbes2[0].content)
|
val kdf = DerReader.children(pbes2[0].content)
|
||||||
|
// KDF must be PBKDF2 (OpenSSL 3 also supports scrypt — not available on the
|
||||||
|
// platform JCE, so report it by name instead of a generic failure).
|
||||||
|
if (kdf.isEmpty() || !kdf[0].content.contentEquals(OID_PBKDF2)) {
|
||||||
|
error(oidName(kdf.firstOrNull()?.content))
|
||||||
|
}
|
||||||
val kdfParams = DerReader.children(kdf[1].content)
|
val kdfParams = DerReader.children(kdf[1].content)
|
||||||
val salt = kdfParams[0].content
|
val salt = kdfParams[0].content
|
||||||
val iterations = readInt(kdfParams[1].content)
|
val iterations = readInt(kdfParams[1].content)
|
||||||
@@ -130,7 +141,7 @@ internal object Pkcs12Reader {
|
|||||||
// Optional PBKDF2-params elements: keyLength (INTEGER) and/or prf (SEQUENCE),
|
// Optional PBKDF2-params elements: keyLength (INTEGER) and/or prf (SEQUENCE),
|
||||||
// in either order. Distinguish by DER tag — mistaking prf for keyLength yields
|
// in either order. Distinguish by DER tag — mistaking prf for keyLength yields
|
||||||
// an absurd key size and a PBKDF2 that runs for hours.
|
// an absurd key size and a PBKDF2 that runs for hours.
|
||||||
var keyBits = 256
|
var keyBits = -1 // -1 = not written; defaulted below from the cipher
|
||||||
var prfName = "PBKDF2WithHmacSHA1"
|
var prfName = "PBKDF2WithHmacSHA1"
|
||||||
for (i in 2 until kdfParams.size) {
|
for (i in 2 until kdfParams.size) {
|
||||||
val param = kdfParams[i]
|
val param = kdfParams[i]
|
||||||
@@ -138,26 +149,61 @@ internal object Pkcs12Reader {
|
|||||||
0x02 -> keyBits = readInt(param.content) * 8
|
0x02 -> keyBits = readInt(param.content) * 8
|
||||||
0x30 -> {
|
0x30 -> {
|
||||||
val prf = DerReader.children(param.content)
|
val prf = DerReader.children(param.content)
|
||||||
prfName = if (prf.isNotEmpty() && prf[0].content.contentEquals(OID_HMAC_SHA1))
|
prfName = when {
|
||||||
"PBKDF2WithHmacSHA1" else "PBKDF2WithHmacSHA256"
|
prf.isEmpty() || prf[0].content.contentEquals(OID_HMAC_SHA1) -> "PBKDF2WithHmacSHA1"
|
||||||
|
prf[0].content.contentEquals(OID_HMAC_SHA256) -> "PBKDF2WithHmacSHA256"
|
||||||
|
prf[0].content.contentEquals(OID_HMAC_SHA384) -> "PBKDF2WithHmacSHA384"
|
||||||
|
prf[0].content.contentEquals(OID_HMAC_SHA512) -> "PBKDF2WithHmacSHA512"
|
||||||
|
else -> error(oidName(prf[0].content))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require(keyBits in 128..512) { "implausible PBKDF2 key size" }
|
|
||||||
val enc = DerReader.children(pbes2[1].content)
|
val enc = DerReader.children(pbes2[1].content)
|
||||||
val encOid = enc[0].content
|
val encOid = enc[0].content
|
||||||
val iv = enc[1].content
|
val iv = enc[1].content
|
||||||
require(encOid.contentEquals(OID_AES_256_CBC) || encOid.contentEquals(OID_AES_128_CBC)) { "unsupported cipher" }
|
val (cipherName, aesKeyBits) = when {
|
||||||
|
encOid.contentEquals(OID_AES_256_CBC) -> "AES/CBC/PKCS5Padding" to 256
|
||||||
|
encOid.contentEquals(OID_AES_192_CBC) -> "AES/CBC/PKCS5Padding" to 192
|
||||||
|
encOid.contentEquals(OID_AES_128_CBC) -> "AES/CBC/PKCS5Padding" to 128
|
||||||
|
encOid.contentEquals(OID_DES_EDE3_CBC) -> "DESede/CBC/PKCS5Padding" to 192
|
||||||
|
else -> error(oidName(encOid))
|
||||||
|
}
|
||||||
|
val finalKeyBits = if (keyBits > 0) keyBits else aesKeyBits
|
||||||
|
require(finalKeyBits in 128..512) { "implausible PBKDF2 key size" }
|
||||||
|
|
||||||
val spec = PBEKeySpec(password, salt, iterations, keyBits)
|
val spec = PBEKeySpec(password, salt, iterations, finalKeyBits)
|
||||||
val secretKey = SecretKeyFactory.getInstance(prfName).generateSecret(spec)
|
val secretKey = try {
|
||||||
// PBKDF2 factories return a PBE key; wrap the raw bytes as an AES key.
|
SecretKeyFactory.getInstance(prfName).generateSecret(spec)
|
||||||
val aesKey = SecretKeySpec(secretKey.encoded, "AES")
|
} catch (e: java.security.NoSuchAlgorithmException) {
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
// e.g. PBKDF2WithHmacSHA512 needs API 26+; surface the real reason.
|
||||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
error(prfName.replace("PBKDF2WithHmac", "PBKDF2-HMAC-") + " (needs Android 8.0+)")
|
||||||
|
}
|
||||||
|
val cipher = Cipher.getInstance(cipherName)
|
||||||
|
cipher.init(
|
||||||
|
Cipher.DECRYPT_MODE,
|
||||||
|
if (cipherName == "AES/CBC/PKCS5Padding") SecretKeySpec(secretKey.encoded, "AES")
|
||||||
|
else SecretKeySpec(secretKey.encoded, "DESede"),
|
||||||
|
IvParameterSpec(iv)
|
||||||
|
)
|
||||||
return cipher.doFinal(encrypted)
|
return cipher.doFinal(encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Human-readable name for a known algorithm OID. */
|
||||||
|
private fun oidName(oid: ByteArray?): String = when {
|
||||||
|
oid == null -> "unknown algorithm"
|
||||||
|
oid.contentEquals(OID_PBKDF2) -> "PBKDF2"
|
||||||
|
oid.contentEquals(OID_HMAC_SHA1) -> "PBKDF2-HMAC-SHA1"
|
||||||
|
oid.contentEquals(OID_HMAC_SHA256) -> "PBKDF2-HMAC-SHA256"
|
||||||
|
oid.contentEquals(OID_HMAC_SHA384) -> "PBKDF2-HMAC-SHA384"
|
||||||
|
oid.contentEquals(OID_HMAC_SHA512) -> "PBKDF2-HMAC-SHA512"
|
||||||
|
oid.contentEquals(OID_AES_256_CBC) -> "AES-256-CBC"
|
||||||
|
oid.contentEquals(OID_AES_192_CBC) -> "AES-192-CBC"
|
||||||
|
oid.contentEquals(OID_AES_128_CBC) -> "AES-128-CBC"
|
||||||
|
oid.contentEquals(OID_DES_EDE3_CBC) -> "DES-EDE3-CBC"
|
||||||
|
else -> "unknown algorithm"
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseSafeBags(
|
private fun parseSafeBags(
|
||||||
safeContentsDer: ByteArray,
|
safeContentsDer: ByteArray,
|
||||||
password: CharArray,
|
password: CharArray,
|
||||||
|
|||||||
@@ -36,6 +36,67 @@ class Pkcs12ReaderTest {
|
|||||||
private fun fixture(): ByteArray =
|
private fun fixture(): ByteArray =
|
||||||
javaClass.classLoader!!.getResourceAsStream("test_pbes2.p12")!!.use { it.readBytes() }
|
javaClass.classLoader!!.getResourceAsStream("test_pbes2.p12")!!.use { it.readBytes() }
|
||||||
|
|
||||||
|
private fun fixture(name: String): ByteArray =
|
||||||
|
javaClass.classLoader!!.getResourceAsStream(name)!!.use { it.readBytes() }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun readsAes192Variant() {
|
||||||
|
val (key, cert) = Pkcs12Reader.read(fixture("test_pbes2_aes192.p12"), "testpass123".toCharArray())
|
||||||
|
assertEquals("RSA", key.algorithm)
|
||||||
|
assertEquals("RSA", cert.publicKey.algorithm)
|
||||||
|
assertTrue("key/cert pair", keyMatches(key, cert))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun readsDesEde3CbcVariant() {
|
||||||
|
val (key, cert) = Pkcs12Reader.read(fixture("test_pbes2_desede3.p12"), "testpass123".toCharArray())
|
||||||
|
assertEquals("RSA", key.algorithm)
|
||||||
|
assertEquals("RSA", cert.publicKey.algorithm)
|
||||||
|
assertTrue("key/cert pair", keyMatches(key, cert))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unsupportedCipherNamesTheAlgorithm() {
|
||||||
|
// A valid PBES2 file re-encrypted with an unsupported cipher must report the
|
||||||
|
// algorithm name (so the user can see exactly what to re-export with).
|
||||||
|
val fixtureBytes = fixture("test_pbes2.p12")
|
||||||
|
val e = assertThrows(IllegalStateException::class.java) {
|
||||||
|
// Simulate: patch the AES-256-CBC OID inside the file to an unknown OID.
|
||||||
|
val bogus = ByteArray(fixtureBytes.size) { fixtureBytes[it] }
|
||||||
|
// find AES-256-CBC OID bytes 0x60 86 48 01 65 03 04 01 2a
|
||||||
|
val oid = byteArrayOf(0x60, 0x86.toByte(), 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a)
|
||||||
|
var idx = -1
|
||||||
|
outer@ for (i in 0..bogus.size - oid.size) {
|
||||||
|
for (j in oid.indices) if (bogus[i + j] != oid[j]) continue@outer
|
||||||
|
idx = i; break
|
||||||
|
}
|
||||||
|
require(idx >= 0) { "AES-256-CBC OID not found in fixture" }
|
||||||
|
bogus[idx] = 0x7f.toByte() // corrupt the OID tag byte → unknown cipher
|
||||||
|
Pkcs12Reader.read(bogus, "testpass123".toCharArray())
|
||||||
|
}
|
||||||
|
assertTrue("mentions algorithm", e.message.orEmpty().contains("algorithm"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyMatches(key: java.security.PrivateKey, cert: java.security.cert.X509Certificate): Boolean {
|
||||||
|
val challenge = "Look4Sat LoTW certificate key check".toByteArray(Charsets.US_ASCII)
|
||||||
|
val signed = Signature.getInstance("SHA1withRSA").run {
|
||||||
|
initSign(key); update(challenge); sign()
|
||||||
|
}
|
||||||
|
return Signature.getInstance("SHA1withRSA").run {
|
||||||
|
initVerify(cert); update(challenge); verify(signed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongPasswordFailsWithBadPadding() {
|
||||||
|
// A wrong password must surface as a decryption failure (BadPadding), which
|
||||||
|
// LoTWKeyMaterial maps to CERTIFICATE_PASSWORD — not a format error.
|
||||||
|
val e = assertThrows(Exception::class.java) {
|
||||||
|
Pkcs12Reader.read(fixture(), "definitely-wrong-password".toCharArray())
|
||||||
|
}
|
||||||
|
assertTrue("BadPadding", e is javax.crypto.BadPaddingException)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun readsPbes2KeyAndCertificate() {
|
fun readsPbes2KeyAndCertificate() {
|
||||||
val (key, cert) = Pkcs12Reader.read(fixture(), "testpass123".toCharArray())
|
val (key, cert) = Pkcs12Reader.read(fixture(), "testpass123".toCharArray())
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -210,7 +210,8 @@
|
|||||||
<string name="prefs_lotw_upload_error_password">证书密码错误,请检查 TQSL 导出 .p12 时设置的密码</string>
|
<string name="prefs_lotw_upload_error_password">证书密码错误,请检查 TQSL 导出 .p12 时设置的密码</string>
|
||||||
<string name="prefs_lotw_upload_error_expired">证书已过期或尚未生效</string>
|
<string name="prefs_lotw_upload_error_expired">证书已过期或尚未生效</string>
|
||||||
<string name="prefs_lotw_upload_error_invalid">不是有效的 LoTW 证书文件</string>
|
<string name="prefs_lotw_upload_error_invalid">不是有效的 LoTW 证书文件</string>
|
||||||
<string name="prefs_lotw_upload_error_format">该 .p12 是新版 PBES2/AES-256 格式(OpenSSL 3 / 新版 TQSL 导出),Android 无法直接读取。请用旧格式重新导出(TQSL 旧加密导出或 OpenSSL -legacy 转换)后再导入</string>
|
<string name="prefs_lotw_upload_error_format">不支持的 .p12 加密,请用 TQSL 默认加密重新导出后再试</string>
|
||||||
|
<string name="prefs_lotw_upload_error_format_alg">不支持的 .p12 加密(%1$s),请用 TQSL 默认加密重新导出后再试</string>
|
||||||
<string name="prefs_lotw_upload_error_unknown">导入失败,请重试</string>
|
<string name="prefs_lotw_upload_error_unknown">导入失败,请重试</string>
|
||||||
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · 有效期至 %3$s</string>
|
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · 有效期至 %3$s</string>
|
||||||
<string name="prefs_lotw_upload_remove">移除证书</string>
|
<string name="prefs_lotw_upload_remove">移除证书</string>
|
||||||
|
|||||||
@@ -241,7 +241,8 @@
|
|||||||
<string name="prefs_lotw_upload_error_password">Incorrect certificate password. Check the password you set when exporting the .p12 from TQSL.</string>
|
<string name="prefs_lotw_upload_error_password">Incorrect certificate password. Check the password you set when exporting the .p12 from TQSL.</string>
|
||||||
<string name="prefs_lotw_upload_error_expired">Certificate is expired or not yet valid.</string>
|
<string name="prefs_lotw_upload_error_expired">Certificate is expired or not yet valid.</string>
|
||||||
<string name="prefs_lotw_upload_error_invalid">Not a valid LoTW certificate file.</string>
|
<string name="prefs_lotw_upload_error_invalid">Not a valid LoTW certificate file.</string>
|
||||||
<string name="prefs_lotw_upload_error_format">This .p12 uses the new PBES2/AES-256 format (OpenSSL 3 / recent TQSL), which Android cannot read directly. Re-export it as a legacy format (TQSL "export with legacy encryption" or an OpenSSL -legacy conversion), then import again.</string>
|
<string name="prefs_lotw_upload_error_format">Unsupported .p12 encryption. Re-export from TQSL and try again.</string>
|
||||||
|
<string name="prefs_lotw_upload_error_format_alg">Unsupported .p12 encryption (%1$s). Re-export from TQSL and try again.</string>
|
||||||
<string name="prefs_lotw_upload_error_unknown">Import failed. Try again.</string>
|
<string name="prefs_lotw_upload_error_unknown">Import failed. Try again.</string>
|
||||||
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · expires %3$s</string>
|
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · expires %3$s</string>
|
||||||
<string name="prefs_lotw_upload_remove">Remove certificate</string>
|
<string name="prefs_lotw_upload_remove">Remove certificate</string>
|
||||||
|
|||||||
+9
-3
@@ -92,6 +92,7 @@ fun LoTWUploadConfigDialog(
|
|||||||
stationMeta: LoTWStationMeta?,
|
stationMeta: LoTWStationMeta?,
|
||||||
busy: Boolean,
|
busy: Boolean,
|
||||||
error: LoTWUploadError?,
|
error: LoTWUploadError?,
|
||||||
|
errorDetail: String = "",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onImport: (ByteArray, CharArray) -> Unit,
|
onImport: (ByteArray, CharArray) -> Unit,
|
||||||
onRemove: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
@@ -162,15 +163,20 @@ fun LoTWUploadConfigDialog(
|
|||||||
)
|
)
|
||||||
error?.let {
|
error?.let {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = when {
|
||||||
|
it == LoTWUploadError.FORMAT && errorDetail.isNotBlank() ->
|
||||||
|
stringResource(R.string.prefs_lotw_upload_error_format_alg, errorDetail)
|
||||||
|
it == LoTWUploadError.FORMAT -> stringResource(R.string.prefs_lotw_upload_error_format)
|
||||||
|
else -> stringResource(
|
||||||
when (it) {
|
when (it) {
|
||||||
LoTWUploadError.PASSWORD -> R.string.prefs_lotw_upload_error_password
|
LoTWUploadError.PASSWORD -> R.string.prefs_lotw_upload_error_password
|
||||||
LoTWUploadError.EXPIRED -> R.string.prefs_lotw_upload_error_expired
|
LoTWUploadError.EXPIRED -> R.string.prefs_lotw_upload_error_expired
|
||||||
LoTWUploadError.INVALID_FILE -> R.string.prefs_lotw_upload_error_invalid
|
LoTWUploadError.INVALID_FILE -> R.string.prefs_lotw_upload_error_invalid
|
||||||
LoTWUploadError.FORMAT -> R.string.prefs_lotw_upload_error_format
|
|
||||||
LoTWUploadError.UNKNOWN -> R.string.prefs_lotw_upload_error_unknown
|
LoTWUploadError.UNKNOWN -> R.string.prefs_lotw_upload_error_unknown
|
||||||
|
LoTWUploadError.FORMAT -> R.string.prefs_lotw_upload_error_format
|
||||||
}
|
}
|
||||||
),
|
)
|
||||||
|
},
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
|
|||||||
+1
@@ -282,6 +282,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
|||||||
stationMeta = uiState.lotwStationMeta,
|
stationMeta = uiState.lotwStationMeta,
|
||||||
busy = uiState.lotwUploadBusy,
|
busy = uiState.lotwUploadBusy,
|
||||||
error = uiState.lotwUploadError,
|
error = uiState.lotwUploadError,
|
||||||
|
errorDetail = uiState.lotwUploadErrorDetail,
|
||||||
onDismiss = { dialogs.lotwUpload = false },
|
onDismiss = { dialogs.lotwUpload = false },
|
||||||
onImport = { bytes, password -> onAction(SettingsAction.ImportLoTWCertificate(bytes, password)) },
|
onImport = { bytes, password -> onAction(SettingsAction.ImportLoTWCertificate(bytes, password)) },
|
||||||
onRemove = { onAction(SettingsAction.RemoveLoTWCertificate) },
|
onRemove = { onAction(SettingsAction.RemoveLoTWCertificate) },
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ data class SettingsState(
|
|||||||
val lotwUploadBusy: Boolean = false,
|
val lotwUploadBusy: Boolean = false,
|
||||||
/** Last certificate import outcome; shown inside the upload config dialog. */
|
/** Last certificate import outcome; shown inside the upload config dialog. */
|
||||||
val lotwUploadError: LoTWUploadError? = null,
|
val lotwUploadError: LoTWUploadError? = null,
|
||||||
|
/** Parser detail for FORMAT errors (e.g. the unsupported algorithm name). */
|
||||||
|
val lotwUploadErrorDetail: String = "",
|
||||||
/** One-click logbook upload: prepared preview awaiting confirmation. */
|
/** One-click logbook upload: prepared preview awaiting confirmation. */
|
||||||
val logbookPreview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview? = null,
|
val logbookPreview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview? = null,
|
||||||
val logbookUploadBusy: Boolean = false,
|
val logbookUploadBusy: Boolean = false,
|
||||||
|
|||||||
+2
-2
@@ -423,9 +423,9 @@ class SettingsViewModel(
|
|||||||
com.rtbishop.look4sat.core.domain.repository.LoTWProblem.CERTIFICATE_FORMAT -> LoTWUploadError.FORMAT
|
com.rtbishop.look4sat.core.domain.repository.LoTWProblem.CERTIFICATE_FORMAT -> LoTWUploadError.FORMAT
|
||||||
else -> LoTWUploadError.INVALID_FILE
|
else -> LoTWUploadError.INVALID_FILE
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = error) }
|
_uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = error, lotwUploadErrorDetail = e.detail) }
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
_uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = LoTWUploadError.UNKNOWN) }
|
_uiState.update { it.copy(lotwUploadBusy = false, lotwUploadError = LoTWUploadError.UNKNOWN, lotwUploadErrorDetail = "") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionCode = "523"
|
appVersionCode = "526"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionName = "4.4.7-ba7opf.17"
|
appVersionName = "4.4.7-ba7opf.17.3"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
compileSdk = "37"
|
compileSdk = "37"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
|
|||||||
Reference in new issue
Block a user