Skip to content

Commit

Permalink
Merge pull request #78 from /issues/65-infinite-stacking
Browse files Browse the repository at this point in the history
Fix infinite stacking of the same cached certificates
  • Loading branch information
TomasKypta authored Jun 28, 2023
2 parents 2e8db93 + 2585006 commit fcb36b7
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 3 deletions.
6 changes: 4 additions & 2 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ plugins {

android {
namespace "com.wultra.android.sslpinning"
testNamespace "com.wultra.android.sslpinning.test"
compileSdkVersion rootProject.ext.compileSdkVersion

defaultConfig {
Expand All @@ -46,8 +47,8 @@ android {
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// avoids a gradle warning, otherwise unused due to custom config in android-release-aar.gradle
publishing {
Expand All @@ -73,6 +74,7 @@ dependencies {
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70'
testImplementation "io.getlime.security:powerauth-java-crypto:1.4.0"

// androidTestImplementation 'androidx.core:core-ktx:+'
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:rules:1.5.0"
androidTestImplementation "androidx.test.ext:junit:1.1.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,23 @@ internal data class CachedData(var certificates: Array<CertificateInfo>,
internal fun sort() {
certificates.sort()
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as CachedData

if (!certificates.contentEquals(other.certificates)) return false
if (nextUpdate != other.nextUpdate) return false

return true
}

override fun hashCode(): Int {
var result = certificates.contentHashCode()
result = 31 * result + nextUpdate.hashCode()
return result
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.wultra.android.sslpinning.model

import java.io.Serializable
import java.util.*
import java.util.Date

/**
* Data class for holding certificate info necessary for certificate validation.
Expand Down Expand Up @@ -45,4 +45,24 @@ data class CertificateInfo(val commonName: String,
}
return this.commonName.compareTo(other.commonName)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as CertificateInfo

if (commonName != other.commonName) return false
if (!fingerprint.contentEquals(other.fingerprint)) return false
if (expires != other.expires) return false

return true
}

override fun hashCode(): Int {
var result = commonName.hashCode()
result = 31 * result + fingerprint.contentHashCode()
result = 31 * result + expires.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,45 @@ data class GetFingerprintResponse(val fingerprints: Array<Entry>) {
val signedString = "${name}&${fingerprintPart}&${expirationTimestampInSeconds}"
return SignedData(data = signedString.toByteArray(Charsets.UTF_8), signature = signature)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Entry

if (name != other.name) return false
if (!fingerprint.contentEquals(other.fingerprint)) return false
if (expires != other.expires) return false
if (signature != null) {
if (other.signature == null) return false
if (!signature.contentEquals(other.signature)) return false
} else if (other.signature != null) return false

return true
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + fingerprint.contentHashCode()
result = 31 * result + expires.hashCode()
result = 31 * result + (signature?.contentHashCode() ?: 0)
return result
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as GetFingerprintResponse

if (!fingerprints.contentEquals(other.fingerprints)) return false

return true
}

override fun hashCode(): Int {
return fingerprints.contentHashCode()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
*/

package com.wultra.android.sslpinning.model

import android.util.Base64
import com.wultra.android.sslpinning.CertStore
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.util.Date

/**
*
*/
internal class CachedDataTest {
@Before
fun setUp() {
mockkStatic(Base64::class)
every { Base64.decode(any<String>(), any()) } answers {
java.util.Base64.getDecoder().decode(it.invocation.args[0] as String)
}
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun testEntries() {
// the 1st item has different signature, otherwise the data are the same
val jsonData = listOf("""{"fingerprints": [
{
"name" : "github.com",
"fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=",
"expires" : 1710460799,
"signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q=="
}
]}""", """{"fingerprints": [
{
"name" : "github.com",
"fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=",
"expires" : 1710460799,
"signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA="
}
]}""")

val responses = jsonData.map {
CertStore.GSON.fromJson(it, GetFingerprintResponse::class.java)
}
responses.forEach {
Assert.assertEquals(1, it.fingerprints.size)
}

val date = Date()
val cachedDatas = responses.map {
val certInfos = it.fingerprints.map { entry -> CertificateInfo(entry) }.toTypedArray()
Assert.assertEquals(1, certInfos.size)
CachedData(certInfos, date)
}

Assert.assertEquals(2, cachedDatas.size)
Assert.assertEquals(cachedDatas[0], cachedDatas[1])
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2023 Wultra s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
*/

package com.wultra.android.sslpinning.model

import android.util.Base64
import com.wultra.android.sslpinning.CertStore
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase.assertTrue
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test


/**
*
*/
internal class CertificateInfoTest {

@Before
fun setUp() {
mockkStatic(Base64::class)
every { Base64.decode(any<String>(), any()) } answers {
java.util.Base64.getDecoder().decode(it.invocation.args[0] as String)
}
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun testIndexOf() {
val certList = mutableListOf<CertificateInfo>()

// different signatures of the same fingerprints
// simulates updates with different data
val jsonData = listOf("""{"fingerprints": [
{
"name" : "github.com",
"fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=",
"expires" : 1710460799,
"signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q=="
}
]}""", """{"fingerprints": [
{
"name" : "github.com",
"fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=",
"expires" : 1710460799,
"signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA="
}
]}""")

// we add the first one
val response = CertStore.GSON.fromJson(jsonData[0], GetFingerprintResponse::class.java)
Assert.assertEquals(1, response.fingerprints.size)
for (entry in response.fingerprints) {
certList.add(CertificateInfo(entry))
}

for (json in jsonData) {
val resp = CertStore.GSON.fromJson(json, GetFingerprintResponse::class.java)
Assert.assertEquals(1, resp.fingerprints.size)
val ci = CertificateInfo(resp.fingerprints[0])
// the data should pre already present
val idx = certList.indexOf(ci)
assertTrue(idx != -1)
assertTrue(certList.contains(ci))
}
}
}
Loading

0 comments on commit fcb36b7

Please sign in to comment.