diff --git a/line-sdk/src/main/java/com/linecorp/linesdk/auth/internal/LineAuthenticationController.java b/line-sdk/src/main/java/com/linecorp/linesdk/auth/internal/LineAuthenticationController.java index c17209a..a7b88d3 100644 --- a/line-sdk/src/main/java/com/linecorp/linesdk/auth/internal/LineAuthenticationController.java +++ b/line-sdk/src/main/java/com/linecorp/linesdk/auth/internal/LineAuthenticationController.java @@ -238,8 +238,14 @@ protected LineLoginResult doInBackground(@Nullable BrowserAuthenticationApi.Resu userId = lineProfile.getUserId(); } - // Cache the acquired access token - accessTokenCache.saveAccessToken(accessToken); + // Cache the acquired access token. A broken device Keystore can throw here, so treat a + // persistence failure as a login failure instead of crashing the background task. + try { + accessTokenCache.saveAccessToken(accessToken); + } catch (final Exception e) { + return LineLoginResult.internalError( + e.getMessage() != null ? e.getMessage() : e.toString()); + } final LineIdToken idToken = issueAccessTokenResult.getIdToken(); if (idToken != null) { diff --git a/line-sdk/src/main/java/com/linecorp/linesdk/internal/EncryptorHolder.java b/line-sdk/src/main/java/com/linecorp/linesdk/internal/EncryptorHolder.java index 0b7ddfa..8ad9269 100644 --- a/line-sdk/src/main/java/com/linecorp/linesdk/internal/EncryptorHolder.java +++ b/line-sdk/src/main/java/com/linecorp/linesdk/internal/EncryptorHolder.java @@ -1,6 +1,7 @@ package com.linecorp.linesdk.internal; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; @@ -14,6 +15,7 @@ * This class prevents to generate secret keys repeatedly because it is very slow. */ public class EncryptorHolder { + private static final String TAG = "EncryptorHolder"; // TODO: Change to be able to specify the iteration count by LINE SDK user. private static final StringCipher ENCRYPTOR = new StringAesCipher(); private static volatile boolean s_isInitializationStarted = false; @@ -45,7 +47,13 @@ private static class EncryptorInitializationTask implements Runnable { @Override public void run() { - ENCRYPTOR.initialize(context); + try { + ENCRYPTOR.initialize(context); + } catch (Exception e) { + // Pre-init is only a latency optimization; a broken Keystore must not crash the + // host app on this background thread. + Log.w(TAG, "Encryptor pre-init failed", e); + } } } } diff --git a/line-sdk/src/test/java/com/linecorp/linesdk/auth/internal/LineAuthenticationControllerTest.java b/line-sdk/src/test/java/com/linecorp/linesdk/auth/internal/LineAuthenticationControllerTest.java index 3d53ebf..ad045ca 100644 --- a/line-sdk/src/test/java/com/linecorp/linesdk/auth/internal/LineAuthenticationControllerTest.java +++ b/line-sdk/src/test/java/com/linecorp/linesdk/auth/internal/LineAuthenticationControllerTest.java @@ -4,6 +4,8 @@ import android.content.Intent; import android.net.Uri; +import androidx.annotation.NonNull; + import com.linecorp.linesdk.LineAccessToken; import com.linecorp.linesdk.LineApiError; import com.linecorp.linesdk.LineApiResponse; @@ -24,10 +26,13 @@ import com.linecorp.linesdk.internal.nwclient.LineAuthenticationApiClient; import com.linecorp.linesdk.internal.nwclient.TalkApiClient; import com.linecorp.linesdk.internal.pkce.PKCECode; +import com.linecorp.linesdk.internal.security.encryption.EncryptionException; +import com.linecorp.linesdk.internal.security.encryption.StringCipher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -36,6 +41,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import java.security.ProviderException; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -110,6 +116,30 @@ public class LineAuthenticationControllerTest { private static final Intent LOGIN_INTENT = new Intent(); + private static class ThrowingStringCipher implements StringCipher { + @Override + public void initialize(@NonNull Context context) { + throw failure(); + } + + @NonNull + @Override + public String encrypt(@NonNull Context context, @NonNull String plainText) { + throw failure(); + } + + @NonNull + @Override + public String decrypt(@NonNull Context context, @NonNull String cipherText) { + throw failure(); + } + + private static EncryptionException failure() { + return new EncryptionException("keystore failure", + new ProviderException("Keystore key generation failed")); + } + } + private LineAuthenticationController target; private LineAuthenticationConfig config; @@ -230,6 +260,46 @@ public void testInternalErrorOfGettingAccessToken() throws Exception { LineLoginResult.error(LineApiResponseCode.INTERNAL_ERROR, LineApiError.DEFAULT)); } + @Test + public void testKeystoreErrorOfSavingAccessToken() throws Exception { + AccessTokenCache failingCache = new AccessTokenCache( + RuntimeEnvironment.application, CHANNEL_ID, new ThrowingStringCipher()); + LineAuthenticationStatus status = new LineAuthenticationStatus(); + status.setOpenIdNonce(NONCE); + target = Mockito.spy(new LineAuthenticationController( + activity, config, authApiClient, talkApiClient, browserAuthenticationApi, + failingCache, status, LINE_AUTH_PARAMS)); + doReturn(PKCE_CODE).when(target).createPKCECode(); + doReturn(new BrowserAuthenticationApi.Request( + LOGIN_INTENT, null /* startActivityOption */, REDIRECT_URI, false)) + .when(browserAuthenticationApi) + .getRequest(any(Context.class), any(LineAuthenticationConfig.class), + any(PKCECode.class), any(LineAuthenticationParams.class)); + + Intent newIntentData = new Intent(); + doReturn(BrowserAuthenticationApi.Result.createAsSuccess(REQUEST_TOKEN_STR, false)) + .when(browserAuthenticationApi) + .getAuthenticationResultFrom(newIntentData); + doReturn(LineApiResponse.createAsSuccess(ISSUE_ACCESS_TOKEN_RESULT)) + .when(authApiClient) + .issueAccessToken(CHANNEL_ID, REQUEST_TOKEN_STR, PKCE_CODE, REDIRECT_URI); + doReturn(LineApiResponse.createAsSuccess(ACCOUNT_INFO)) + .when(talkApiClient) + .getProfile(ACCESS_TOKEN); + + target.startLineAuthentication(); + Robolectric.getBackgroundThreadScheduler().runOneTask(); + Robolectric.getForegroundThreadScheduler().runOneTask(); + + target.handleIntentFromLineApp(newIntentData); + Robolectric.getBackgroundThreadScheduler().runOneTask(); + Robolectric.getForegroundThreadScheduler().runOneTask(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LineLoginResult.class); + verify(activity, times(1)).onAuthenticationFinished(captor.capture()); + assertEquals(LineApiResponseCode.INTERNAL_ERROR, captor.getValue().getResponseCode()); + } + @Test public void testAccessDeniedErrorOfGettingAccessToken() throws Exception { Intent newIntentData = new Intent();