From d9133e92850f7cee7c743ab41b50b5a40a1cf5c9 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 27 Jun 2024 15:43:01 -0300 Subject: [PATCH 1/5] Fix in failure listener --- .../network/CertificateCheckerImpl.java | 10 +++++-- .../network/CertificateCheckerImplTest.java | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java b/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java index 12ce682f0..f733e28f9 100644 --- a/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java +++ b/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java @@ -51,7 +51,7 @@ class CertificateCheckerImpl implements CertificateChecker { } @Override - public void checkPins(HttpsURLConnection httpsConnection) throws SSLPeerUnverifiedException { + public synchronized void checkPins(HttpsURLConnection httpsConnection) throws SSLPeerUnverifiedException { String host = httpsConnection.getURL().getHost(); Set pinsForHost = getPinsForHost(host, mConfiguredPins); if (pinsForHost == null || pinsForHost.isEmpty()) { @@ -78,8 +78,12 @@ public void checkPins(HttpsURLConnection httpsConnection) throws SSLPeerUnverifi } } - if (mFailureListener != null) { - mFailureListener.onCertificatePinningFailure(host, cleanCertificates); + try { + if (mFailureListener != null) { + mFailureListener.onCertificatePinningFailure(host, cleanCertificates); + } + } catch (Exception e) { + Logger.w("Exception occurred executing certificate pinning failure listener: " + e.getLocalizedMessage()); } throw new SSLPeerUnverifiedException("Certificate pinning verification failed for host: " + host + ". Chain:\n" + certificateChainInfo(cleanCertificates)); diff --git a/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java b/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java index 98b59dc99..f6c11a9f0 100644 --- a/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java +++ b/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; @@ -238,6 +239,34 @@ public void failureListenerIsCalledWhenThereAreHostsButNoMatchingCerts() { verify(mFailureListener).onCertificatePinningFailure(any(), any()); } + @Test + public void sslPeerUnverifiedExceptionIsThrownWhenThereAreHostsButNoMatchingCertsAndFailureListenerThrows() { + PublicKey mockedPublicKey = mock(PublicKey.class); + byte[] bytes1 = {0, 1, 2, 3}; + when(mockedPublicKey.getEncoded()).thenReturn(bytes1); + + Principal mockedPrincipal = mock(Principal.class); + when(mockedPrincipal.getName()).thenReturn("CN=cert1"); + + X509Certificate mockedX509Cert = mock(X509Certificate.class); + when(mockedX509Cert.getPublicKey()).thenReturn(mockedPublicKey); + when(mockedX509Cert.getSubjectDN()).thenReturn(mockedPrincipal); + + CertificatePin pin = new CertificatePin(new byte[]{1, 2, 2, 3}, "sha256"); + when(mChainCleaner.clean(any(), any())).thenReturn(Collections.singletonList(mockedX509Cert)); + when(mPinEncoder.encodeCertPin(any(), any())).thenReturn(new byte[]{3, 2, 2, 3}); + doThrow(new RuntimeException("Error in failure listener")).when(mFailureListener).onCertificatePinningFailure(any(), any()); + + mChecker = getChecker(Collections.singletonMap("www.subdomain.my-url.com", Collections.singleton(pin))); + + try { + mChecker.checkPins(mMockConnection); + } catch (SSLPeerUnverifiedException e) { + return; + } + fail(); + } + @Test public void sslPeerUnverifiedExceptionIsThrownWhenThereAreHostsButNoMatchingCerts() { PublicKey mockedPublicKey = mock(PublicKey.class); From e52fb28a3df3471b2001f901d0e1029209d1bee8 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 27 Jun 2024 16:51:47 -0300 Subject: [PATCH 2/5] New instrumented test --- .../integration/pin/CertPinningTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/androidTest/java/tests/integration/pin/CertPinningTest.java b/src/androidTest/java/tests/integration/pin/CertPinningTest.java index 42039814b..088167a40 100644 --- a/src/androidTest/java/tests/integration/pin/CertPinningTest.java +++ b/src/androidTest/java/tests/integration/pin/CertPinningTest.java @@ -10,8 +10,11 @@ import org.junit.Before; import org.junit.Test; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -140,6 +143,39 @@ public void onPostExecution(SplitClient client) { assertFalse(failureAwait); } + @Test + public void certPinningSuccessfulWithFileTest() throws InterruptedException, IOException, CertificateEncodingException { + // Generate file with certificate + try (FileOutputStream outputStream = mContext.openFileOutput("generated_cert.der", Context.MODE_PRIVATE)) { + outputStream.write(mHeldCertificate.certificate().getEncoded()); + } + + try (InputStream certInputStream = mContext.openFileInput("generated_cert.der")) { + // Setup factory with pin from file + CountDownLatch failureLatch = new CountDownLatch(2); // 2 counts, one for splitChanges and one for mySegments + SplitClientConfig config = getConfig(CertificatePinningConfiguration.builder() + .addPin("localhost", certInputStream) + .failureListener((host, certificateChain) -> failureLatch.countDown()) + .build()); + + SplitFactory factory = getFactory(config); + + CountDownLatch latch = new CountDownLatch(1); + factory.client().on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + latch.countDown(); + } + }); + boolean await = latch.await(5, TimeUnit.SECONDS); + boolean failureAwait = failureLatch.await(5, TimeUnit.SECONDS); + + // verify client is ready and no pinning failures were registered in listener + assertTrue(await); + assertFalse(failureAwait); + } + } + private String sha256(byte[] encoded) { try { return IntegrationHelper.sha256(encoded); From 44d169ad8d630d942fa5888200f098d82f9cec86 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 27 Jun 2024 16:56:18 -0300 Subject: [PATCH 3/5] Version 4.2.0-rc2 --- build.gradle | 2 +- src/main/java/io/split/android/client/SplitClientConfig.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9e128c845..d8d853c39 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '4.2.0-alpha.3' + splitVersion = '4.2.0-rc2' } android { diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index f70b660f8..92de2dba8 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -1094,6 +1094,8 @@ public Builder certificatePinningConfiguration(CertificatePinningConfiguration c * * @Experimental This method is experimental and may change or be removed in future versions. * To be used upon Split team recommendation. + * + * @param impressionsDedupeTimeInterval The time interval in milliseconds. */ @Deprecated public Builder impressionsDedupeTimeInterval(long impressionsDedupeTimeInterval) { From de38f2b1f3419cfed01e7ed6620e9aba1b1c328e Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 28 Jun 2024 10:34:41 -0300 Subject: [PATCH 4/5] Change observer cache expiration --- .../observer/DedupeIntegrationTest.java | 1 + .../android/client/SplitClientConfig.java | 2 +- .../android/client/SplitClientConfigTest.java | 33 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index 3d9740a57..2fb949d71 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java @@ -216,6 +216,7 @@ public void expiredObserverCacheValuesExistingInDatabaseAreRemovedOnStartup() th SplitClient client = initSplitFactory(new TestableSplitConfigBuilder() .impressionsMode(ImpressionsMode.DEBUG) .enableDebug() + .impressionsDedupeTimeInterval(1) .observerCacheExpirationPeriod(100), mHttpClient).client(); client.getTreatment("FACUNDO_TEST"); diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index 92de2dba8..61e423a87 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -476,7 +476,7 @@ public long sseDisconnectionDelay() { private void enableTelemetry() { mShouldRecordTelemetry = true; } public long observerCacheExpirationPeriod() { - return mObserverCacheExpirationPeriod; + return Math.max(mImpressionsDedupeTimeInterval, mObserverCacheExpirationPeriod); } public CertificatePinningConfiguration certificatePinningConfiguration() { diff --git a/src/test/java/io/split/android/client/SplitClientConfigTest.java b/src/test/java/io/split/android/client/SplitClientConfigTest.java index 329ffa46f..8f3351ccb 100644 --- a/src/test/java/io/split/android/client/SplitClientConfigTest.java +++ b/src/test/java/io/split/android/client/SplitClientConfigTest.java @@ -191,6 +191,39 @@ public void defaultImpressionsDedupeTimeIntervalIsOneHour() { assertEquals(TimeUnit.HOURS.toMillis(1), config.impressionsDedupeTimeInterval()); } + @Test + public void defaultObserverCacheExpirationPeriodIs4Hours() { + SplitClientConfig config = SplitClientConfig.builder().build(); + + assertEquals(TimeUnit.HOURS.toMillis(4), config.observerCacheExpirationPeriod()); + } + + @Test + public void observerCacheExpirationPeriodIs4HoursWhenDedupeTimeIntervalIsLessThan4Hours() { + SplitClientConfig config = SplitClientConfig.builder() + .impressionsDedupeTimeInterval(TimeUnit.HOURS.toMillis(3)) + .build(); + + assertEquals(TimeUnit.HOURS.toMillis(4), config.observerCacheExpirationPeriod()); + } + + @Test + public void observerCacheExpirationPeriodMatchesDedupeTimeIntervalWhenDedupeTimeIntervalIsGreaterThan4Hours() { + SplitClientConfig config = SplitClientConfig.builder() + .impressionsDedupeTimeInterval(TimeUnit.HOURS.toMillis(5)) + .build(); + SplitClientConfig config2 = SplitClientConfig.builder() + .impressionsDedupeTimeInterval(TimeUnit.HOURS.toMillis(24)) + .build(); + SplitClientConfig config3 = SplitClientConfig.builder() + .impressionsDedupeTimeInterval(TimeUnit.HOURS.toMillis(25)) + .build(); + + assertEquals(TimeUnit.HOURS.toMillis(5), config.observerCacheExpirationPeriod()); + assertEquals(TimeUnit.HOURS.toMillis(24), config2.observerCacheExpirationPeriod()); + assertEquals(TimeUnit.HOURS.toMillis(4), config3.observerCacheExpirationPeriod()); + } + @NonNull private static Queue getLogMessagesQueue() { Queue logMessages = new LinkedList<>(); From fa18c1327077191d9d78ee8e9a11b9f34e737e65 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 28 Jun 2024 10:35:12 -0300 Subject: [PATCH 5/5] Prepare release 4.2.0 --- CHANGES.txt | 3 +++ build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b15cd7cf1..3984046d3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +4.2.0 (Jun 28, 2024) +- Added certificate pinning functionality. This feature allows you to pin a certificate to the SDK, ensuring that the SDK only communicates with servers that present this certificate. Read more in our documentation. + 4.1.1 (Jun 6, 2024) - Fixed concurrency issue when building URLs. diff --git a/build.gradle b/build.gradle index d8d853c39..e463f2568 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '4.2.0-rc2' + splitVersion = '4.2.0' } android {