Add FingerprintFormatter
This commit is contained in:
parent
5ebf7e50af
commit
75a9287463
11 changed files with 267 additions and 47 deletions
|
@ -20,6 +20,7 @@ data class Colors(
|
|||
val onPrimary: Color,
|
||||
val onSecondary: Color,
|
||||
val onBackground: Color,
|
||||
val onBackgroundSecondary: Color,
|
||||
val onSurface: Color,
|
||||
val onMessage: Color,
|
||||
val toolbar: Color,
|
||||
|
@ -41,6 +42,7 @@ internal fun lightColors(
|
|||
onPrimary: Color = Color.White,
|
||||
onSecondary: Color = Color.Black,
|
||||
onBackground: Color = Color.Black,
|
||||
onBackgroundSecondary: Color = MaterialColor.gray_700,
|
||||
onSurface: Color = Color.Black,
|
||||
onMessage: Color = Color.White,
|
||||
toolbar: Color = primary,
|
||||
|
@ -58,6 +60,7 @@ internal fun lightColors(
|
|||
onPrimary = onPrimary,
|
||||
onSecondary = onSecondary,
|
||||
onBackground = onBackground,
|
||||
onBackgroundSecondary = onBackgroundSecondary,
|
||||
onSurface = onSurface,
|
||||
onMessage = onMessage,
|
||||
toolbar = toolbar,
|
||||
|
@ -79,6 +82,7 @@ internal fun darkColors(
|
|||
onPrimary: Color = Color.Black,
|
||||
onSecondary: Color = Color.Black,
|
||||
onBackground: Color = Color.White,
|
||||
onBackgroundSecondary: Color = MaterialColor.gray_400,
|
||||
onSurface: Color = Color.White,
|
||||
onMessage: Color = Color.Black,
|
||||
toolbar: Color = surface,
|
||||
|
@ -96,6 +100,7 @@ internal fun darkColors(
|
|||
onPrimary = onPrimary,
|
||||
onSecondary = onSecondary,
|
||||
onBackground = onBackground,
|
||||
onBackgroundSecondary = onBackgroundSecondary,
|
||||
onSurface = onSurface,
|
||||
onMessage = onMessage,
|
||||
toolbar = toolbar,
|
||||
|
|
|
@ -13,6 +13,7 @@ dependencies {
|
|||
implementation(projects.feature.account.common)
|
||||
|
||||
implementation(projects.mail.common)
|
||||
implementation(libs.okio)
|
||||
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ import app.k9mail.feature.account.server.certificate.data.InMemoryServerCertific
|
|||
import app.k9mail.feature.account.server.certificate.domain.ServerCertificateDomainContract
|
||||
import app.k9mail.feature.account.server.certificate.domain.usecase.AddServerCertificateException
|
||||
import app.k9mail.feature.account.server.certificate.domain.usecase.FormatServerCertificateError
|
||||
import app.k9mail.feature.account.server.certificate.ui.DefaultFingerprintFormatter
|
||||
import app.k9mail.feature.account.server.certificate.ui.DefaultServerNameFormatter
|
||||
import app.k9mail.feature.account.server.certificate.ui.FingerprintFormatter
|
||||
import app.k9mail.feature.account.server.certificate.ui.ServerCertificateErrorViewModel
|
||||
import app.k9mail.feature.account.server.certificate.ui.ServerNameFormatter
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
|
@ -29,6 +31,8 @@ val featureAccountServerCertificateModule: Module = module {
|
|||
|
||||
factory<ServerNameFormatter> { DefaultServerNameFormatter() }
|
||||
|
||||
factory<FingerprintFormatter> { DefaultFingerprintFormatter() }
|
||||
|
||||
viewModel {
|
||||
ServerCertificateErrorViewModel(
|
||||
certificateErrorRepository = get(),
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package app.k9mail.feature.account.server.certificate.domain.entity
|
||||
|
||||
import okio.ByteString
|
||||
|
||||
data class ServerCertificateProperties(
|
||||
val subjectAlternativeNames: List<String>,
|
||||
val notValidBefore: String,
|
||||
val notValidAfter: String,
|
||||
val subject: String,
|
||||
val issuer: String,
|
||||
val fingerprintSha1: String,
|
||||
val fingerprintSha256: String,
|
||||
val fingerprintSha512: String,
|
||||
val fingerprintSha1: ByteString,
|
||||
val fingerprintSha256: ByteString,
|
||||
val fingerprintSha512: ByteString,
|
||||
)
|
||||
|
|
|
@ -4,15 +4,14 @@ import app.k9mail.feature.account.server.certificate.domain.ServerCertificateDom
|
|||
import app.k9mail.feature.account.server.certificate.domain.entity.FormattedServerCertificateError
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateError
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateProperties
|
||||
import com.fsck.k9.logging.Timber
|
||||
import com.fsck.k9.mail.filter.Hex
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlinx.datetime.Instant
|
||||
import okio.ByteString
|
||||
import okio.HashingSink
|
||||
import okio.blackholeSink
|
||||
import okio.buffer
|
||||
|
||||
class FormatServerCertificateError(
|
||||
private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT),
|
||||
|
@ -33,9 +32,9 @@ class FormatServerCertificateError(
|
|||
val subject = certificate.subjectX500Principal.toString()
|
||||
val issuer = certificate.issuerX500Principal.toString()
|
||||
|
||||
val fingerprintSha1 = getFingerprint(certificate, algorithm = "SHA-1")
|
||||
val fingerprintSha256 = getFingerprint(certificate, algorithm = "SHA-256")
|
||||
val fingerprintSha512 = getFingerprint(certificate, algorithm = "SHA-512")
|
||||
val fingerprintSha1 = computeFingerprint(certificate, HashAlgorithm.SHA_1)
|
||||
val fingerprintSha256 = computeFingerprint(certificate, HashAlgorithm.SHA_256)
|
||||
val fingerprintSha512 = computeFingerprint(certificate, HashAlgorithm.SHA_512)
|
||||
|
||||
return FormattedServerCertificateError(
|
||||
hostname = serverCertificateError.hostname,
|
||||
|
@ -52,28 +51,23 @@ class FormatServerCertificateError(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getFingerprint(certificate: X509Certificate, algorithm: String): String {
|
||||
val fingerprint = computeFingerprint(certificate, algorithm)
|
||||
return formatFingerprint(fingerprint)
|
||||
}
|
||||
|
||||
private fun computeFingerprint(certificate: X509Certificate, algorithm: String): ByteArray {
|
||||
val digest = try {
|
||||
MessageDigest.getInstance(algorithm)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.e(e, "Error while initializing MessageDigest (%s)", algorithm)
|
||||
return "??".toByteArray()
|
||||
private fun computeFingerprint(certificate: X509Certificate, algorithm: HashAlgorithm): ByteString {
|
||||
val sink = when (algorithm) {
|
||||
HashAlgorithm.SHA_1 -> HashingSink.sha1(blackholeSink())
|
||||
HashAlgorithm.SHA_256 -> HashingSink.sha256(blackholeSink())
|
||||
HashAlgorithm.SHA_512 -> HashingSink.sha512(blackholeSink())
|
||||
}
|
||||
|
||||
return try {
|
||||
digest.digest(certificate.encoded)
|
||||
} catch (e: CertificateEncodingException) {
|
||||
Timber.e(e, "Error while encoding certificate")
|
||||
"??".toByteArray()
|
||||
}
|
||||
}
|
||||
sink.buffer()
|
||||
.write(certificate.encoded)
|
||||
.flush()
|
||||
|
||||
private fun formatFingerprint(fingerprint: ByteArray): String {
|
||||
return Hex.encodeHex(fingerprint)
|
||||
return sink.hash
|
||||
}
|
||||
}
|
||||
|
||||
private enum class HashAlgorithm {
|
||||
SHA_1,
|
||||
SHA_256,
|
||||
SHA_512,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package app.k9mail.feature.account.server.certificate.ui
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import okio.ByteString
|
||||
|
||||
/**
|
||||
* Format a certificate fingerprint.
|
||||
*
|
||||
* Outputs bytes as hexadecimal number, separated by `:`. Includes zero width space (U+200B) after colons to decrease
|
||||
* the chance of long lines being displayed with a line break in the middle of a byte.
|
||||
*/
|
||||
internal fun interface FingerprintFormatter {
|
||||
fun format(fingerprint: ByteString, separatorColor: Color): AnnotatedString
|
||||
}
|
||||
|
||||
internal class DefaultFingerprintFormatter : FingerprintFormatter {
|
||||
override fun format(fingerprint: ByteString, separatorColor: Color): AnnotatedString {
|
||||
require(fingerprint.size > 0)
|
||||
|
||||
return buildAnnotatedString {
|
||||
appendByteAsHexNumber(fingerprint[0])
|
||||
|
||||
for (i in 1 until fingerprint.size) {
|
||||
appendSeparator(separatorColor)
|
||||
appendByteAsHexNumber(fingerprint[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun AnnotatedString.Builder.appendByteAsHexNumber(byte: Byte) {
|
||||
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
|
||||
append(byte.toHexString(format = HexFormat.UpperCase))
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.appendSeparator(separatorColor: Color) {
|
||||
withStyle(style = SpanStyle(color = separatorColor)) {
|
||||
append(":")
|
||||
}
|
||||
|
||||
// Zero width space so long lines will be broken here and not in the middle of a byte value
|
||||
append('\u200B')
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import app.k9mail.feature.account.server.certificate.R
|
|||
import app.k9mail.feature.account.server.certificate.domain.entity.FormattedServerCertificateError
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateProperties
|
||||
import app.k9mail.feature.account.server.certificate.ui.ServerCertificateErrorContract.State
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
|
@ -126,16 +127,19 @@ internal fun ServerCertificateErrorContentPreview() {
|
|||
notValidAfter = "December 31, 2023, 11:59 PM",
|
||||
subject = "CN=*.domain.example",
|
||||
issuer = "CN=test, O=MZLA",
|
||||
fingerprintSha1 = "33ab5639bfd8e7b95eb1d8d0b87781d4ffea4d5d",
|
||||
fingerprintSha256 = "1894a19c85ba153acbf743ac4e43fc004c891604b26f8c69e1e83ea2afc7c48f",
|
||||
fingerprintSha512 = "81381f1dacd4824a6c503fd07057763099c12b8309d0abcec4000c9060cbbfa67988b2ada669ab" +
|
||||
"4837fcd3d4ea6e2b8db2b9da9197d5112fb369fd006da545de",
|
||||
fingerprintSha1 = "33ab5639bfd8e7b95eb1d8d0b87781d4ffea4d5d".decodeHex(),
|
||||
fingerprintSha256 = "1894a19c85ba153acbf743ac4e43fc004c891604b26f8c69e1e83ea2afc7c48f".decodeHex(),
|
||||
fingerprintSha512 = (
|
||||
"81381f1dacd4824a6c503fd07057763099c12b8309d0abcec4000c9060cbbfa6" +
|
||||
"7988b2ada669ab4837fcd3d4ea6e2b8db2b9da9197d5112fb369fd006da545de"
|
||||
).decodeHex(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
koinPreview {
|
||||
factory<ServerNameFormatter> { DefaultServerNameFormatter() }
|
||||
factory<FingerprintFormatter> { DefaultFingerprintFormatter() }
|
||||
} WithContent {
|
||||
K9Theme {
|
||||
ServerCertificateErrorContent(
|
||||
|
|
|
@ -171,6 +171,7 @@ internal fun ServerCertificateErrorScreenK9Preview() {
|
|||
|
||||
koinPreview {
|
||||
factory<ServerNameFormatter> { DefaultServerNameFormatter() }
|
||||
factory<FingerprintFormatter> { DefaultFingerprintFormatter() }
|
||||
} WithContent {
|
||||
K9Theme {
|
||||
ServerCertificateErrorScreen(
|
||||
|
|
|
@ -18,6 +18,8 @@ import app.k9mail.core.ui.compose.theme.K9Theme
|
|||
import app.k9mail.core.ui.compose.theme.MainTheme
|
||||
import app.k9mail.feature.account.server.certificate.R
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateProperties
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
|
@ -25,6 +27,7 @@ internal fun ServerCertificateView(
|
|||
serverCertificateProperties: ServerCertificateProperties,
|
||||
modifier: Modifier = Modifier,
|
||||
serverNameFormatter: ServerNameFormatter = koinInject(),
|
||||
fingerprintFormatter: FingerprintFormatter = koinInject(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(
|
||||
|
@ -66,16 +69,26 @@ internal fun ServerCertificateView(
|
|||
TextOverline(text = stringResource(R.string.account_server_certificate_fingerprints_section))
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.default))
|
||||
|
||||
TextSubtitle2(text = "SHA-1")
|
||||
TextBody1(text = serverCertificateProperties.fingerprintSha1)
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
Fingerprint("SHA-1", serverCertificateProperties.fingerprintSha1, fingerprintFormatter)
|
||||
Fingerprint("SHA-256", serverCertificateProperties.fingerprintSha256, fingerprintFormatter)
|
||||
Fingerprint("SHA-512", serverCertificateProperties.fingerprintSha512, fingerprintFormatter)
|
||||
}
|
||||
}
|
||||
|
||||
TextSubtitle2(text = "SHA-256")
|
||||
TextBody1(text = serverCertificateProperties.fingerprintSha256)
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
@Composable
|
||||
private fun Fingerprint(
|
||||
title: String,
|
||||
fingerprint: ByteString,
|
||||
fingerprintFormatter: FingerprintFormatter,
|
||||
) {
|
||||
val formattedFingerprint = fingerprintFormatter.format(
|
||||
fingerprint,
|
||||
separatorColor = MainTheme.colors.onBackgroundSecondary,
|
||||
)
|
||||
|
||||
TextSubtitle2(text = "SHA-512")
|
||||
TextBody1(text = serverCertificateProperties.fingerprintSha512)
|
||||
Column {
|
||||
TextSubtitle2(text = title)
|
||||
TextBody1(text = formattedFingerprint)
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
}
|
||||
}
|
||||
|
@ -104,14 +117,17 @@ internal fun ServerCertificateViewPreview() {
|
|||
notValidAfter = "December 31, 2023, 11:59 PM",
|
||||
subject = "CN=*.domain.example",
|
||||
issuer = "CN=test, O=MZLA",
|
||||
fingerprintSha1 = "33ab5639bfd8e7b95eb1d8d0b87781d4ffea4d5d",
|
||||
fingerprintSha256 = "1894a19c85ba153acbf743ac4e43fc004c891604b26f8c69e1e83ea2afc7c48f",
|
||||
fingerprintSha512 = "81381f1dacd4824a6c503fd07057763099c12b8309d0abcec4000c9060cbbfa67988b2ada669ab4837fcd3d4" +
|
||||
"ea6e2b8db2b9da9197d5112fb369fd006da545de",
|
||||
fingerprintSha1 = "33ab5639bfd8e7b95eb1d8d0b87781d4ffea4d5d".decodeHex(),
|
||||
fingerprintSha256 = "1894a19c85ba153acbf743ac4e43fc004c891604b26f8c69e1e83ea2afc7c48f".decodeHex(),
|
||||
fingerprintSha512 = (
|
||||
"81381f1dacd4824a6c503fd07057763099c12b8309d0abcec4000c9060cbbfa6" +
|
||||
"7988b2ada669ab4837fcd3d4ea6e2b8db2b9da9197d5112fb369fd006da545de"
|
||||
).decodeHex(),
|
||||
)
|
||||
|
||||
koinPreview {
|
||||
factory<ServerNameFormatter> { DefaultServerNameFormatter() }
|
||||
factory<FingerprintFormatter> { DefaultFingerprintFormatter() }
|
||||
} WithContent {
|
||||
K9Theme {
|
||||
ServerCertificateView(
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package app.k9mail.feature.account.server.certificate.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateError
|
||||
import app.k9mail.feature.account.server.certificate.domain.entity.ServerCertificateProperties
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.DateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
|
||||
class FormatServerCertificateErrorTest {
|
||||
@BeforeTest
|
||||
fun setTimeZone() {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `format expired certificate`() {
|
||||
val formatCertificateError = FormatServerCertificateError(
|
||||
dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, Locale.ROOT),
|
||||
)
|
||||
val serverCertificateError = ServerCertificateError(
|
||||
hostname = "expired.badssl.com",
|
||||
port = 443,
|
||||
certificateChain = listOf(readCertificate(EXPIRED_CERTIFICATE)),
|
||||
)
|
||||
|
||||
val result = formatCertificateError(serverCertificateError)
|
||||
|
||||
assertThat(result.hostname).isEqualTo("expired.badssl.com")
|
||||
assertThat(result.serverCertificateProperties).isEqualTo(
|
||||
ServerCertificateProperties(
|
||||
subjectAlternativeNames = listOf("*.badssl.com", "badssl.com"),
|
||||
notValidBefore = "2015 Apr 9 00:00",
|
||||
notValidAfter = "2015 Apr 12 23:59",
|
||||
subject = "CN=*.badssl.com, OU=PositiveSSL Wildcard, OU=Domain Control Validated",
|
||||
issuer = "CN=COMODO RSA Domain Validation Secure Server CA, O=COMODO CA Limited, L=Salford, " +
|
||||
"ST=Greater Manchester, C=GB",
|
||||
fingerprintSha1 = "404bbd2f1f4cc2fdeef13aabdd523ef61f1c71f3".decodeHex(),
|
||||
fingerprintSha256 = "ba105ce02bac76888ecee47cd4eb7941653e9ac993b61b2eb3dcc82014d21b4f".decodeHex(),
|
||||
fingerprintSha512 = (
|
||||
"851d7249d64f85d1242090b06224b6da67d442ae38cea5d8a78ae1d7d8c3e2f8" +
|
||||
"f4ad44c7cf239ba5abb05170e0910fd72e6ea5e5c2604888f6c59e5f57c3db27"
|
||||
).decodeHex(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun readCertificate(asciiArmoredCertificate: String): X509Certificate {
|
||||
val inputStream = asciiArmoredCertificate.byteInputStream()
|
||||
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
return certificateFactory.generateCertificate(inputStream) as X509Certificate
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EXPIRED_CERTIFICATE = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFSzCCBDOgAwIBAgIQSueVSfqavj8QDxekeOFpCTANBgkqhkiG9w0BAQsFADCB
|
||||
kDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxNjA0BgNV
|
||||
BAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
|
||||
QTAeFw0xNTA0MDkwMDAwMDBaFw0xNTA0MTIyMzU5NTlaMFkxITAfBgNVBAsTGERv
|
||||
bWFpbiBDb250cm9sIFZhbGlkYXRlZDEdMBsGA1UECxMUUG9zaXRpdmVTU0wgV2ls
|
||||
ZGNhcmQxFTATBgNVBAMUDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2PmzA
|
||||
S2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMWhyef
|
||||
dOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3AxPxT
|
||||
uW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqveww9H
|
||||
dFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SYQCeF
|
||||
xxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaOCAdUwggHRMB8GA1Ud
|
||||
IwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSd7sF7gQs6R2lx
|
||||
GH0RN5O8pRs/+zAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUE
|
||||
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQIC
|
||||
BzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAI
|
||||
BgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9jYS5j
|
||||
b20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNybDCB
|
||||
hQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9kb2Nh
|
||||
LmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0
|
||||
MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wIwYDVR0RBBww
|
||||
GoIMKi5iYWRzc2wuY29tggpiYWRzc2wuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBq
|
||||
evHa/wMHcnjFZqFPRkMOXxQhjHUa6zbgH6QQFezaMyV8O7UKxwE4PSf9WNnM6i1p
|
||||
OXy+l+8L1gtY54x/v7NMHfO3kICmNnwUW+wHLQI+G1tjWxWrAPofOxkt3+IjEBEH
|
||||
fnJ/4r+3ABuYLyw/zoWaJ4wQIghBK4o+gk783SHGVnRwpDTysUCeK1iiWQ8dSO/r
|
||||
ET7BSp68ZVVtxqPv1dSWzfGuJ/ekVxQ8lEEFeouhN0fX9X3c+s5vMaKwjOrMEpsi
|
||||
8TRwz311SotoKQwe6Zaoz7ASH1wq7mcvf71z81oBIgxw+s1F73hczg36TuHvzmWf
|
||||
RwxPuzZEaFZcVlmtqoq8
|
||||
-----END CERTIFICATE-----
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package app.k9mail.feature.account.server.certificate.ui
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
|
||||
class FingerprintFormatterTest {
|
||||
private val formatter = DefaultFingerprintFormatter()
|
||||
|
||||
@Test
|
||||
fun `simple fingerprint`() {
|
||||
val fingerprint = "0088FF".decodeHex()
|
||||
val separatorColor = Color.Cyan
|
||||
|
||||
val result = formatter.format(fingerprint, separatorColor)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
|
||||
append("00")
|
||||
}
|
||||
withStyle(SpanStyle(color = separatorColor)) {
|
||||
append(":")
|
||||
}
|
||||
append('\u200B')
|
||||
|
||||
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
|
||||
append("88")
|
||||
}
|
||||
withStyle(SpanStyle(color = separatorColor)) {
|
||||
append(":")
|
||||
}
|
||||
append('\u200B')
|
||||
|
||||
withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) {
|
||||
append("FF")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue