Skip to content

Commit

Permalink
Prepare release 4.2.0 (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
gthea authored Jun 28, 2024
2 parents b5cbd0f + fa18c13 commit 266dbd8
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android'
apply from: 'spec.gradle'

ext {
splitVersion = '4.2.0-alpha.3'
splitVersion = '4.2.0'
}

android {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
36 changes: 36 additions & 0 deletions src/androidTest/java/tests/integration/pin/CertPinningTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/io/split/android/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CertificatePin> pinsForHost = getPinsForHost(host, mConfiguredPins);
if (pinsForHost == null || pinsForHost.isEmpty()) {
Expand All @@ -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));
Expand Down
33 changes: 33 additions & 0 deletions src/test/java/io/split/android/client/SplitClientConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> getLogMessagesQueue() {
Queue<String> logMessages = new LinkedList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 266dbd8

Please sign in to comment.