From 17cfd36705ca7299676174cd230b04f9fa31a7b3 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:09:04 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4java=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/AndroidManifest.xml | 10 - .../release/activity/BiometricActivity.kt | 6 +- .../jc/jcfw/accountmanager/Authenticator.java | 56 ---- .../accountmanager/AuthenticatorService.java | 19 -- .../com/jc/jcfw/appauth/AuthStateManager.java | 150 --------- .../com/jc/jcfw/appauth/AuthStateManager.kt | 130 ++++++++ .../appauth/ConnectionBuilderForTesting.java | 108 ------ .../com/jc/jcfw/appauth/JConfiguration.java | 310 ------------------ app/src/com/jc/jcfw/appauth/JConfiguration.kt | 223 +++++++++++++ .../com/jc/jcfw/security/BiometricHelper.java | 61 ---- .../com/jc/jcfw/security/BiometricHelper.kt | 54 +++ .../com/jc/jcfw/security/BiometricResult.java | 61 ---- .../com/jc/jcfw/security/BiometricResult.kt | 27 ++ ...phyManager.java => CryptographyManager.kt} | 16 +- .../security/CryptographyManagerImpl.java | 101 ------ .../jcfw/security/CryptographyManagerImpl.kt | 96 ++++++ .../com/jc/jcfw/security/EncryptedData.java | 19 -- app/src/com/jc/jcfw/security/EncryptedData.kt | 3 + 18 files changed, 544 insertions(+), 906 deletions(-) delete mode 100644 app/src/com/jc/jcfw/accountmanager/Authenticator.java delete mode 100644 app/src/com/jc/jcfw/accountmanager/AuthenticatorService.java delete mode 100644 app/src/com/jc/jcfw/appauth/AuthStateManager.java create mode 100644 app/src/com/jc/jcfw/appauth/AuthStateManager.kt delete mode 100644 app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java delete mode 100644 app/src/com/jc/jcfw/appauth/JConfiguration.java create mode 100644 app/src/com/jc/jcfw/appauth/JConfiguration.kt delete mode 100644 app/src/com/jc/jcfw/security/BiometricHelper.java create mode 100644 app/src/com/jc/jcfw/security/BiometricHelper.kt delete mode 100644 app/src/com/jc/jcfw/security/BiometricResult.java create mode 100644 app/src/com/jc/jcfw/security/BiometricResult.kt rename app/src/com/jc/jcfw/security/{CryptographyManager.java => CryptographyManager.kt} (59%) delete mode 100644 app/src/com/jc/jcfw/security/CryptographyManagerImpl.java create mode 100644 app/src/com/jc/jcfw/security/CryptographyManagerImpl.kt delete mode 100644 app/src/com/jc/jcfw/security/EncryptedData.java create mode 100644 app/src/com/jc/jcfw/security/EncryptedData.kt diff --git a/app/AndroidManifest.xml b/app/AndroidManifest.xml index 1f221f0..89d429c 100644 --- a/app/AndroidManifest.xml +++ b/app/AndroidManifest.xml @@ -219,16 +219,6 @@ - - - - - - - > INSTANCE_REF = - new AtomicReference<>(new WeakReference<>(null)); - - private static final String TAG = "AuthStateManager"; - - private static final String STORE_NAME = "AuthState"; - private static final String KEY_STATE = "state"; - - private final SharedPreferences mPrefs; - private final ReentrantLock mPrefsLock; - private final AtomicReference mCurrentAuthState; - - @AnyThread - public static AuthStateManager getInstance(@NonNull Context context) { - AuthStateManager manager = INSTANCE_REF.get().get(); - if (manager == null) { - manager = new AuthStateManager(context.getApplicationContext()); - INSTANCE_REF.set(new WeakReference<>(manager)); - } - - return manager; - } - - private AuthStateManager(Context context) { - mPrefs = context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE); - mPrefsLock = new ReentrantLock(); - mCurrentAuthState = new AtomicReference<>(); - } - - @AnyThread - @NonNull - public AuthState getCurrent() { - if (mCurrentAuthState.get() != null) { - return mCurrentAuthState.get(); - } - - AuthState state = readState(); - if (mCurrentAuthState.compareAndSet(null, state)) { - return state; - } else { - return mCurrentAuthState.get(); - } - } - - @AnyThread - @NonNull - public AuthState replace(@NonNull AuthState state) { - writeState(state); - mCurrentAuthState.set(state); - return state; - } - - @AnyThread - @NonNull - public AuthState updateAfterAuthorization( - @Nullable AuthorizationResponse response, - @Nullable AuthorizationException ex) { - AuthState current = getCurrent(); - current.update(response, ex); - return replace(current); - } - - @AnyThread - @NonNull - public AuthState updateAfterTokenResponse( - @Nullable TokenResponse response, - @Nullable AuthorizationException ex) { - AuthState current = getCurrent(); - current.update(response, ex); - return replace(current); - } - - @AnyThread - @NonNull - public AuthState updateAfterRegistration( - RegistrationResponse response, - AuthorizationException ex) { - AuthState current = getCurrent(); - if (ex != null) { - return current; - } - - current.update(response); - return replace(current); - } - - @AnyThread - @NonNull - private AuthState readState() { - mPrefsLock.lock(); - try { - String currentState = mPrefs.getString(KEY_STATE, null); - if (currentState == null) { - return new AuthState(); - } - - try { - return AuthState.jsonDeserialize(currentState); - } catch (JSONException ex) { - Log.w(TAG, "Failed to deserialize stored auth state - discarding"); - return new AuthState(); - } - } finally { - mPrefsLock.unlock(); - } - } - - @AnyThread - private void writeState(@Nullable AuthState state) { - mPrefsLock.lock(); - try { - SharedPreferences.Editor editor = mPrefs.edit(); - if (state == null) { - editor.remove(KEY_STATE); - } else { - editor.putString(KEY_STATE, state.jsonSerializeString()); - } - - if (!editor.commit()) { - throw new IllegalStateException("Failed to write state to shared prefs"); - } - } finally { - mPrefsLock.unlock(); - } - } -} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/appauth/AuthStateManager.kt b/app/src/com/jc/jcfw/appauth/AuthStateManager.kt new file mode 100644 index 0000000..15846d7 --- /dev/null +++ b/app/src/com/jc/jcfw/appauth/AuthStateManager.kt @@ -0,0 +1,130 @@ +package com.jc.jcfw.appauth + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import androidx.annotation.AnyThread +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.RegistrationResponse +import net.openid.appauth.TokenResponse +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock + +class AuthStateManager private constructor(context: Context) { + private val mPrefs: SharedPreferences + private val mPrefsLock: ReentrantLock + private val mCurrentAuthState: AtomicReference + + init { + mPrefs = context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE) + mPrefsLock = ReentrantLock() + mCurrentAuthState = AtomicReference() + } + + @get:AnyThread + val current: AuthState + get() { + if (mCurrentAuthState.get() != null) { + return mCurrentAuthState.get() + } + val state = readState() + return if (mCurrentAuthState.compareAndSet(null, state)) { + state + } else { + mCurrentAuthState.get() + } + } + + @AnyThread + fun replace(state: AuthState): AuthState { + writeState(state) + mCurrentAuthState.set(state) + return state + } + + @AnyThread + fun updateAfterAuthorization( + response: AuthorizationResponse?, + ex: AuthorizationException? + ): AuthState { + val current = current + current.update(response, ex) + return replace(current) + } + + @AnyThread + fun updateAfterTokenResponse( + response: TokenResponse?, + ex: AuthorizationException? + ): AuthState { + val current = current + current.update(response, ex) + return replace(current) + } + + @AnyThread + fun updateAfterRegistration( + response: RegistrationResponse?, + ex: AuthorizationException? + ): AuthState { + val current = current + if (ex != null) { + return current + } + current.update(response) + return replace(current) + } + + @AnyThread + private fun readState(): AuthState { + mPrefsLock.lock() + return try { + val currentState = mPrefs.getString(KEY_STATE, null) + ?: return AuthState() + try { + AuthState.jsonDeserialize(currentState) + } catch (ex: JSONException) { + Log.w(TAG, "Failed to deserialize stored auth state - discarding") + AuthState() + } + } finally { + mPrefsLock.unlock() + } + } + + @AnyThread + private fun writeState(state: AuthState?) { + mPrefsLock.lock() + try { + val editor = mPrefs.edit() + if (state == null) { + editor.remove(KEY_STATE) + } else { + editor.putString(KEY_STATE, state.jsonSerializeString()) + } + check(editor.commit()) { "Failed to write state to shared prefs" } + } finally { + mPrefsLock.unlock() + } + } + + companion object { + private val INSTANCE_REF = AtomicReference(WeakReference(null)) + private const val TAG = "AuthStateManager" + private const val STORE_NAME = "AuthState" + private const val KEY_STATE = "state" + @AnyThread + fun getInstance(context: Context): AuthStateManager { + var manager = INSTANCE_REF.get().get() + if (manager == null) { + manager = AuthStateManager(context.applicationContext) + INSTANCE_REF.set(WeakReference(manager)) + } + return manager + } + } +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java b/app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java deleted file mode 100644 index d5b1386..0000000 --- a/app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.jc.jcfw.appauth; - -import android.annotation.SuppressLint; -import android.net.Uri; -import android.util.Log; - -import net.openid.appauth.Preconditions; -import net.openid.appauth.connectivity.ConnectionBuilder; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class ConnectionBuilderForTesting implements ConnectionBuilder { - - public static final ConnectionBuilderForTesting INSTANCE = new ConnectionBuilderForTesting(); - - private static final String TAG = "ConnBuilder"; - - private static final int CONNECTION_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(15); - private static final int READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10); - - private static final String HTTP = "http"; - private static final String HTTPS = "https"; - - @SuppressLint("TrustAllX509TrustManager") - private static final TrustManager[] ANY_CERT_MANAGER = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - - public void checkServerTrusted(X509Certificate[] certs, String authType) {} - } - }; - - @SuppressLint("BadHostnameVerifier") - private static final HostnameVerifier ANY_HOSTNAME_VERIFIER = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - - @Nullable - private static final SSLContext TRUSTING_CONTEXT; - - static { - SSLContext context; - try { - context = SSLContext.getInstance("SSL"); - } catch (NoSuchAlgorithmException e) { - Log.e("ConnBuilder", "Unable to acquire SSL context"); - context = null; - } - - SSLContext initializedContext = null; - if (context != null) { - try { - context.init(null, ANY_CERT_MANAGER, new java.security.SecureRandom()); - initializedContext = context; - } catch (KeyManagementException e) { - Log.e(TAG, "Failed to initialize trusting SSL context"); - } - } - - TRUSTING_CONTEXT = initializedContext; - } - - private ConnectionBuilderForTesting() { - // no need to construct new instances - } - - @NonNull - @Override - public HttpURLConnection openConnection(@NonNull Uri uri) throws IOException { - Preconditions.checkNotNull(uri, "url must not be null"); - Preconditions.checkArgument(HTTP.equals(uri.getScheme()) || HTTPS.equals(uri.getScheme()), - "scheme or uri must be http or https"); - HttpURLConnection conn = (HttpURLConnection) new URL(uri.toString()).openConnection(); - conn.setConnectTimeout(CONNECTION_TIMEOUT_MS); - conn.setReadTimeout(READ_TIMEOUT_MS); - conn.setInstanceFollowRedirects(false); - - if (conn instanceof HttpsURLConnection && TRUSTING_CONTEXT != null) { - HttpsURLConnection httpsConn = (HttpsURLConnection) conn; - httpsConn.setSSLSocketFactory(TRUSTING_CONTEXT.getSocketFactory()); - httpsConn.setHostnameVerifier(ANY_HOSTNAME_VERIFIER); - } - - return conn; - } -} diff --git a/app/src/com/jc/jcfw/appauth/JConfiguration.java b/app/src/com/jc/jcfw/appauth/JConfiguration.java deleted file mode 100644 index 30e610a..0000000 --- a/app/src/com/jc/jcfw/appauth/JConfiguration.java +++ /dev/null @@ -1,310 +0,0 @@ -package com.jc.jcfw.appauth; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.net.Uri; -import android.text.TextUtils; - -import com.ctf.games.release.R; - -import net.openid.appauth.connectivity.ConnectionBuilder; -import net.openid.appauth.connectivity.DefaultConnectionBuilder; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.charset.StandardCharsets; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import okio.Buffer; -import okio.BufferedSource; -import okio.Okio; - -public final class JConfiguration { - - private static final String TAG = "Configuration"; - - private static final String PREFS_NAME = "config"; - private static final String KEY_LAST_HASH = "lastHash"; - - private static WeakReference sInstance = new WeakReference<>(null); - - private final Context mContext; - private final SharedPreferences mPrefs; - private final Resources mResources; - - private JSONObject mConfigJson; - private String mConfigHash; - private String mConfigError; - - private String mClientId; - private String mScope; - private Uri mRedirectUri; - private Uri mEndSessionRedirectUri; - private Uri mDiscoveryUri; - private Uri mAuthEndpointUri; - private Uri mTokenEndpointUri; - private Uri mEndSessionEndpoint; - private Uri mRegistrationEndpointUri; - private Uri mUserInfoEndpointUri; - private boolean mHttpsRequired; - - public static JConfiguration getInstance(Context context) { - JConfiguration config = sInstance.get(); - if (config == null) { - config = new JConfiguration(context); - sInstance = new WeakReference(config); - } - - return config; - } - - public JConfiguration(Context context) { - mContext = context; - mPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - mResources = context.getResources(); - - try { - readConfiguration(); - } catch (InvalidConfigurationException ex) { - mConfigError = ex.getMessage(); - } - } - - /** - * Indicates whether the configuration has changed from the last known valid state. - */ - public boolean hasConfigurationChanged() { - String lastHash = getLastKnownConfigHash(); - return !mConfigHash.equals(lastHash); - } - - /** - * Indicates whether the current configuration is valid. - */ - public boolean isValid() { - return mConfigError == null; - } - - /** - * Returns a description of the configuration error, if the configuration is invalid. - */ - @Nullable - public String getConfigurationError() { - return mConfigError; - } - - /** - * Indicates that the current configuration should be accepted as the "last known valid" - * configuration. - */ - public void acceptConfiguration() { - mPrefs.edit().putString(KEY_LAST_HASH, mConfigHash).apply(); - } - - @Nullable - public String getClientId() { - return mClientId; - } - - @NonNull - public String getScope() { - return mScope; - } - - @NonNull - public Uri getRedirectUri() { - return mRedirectUri; - } - - @Nullable - public Uri getDiscoveryUri() { - return mDiscoveryUri; - } - - @Nullable - public Uri getEndSessionRedirectUri() { return mEndSessionRedirectUri; } - - @Nullable - public Uri getAuthEndpointUri() { - return mAuthEndpointUri; - } - - @Nullable - public Uri getTokenEndpointUri() { - return mTokenEndpointUri; - } - - @Nullable - public Uri getEndSessionEndpoint() { - return mEndSessionEndpoint; - } - - @Nullable - public Uri getRegistrationEndpointUri() { - return mRegistrationEndpointUri; - } - - @Nullable - public Uri getUserInfoEndpointUri() { - return mUserInfoEndpointUri; - } - - public boolean isHttpsRequired() { - return mHttpsRequired; - } - - public ConnectionBuilder getConnectionBuilder() { - if (isHttpsRequired()) { - return DefaultConnectionBuilder.INSTANCE; - } - return ConnectionBuilderForTesting.INSTANCE; - } - - private String getLastKnownConfigHash() { - return mPrefs.getString(KEY_LAST_HASH, null); - } - - private void readConfiguration() throws InvalidConfigurationException { - BufferedSource configSource = - Okio.buffer(Okio.source(mResources.openRawResource(R.raw.auth_config))); - Buffer configData = new Buffer(); - try { - configSource.readAll(configData); - mConfigJson = new JSONObject(configData.readString(StandardCharsets.UTF_8)); - } catch (IOException ex) { - throw new InvalidConfigurationException( - "Failed to read configuration: " + ex.getMessage()); - } catch (JSONException ex) { - throw new InvalidConfigurationException( - "Unable to parse configuration: " + ex.getMessage()); - } - - mConfigHash = configData.sha256().base64(); - mClientId = getConfigString("client_id"); - mScope = getRequiredConfigString("authorization_scope"); - mRedirectUri = getRequiredConfigUri("redirect_uri"); - mEndSessionRedirectUri = getRequiredConfigUri("end_session_redirect_uri"); - - if (!isRedirectUriRegistered()) { - throw new InvalidConfigurationException( - "redirect_uri is not handled by any activity in this app! " - + "Ensure that the appAuthRedirectScheme in your build.gradle file " - + "is correctly configured, or that an appropriate intent filter " - + "exists in your app manifest."); - } - - if (getConfigString("discovery_uri") == null) { - mAuthEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri"); - - mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri"); - mUserInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri"); - mEndSessionEndpoint = getRequiredConfigUri("end_session_endpoint"); - - if (mClientId == null) { - mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri"); - } - } else { - mDiscoveryUri = getRequiredConfigWebUri("discovery_uri"); - } - - mHttpsRequired = mConfigJson.optBoolean("https_required", true); - } - - @Nullable - String getConfigString(String propName) { - String value = mConfigJson.optString(propName); - if (value == null) { - return null; - } - - value = value.trim(); - if (TextUtils.isEmpty(value)) { - return null; - } - - return value; - } - - @NonNull - private String getRequiredConfigString(String propName) - throws InvalidConfigurationException { - String value = getConfigString(propName); - if (value == null) { - throw new InvalidConfigurationException( - propName + " is required but not specified in the configuration"); - } - - return value; - } - - @NonNull - Uri getRequiredConfigUri(String propName) - throws InvalidConfigurationException { - String uriStr = getRequiredConfigString(propName); - Uri uri; - try { - uri = Uri.parse(uriStr); - } catch (Throwable ex) { - throw new InvalidConfigurationException(propName + " could not be parsed", ex); - } - - if (!uri.isHierarchical() || !uri.isAbsolute()) { - throw new InvalidConfigurationException( - propName + " must be hierarchical and absolute"); - } - - if (!TextUtils.isEmpty(uri.getEncodedUserInfo())) { - throw new InvalidConfigurationException(propName + " must not have user info"); - } - - if (!TextUtils.isEmpty(uri.getEncodedQuery())) { - throw new InvalidConfigurationException(propName + " must not have query parameters"); - } - - if (!TextUtils.isEmpty(uri.getEncodedFragment())) { - throw new InvalidConfigurationException(propName + " must not have a fragment"); - } - - return uri; - } - - Uri getRequiredConfigWebUri(String propName) - throws InvalidConfigurationException { - Uri uri = getRequiredConfigUri(propName); - String scheme = uri.getScheme(); - if (TextUtils.isEmpty(scheme) || !("http".equals(scheme) || "https".equals(scheme))) { - throw new InvalidConfigurationException( - propName + " must have an http or https scheme"); - } - - return uri; - } - - private boolean isRedirectUriRegistered() { - // ensure that the redirect URI declared in the configuration is handled by some activity - // in the app, by querying the package manager speculatively - Intent redirectIntent = new Intent(); - redirectIntent.setPackage(mContext.getPackageName()); - redirectIntent.setAction(Intent.ACTION_VIEW); - redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE); - redirectIntent.setData(mRedirectUri); - - return !mContext.getPackageManager().queryIntentActivities(redirectIntent, 0).isEmpty(); - } - - public static final class InvalidConfigurationException extends Exception { - InvalidConfigurationException(String reason) { - super(reason); - } - - InvalidConfigurationException(String reason, Throwable cause) { - super(reason, cause); - } - } -} diff --git a/app/src/com/jc/jcfw/appauth/JConfiguration.kt b/app/src/com/jc/jcfw/appauth/JConfiguration.kt new file mode 100644 index 0000000..f53c834 --- /dev/null +++ b/app/src/com/jc/jcfw/appauth/JConfiguration.kt @@ -0,0 +1,223 @@ +package com.jc.jcfw.appauth + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.net.Uri +import android.text.TextUtils +import com.ctf.games.release.R +import net.openid.appauth.connectivity.ConnectionBuilder +import net.openid.appauth.connectivity.DefaultConnectionBuilder +import okio.Buffer +import okio.buffer +import okio.source +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.lang.ref.WeakReference +import java.nio.charset.StandardCharsets + +class JConfiguration(private val mContext: Context) { + private val mPrefs: SharedPreferences + private val mResources: Resources + private var mConfigJson: JSONObject? = null + private var mConfigHash: String? = null + + /** + * Returns a description of the configuration error, if the configuration is invalid. + */ + var configurationError: String? = null + var clientId: String? = null + private set + private var mScope: String? = null + private var mRedirectUri: Uri? = null + var endSessionRedirectUri: Uri? = null + private set + var discoveryUri: Uri? = null + private set + var authEndpointUri: Uri? = null + private set + var tokenEndpointUri: Uri? = null + private set + var endSessionEndpoint: Uri? = null + private set + var registrationEndpointUri: Uri? = null + private set + var userInfoEndpointUri: Uri? = null + private set + var isHttpsRequired = false + private set + + init { + mPrefs = mContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + mResources = mContext.resources + try { + readConfiguration() + } catch (ex: InvalidConfigurationException) { + configurationError = ex.message + } + } + + /** + * Indicates whether the configuration has changed from the last known valid state. + */ + fun hasConfigurationChanged(): Boolean { + val lastHash = lastKnownConfigHash + return mConfigHash != lastHash + } + + /** + * Indicates whether the current configuration is valid. + */ + val isValid: Boolean + get() = configurationError == null + + /** + * Indicates that the current configuration should be accepted as the "last known valid" + * configuration. + */ + fun acceptConfiguration() { + mPrefs.edit().putString(KEY_LAST_HASH, mConfigHash).apply() + } + + val scope: String + get() = mScope!! + val redirectUri: Uri + get() = mRedirectUri!! + val connectionBuilder: ConnectionBuilder + get() = DefaultConnectionBuilder.INSTANCE + private val lastKnownConfigHash: String? + private get() = mPrefs.getString(KEY_LAST_HASH, null) + + @Throws(InvalidConfigurationException::class) + private fun readConfiguration() { + val configSource = mResources.openRawResource(R.raw.auth_config).source().buffer() + val configData = Buffer() + mConfigJson = try { + configSource.readAll(configData) + JSONObject(configData.readString(StandardCharsets.UTF_8)) + } catch (ex: IOException) { + throw InvalidConfigurationException( + "Failed to read configuration: " + ex.message + ) + } catch (ex: JSONException) { + throw InvalidConfigurationException( + "Unable to parse configuration: " + ex.message + ) + } + mConfigHash = configData.sha256().base64() + clientId = getConfigString("client_id") + mScope = getRequiredConfigString("authorization_scope") + mRedirectUri = getRequiredConfigUri("redirect_uri") + endSessionRedirectUri = getRequiredConfigUri("end_session_redirect_uri") + if (!isRedirectUriRegistered) { + throw InvalidConfigurationException( + "redirect_uri is not handled by any activity in this app! " + + "Ensure that the appAuthRedirectScheme in your build.gradle file " + + "is correctly configured, or that an appropriate intent filter " + + "exists in your app manifest." + ) + } + if (getConfigString("discovery_uri") == null) { + authEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri") + tokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri") + userInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri") + endSessionEndpoint = getRequiredConfigUri("end_session_endpoint") + if (clientId == null) { + registrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri") + } + } else { + discoveryUri = getRequiredConfigWebUri("discovery_uri") + } + isHttpsRequired = mConfigJson!!.optBoolean("https_required", true) + } + + fun getConfigString(propName: String?): String? { + var value = mConfigJson!!.optString(propName) ?: return null + value = value.trim { it <= ' ' } + return if (TextUtils.isEmpty(value)) { + null + } else value + } + + @Throws(InvalidConfigurationException::class) + private fun getRequiredConfigString(propName: String): String { + return getConfigString(propName) + ?: throw InvalidConfigurationException( + "$propName is required but not specified in the configuration" + ) + } + + @Throws(InvalidConfigurationException::class) + fun getRequiredConfigUri(propName: String): Uri { + val uriStr = getRequiredConfigString(propName) + val uri: Uri + uri = try { + Uri.parse(uriStr) + } catch (ex: Throwable) { + throw InvalidConfigurationException("$propName could not be parsed", ex) + } + if (!uri.isHierarchical || !uri.isAbsolute) { + throw InvalidConfigurationException( + "$propName must be hierarchical and absolute" + ) + } + if (!TextUtils.isEmpty(uri.encodedUserInfo)) { + throw InvalidConfigurationException("$propName must not have user info") + } + if (!TextUtils.isEmpty(uri.encodedQuery)) { + throw InvalidConfigurationException("$propName must not have query parameters") + } + if (!TextUtils.isEmpty(uri.encodedFragment)) { + throw InvalidConfigurationException("$propName must not have a fragment") + } + return uri + } + + @Throws(InvalidConfigurationException::class) + fun getRequiredConfigWebUri(propName: String): Uri { + val uri = getRequiredConfigUri(propName) + val scheme = uri.scheme + if (TextUtils.isEmpty(scheme) || !("http" == scheme || "https" == scheme)) { + throw InvalidConfigurationException( + "$propName must have an http or https scheme" + ) + } + return uri + } + + // ensure that the redirect URI declared in the configuration is handled by some activity + // in the app, by querying the package manager speculatively + private val isRedirectUriRegistered: Boolean + private get() { + // ensure that the redirect URI declared in the configuration is handled by some activity + // in the app, by querying the package manager speculatively + val redirectIntent = Intent() + redirectIntent.setPackage(mContext.packageName) + redirectIntent.action = Intent.ACTION_VIEW + redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE) + redirectIntent.data = mRedirectUri + return !mContext.packageManager.queryIntentActivities(redirectIntent, 0).isEmpty() + } + + class InvalidConfigurationException : Exception { + internal constructor(reason: String?) : super(reason) {} + internal constructor(reason: String?, cause: Throwable?) : super(reason, cause) {} + } + + companion object { + private const val TAG = "Configuration" + private const val PREFS_NAME = "config" + private const val KEY_LAST_HASH = "lastHash" + private var sInstance = WeakReference(null) + fun getInstance(context: Context): JConfiguration { + var config = sInstance.get() + if (config == null) { + config = JConfiguration(context) + sInstance = WeakReference(config) + } + return config + } + } +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/security/BiometricHelper.java b/app/src/com/jc/jcfw/security/BiometricHelper.java deleted file mode 100644 index 9af56d0..0000000 --- a/app/src/com/jc/jcfw/security/BiometricHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.jc.jcfw.security; - -import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; -import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.biometric.BiometricPrompt; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; - -import com.ctf.games.release.R; - -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -public class BiometricHelper { - - public static final int ERROR_BIOMETRIC_FAIL = 100; - public static final int ERROR_BIOMETRIC_NO_CIPHER = 101; - private static final String TAG = BiometricHelper.class.getSimpleName(); - public static BiometricPrompt.PromptInfo createPromptInfo(Context context) { - BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.prompt_info_title)) // e.g. "Sign in" - .setSubtitle(context.getString(R.string.prompt_info_subtitle)) // e.g. "Biometric for My App" - .setDescription(context.getString(R.string.prompt_info_description)) // e.g. "Confirm biometric to continue" - .setConfirmationRequired(false) - .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) -// .setDeviceCredentialAllowed(true) // Allow PIN/pattern/password authentication. - // Also note that setDeviceCredentialAllowed and setNegativeButtonText are - // incompatible so that if you uncomment one you must comment out the other - .build(); - return promptInfo; - } - - public static BiometricPrompt createBiometricPrompt(FragmentActivity activity, Consumer func) { - Executor executor = ContextCompat.getMainExecutor(activity.getApplicationContext()); - return new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errcode, @NonNull CharSequence errString) { - super.onAuthenticationError(errcode, errString); - Log.i(TAG, "Authentication error: " + errcode +" | "+ errString); - func.accept(new BiometricResult(errcode, (String) errString)); - } - @Override - public void onAuthenticationFailed() { - super.onAuthenticationFailed(); - Log.i(TAG, "Authentication failed!"); - func.accept(new BiometricResult(ERROR_BIOMETRIC_FAIL, "Authentication failed")); - } - @Override - public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - Log.i(TAG, "Authentication succeeded!"); - func.accept(new BiometricResult(result.getCryptoObject().getCipher())); - } - }); - } -} diff --git a/app/src/com/jc/jcfw/security/BiometricHelper.kt b/app/src/com/jc/jcfw/security/BiometricHelper.kt new file mode 100644 index 0000000..0a64f49 --- /dev/null +++ b/app/src/com/jc/jcfw/security/BiometricHelper.kt @@ -0,0 +1,54 @@ +package com.jc.jcfw.security + +import android.content.Context +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import com.ctf.games.release.R +import java.util.function.Consumer + +object BiometricHelper { + const val ERROR_BIOMETRIC_FAIL = 100 + const val ERROR_BIOMETRIC_NO_CIPHER = 101 + private val TAG = BiometricHelper::class.java.simpleName + fun createPromptInfo(context: Context): PromptInfo { + return PromptInfo.Builder() + .setTitle(context.getString(R.string.prompt_info_title)) // e.g. "Sign in" + .setSubtitle(context.getString(R.string.prompt_info_subtitle)) // e.g. "Biometric for My App" + .setDescription(context.getString(R.string.prompt_info_description)) // e.g. "Confirm biometric to continue" + .setConfirmationRequired(false) + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL) // .setDeviceCredentialAllowed(true) // Allow PIN/pattern/password authentication. + // Also note that setDeviceCredentialAllowed and setNegativeButtonText are + // incompatible so that if you uncomment one you must comment out the other + .build() + } + + fun createBiometricPrompt( + activity: FragmentActivity, + func: Consumer + ): BiometricPrompt { + val executor = ContextCompat.getMainExecutor(activity.applicationContext) + return BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errcode: Int, errString: CharSequence) { + super.onAuthenticationError(errcode, errString) + Log.i(TAG, "Authentication error: $errcode | $errString") + func.accept(BiometricResult(errcode, errString as String)) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.i(TAG, "Authentication failed!") + func.accept(BiometricResult(ERROR_BIOMETRIC_FAIL, "Authentication failed")) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.i(TAG, "Authentication succeeded!") + func.accept(BiometricResult(result.cryptoObject!!.cipher)) + } + }) + } +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/security/BiometricResult.java b/app/src/com/jc/jcfw/security/BiometricResult.java deleted file mode 100644 index 45b31a8..0000000 --- a/app/src/com/jc/jcfw/security/BiometricResult.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.jc.jcfw.security; - -import androidx.annotation.Nullable; - -import javax.crypto.Cipher; - -public class BiometricResult { - private Cipher cipher; - private boolean error; - private int errcode; - private String errmsg; - - public BiometricResult(Cipher cipher) { - if (null != cipher) { - this.cipher = cipher; - this.error = false; - } else { - this.error = true; - this.errcode = 101; - this.errmsg = "cipher is null"; - } - } - - public BiometricResult(int errcode, String errmsg) { - this.error = true; - this.errcode = errcode; - this.errmsg = errmsg; - } - @Nullable - public Cipher getCipher() { - return cipher; - } - - public void setCipher(Cipher cipher) { - this.cipher = cipher; - } - - public boolean isError() { - return error; - } - - public void setError(boolean error) { - this.error = error; - } - - public int getErrcode() { - return errcode; - } - - public void setErrcode(int errcode) { - this.errcode = errcode; - } - - public String getErrmsg() { - return errmsg; - } - - public void setErrmsg(String errmsg) { - this.errmsg = errmsg; - } -} diff --git a/app/src/com/jc/jcfw/security/BiometricResult.kt b/app/src/com/jc/jcfw/security/BiometricResult.kt new file mode 100644 index 0000000..46fa543 --- /dev/null +++ b/app/src/com/jc/jcfw/security/BiometricResult.kt @@ -0,0 +1,27 @@ +package com.jc.jcfw.security + +import javax.crypto.Cipher + +class BiometricResult { + var cipher: Cipher? = null + var isError = false + var errcode = 0 + var errmsg: String? = null + + constructor(cipher: Cipher?) { + if (null != cipher) { + this.cipher = cipher + isError = false + } else { + isError = true + errcode = 101 + errmsg = "cipher is null" + } + } + + constructor(errcode: Int, errmsg: String?) { + isError = true + this.errcode = errcode + this.errmsg = errmsg + } +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/security/CryptographyManager.java b/app/src/com/jc/jcfw/security/CryptographyManager.kt similarity index 59% rename from app/src/com/jc/jcfw/security/CryptographyManager.java rename to app/src/com/jc/jcfw/security/CryptographyManager.kt index 566eb16..271b961 100644 --- a/app/src/com/jc/jcfw/security/CryptographyManager.java +++ b/app/src/com/jc/jcfw/security/CryptographyManager.kt @@ -1,27 +1,27 @@ -package com.jc.jcfw.security; +package com.jc.jcfw.security -import javax.crypto.Cipher; +import javax.crypto.Cipher -public interface CryptographyManager { +interface CryptographyManager { /** * This method first gets or generates an instance of SecretKey and then initializes the Cipher * with the key. The secret key uses [ENCRYPT_MODE][Cipher.ENCRYPT_MODE] is used. */ - Cipher getInitializedCipherForEncryption(String keyName); + fun getInitializedCipherForEncryption(keyName: String?): Cipher? /** * This method first gets or generates an instance of SecretKey and then initializes the Cipher * with the key. The secret key uses [DECRYPT_MODE][Cipher.DECRYPT_MODE] is used. */ - Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector); + fun getInitializedCipherForDecryption(keyName: String?, initializationVector: ByteArray?): Cipher? /** * The Cipher created with [getInitializedCipherForEncryption] is used here */ - EncryptedData encryptData(String plaintext, Cipher cipher); + fun encryptData(plaintext: String?, cipher: Cipher?): EncryptedData? /** * The Cipher created with [getInitializedCipherForDecryption] is used here */ - String decryptData(byte[] ciphertext, Cipher cipher); -} + fun decryptData(ciphertext: ByteArray?, cipher: Cipher?): String? +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/security/CryptographyManagerImpl.java b/app/src/com/jc/jcfw/security/CryptographyManagerImpl.java deleted file mode 100644 index 856aa7f..0000000 --- a/app/src/com/jc/jcfw/security/CryptographyManagerImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.jc.jcfw.security; - -import static android.security.keystore.KeyProperties.BLOCK_MODE_CBC; -import static android.security.keystore.KeyProperties.ENCRYPTION_PADDING_PKCS7; - -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; -import android.util.Log; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class CryptographyManagerImpl implements CryptographyManager { - - @Override - public Cipher getInitializedCipherForEncryption(String keyName) { - Cipher cipher; - try { - cipher = getCipher(); - SecretKey secretKey = getOrCreateSecretKey(keyName); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - } catch (Exception e) { - throw new RuntimeException("Failed to initialize Cipher for encryption", e); - } - return cipher; - } - - @Override - public Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector) { - Cipher cipher; - try { - cipher = getCipher(); - SecretKey secretKey = getOrCreateSecretKey(keyName); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(initializationVector)); - } catch (Exception e) { - throw new RuntimeException("Failed to initialize Cipher for decryption", e); - } - return cipher; - } - - @Override - public EncryptedData encryptData(String plaintext, Cipher cipher) { - try { - byte[] ciphertext = cipher.doFinal(plaintext.getBytes(Charset.defaultCharset())); - return new EncryptedData(ciphertext, cipher.getIV()); - } catch (Exception e) { - throw new RuntimeException("Failed to encrypt data", e); - } - } - - @Override - public String decryptData(byte[] ciphertext, Cipher cipher) { - try { - byte[] plaintext = cipher.doFinal(ciphertext); - return new String(plaintext, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new RuntimeException("Failed to encrypt data", e); - } - } - - private Cipher getCipher() throws Exception { - return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" - + BLOCK_MODE_CBC + "/" - + ENCRYPTION_PADDING_PKCS7); - } - - private SecretKey getOrCreateSecretKey(String keyName) throws Exception { - String ANDROID_KEYSTORE = "AndroidKeyStore"; - KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); - keyStore.load(null); // Keystore must be loaded before it can be accessed - SecretKey existingKey = (SecretKey) keyStore.getKey(keyName, null); - if (existingKey != null) { - return existingKey; - } - - // if you reach here, then a new SecretKey must be generated for that keyName - KeyGenParameterSpec.Builder paramsBuilder = new KeyGenParameterSpec.Builder(keyName, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT); - paramsBuilder.setBlockModes(BLOCK_MODE_CBC); - paramsBuilder.setEncryptionPaddings(ENCRYPTION_PADDING_PKCS7); - int KEY_SIZE = 256; - paramsBuilder.setKeySize(KEY_SIZE); - paramsBuilder.setUserAuthenticationRequired(false); - paramsBuilder.setUserAuthenticationValidityDurationSeconds(30); - - KeyGenParameterSpec keyGenParams = paramsBuilder.build(); - KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, - ANDROID_KEYSTORE); - keyGenerator.init(keyGenParams); - return keyGenerator.generateKey(); - } -} diff --git a/app/src/com/jc/jcfw/security/CryptographyManagerImpl.kt b/app/src/com/jc/jcfw/security/CryptographyManagerImpl.kt new file mode 100644 index 0000000..b652470 --- /dev/null +++ b/app/src/com/jc/jcfw/security/CryptographyManagerImpl.kt @@ -0,0 +1,96 @@ +package com.jc.jcfw.security + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +class CryptographyManagerImpl : CryptographyManager { + override fun getInitializedCipherForEncryption(keyName: String?): Cipher? { + val cipher: Cipher + try { + cipher = this.cipher + val secretKey = getOrCreateSecretKey(keyName) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + } catch (e: Exception) { + throw RuntimeException("Failed to initialize Cipher for encryption", e) + } + return cipher + } + + override fun getInitializedCipherForDecryption( + keyName: String?, + initializationVector: ByteArray? + ): Cipher? { + val cipher: Cipher + try { + cipher = this.cipher + val secretKey = getOrCreateSecretKey(keyName) + cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(initializationVector)) + } catch (e: Exception) { + throw RuntimeException("Failed to initialize Cipher for decryption", e) + } + return cipher + } + + override fun encryptData(plaintext: String?, cipher: Cipher?): EncryptedData? { + return try { + val ciphertext = cipher!!.doFinal(plaintext!!.toByteArray(Charset.defaultCharset())) + EncryptedData(ciphertext, cipher.iv) + } catch (e: Exception) { + throw RuntimeException("Failed to encrypt data", e) + } + } + + override fun decryptData(ciphertext: ByteArray?, cipher: Cipher?): String? { + return try { + val plaintext = cipher!!.doFinal(ciphertext) + String(plaintext, StandardCharsets.UTF_8) + } catch (e: Exception) { + throw RuntimeException("Failed to encrypt data", e) + } + } + + @get:Throws(Exception::class) + private val cipher: Cipher + private get() = Cipher.getInstance( + KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7 + ) + + @Throws(Exception::class) + private fun getOrCreateSecretKey(keyName: String?): SecretKey { + val ANDROID_KEYSTORE = "AndroidKeyStore" + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + keyStore.load(null) // Keystore must be loaded before it can be accessed + val existingKey = keyStore.getKey(keyName, null) as SecretKey + if (existingKey != null) { + return existingKey + } + + // if you reach here, then a new SecretKey must be generated for that keyName + val paramsBuilder = KeyGenParameterSpec.Builder( + keyName!!, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + paramsBuilder.setBlockModes(KeyProperties.BLOCK_MODE_CBC) + paramsBuilder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + val KEY_SIZE = 256 + paramsBuilder.setKeySize(KEY_SIZE) + paramsBuilder.setUserAuthenticationRequired(false) + paramsBuilder.setUserAuthenticationValidityDurationSeconds(30) + val keyGenParams = paramsBuilder.build() + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + ANDROID_KEYSTORE + ) + keyGenerator.init(keyGenParams) + return keyGenerator.generateKey() + } +} \ No newline at end of file diff --git a/app/src/com/jc/jcfw/security/EncryptedData.java b/app/src/com/jc/jcfw/security/EncryptedData.java deleted file mode 100644 index 5e4fe6d..0000000 --- a/app/src/com/jc/jcfw/security/EncryptedData.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.jc.jcfw.security; - -public class EncryptedData { - private final byte[] ciphertext; - private final byte[] initializationVector; - - public EncryptedData(byte[] ciphertext, byte[] initializationVector) { - this.ciphertext = ciphertext; - this.initializationVector = initializationVector; - } - - public byte[] getCiphertext() { - return ciphertext; - } - - public byte[] getInitializationVector() { - return initializationVector; - } -} diff --git a/app/src/com/jc/jcfw/security/EncryptedData.kt b/app/src/com/jc/jcfw/security/EncryptedData.kt new file mode 100644 index 0000000..0bbb325 --- /dev/null +++ b/app/src/com/jc/jcfw/security/EncryptedData.kt @@ -0,0 +1,3 @@ +package com.jc.jcfw.security + +class EncryptedData(val ciphertext: ByteArray, val initializationVector: ByteArray) \ No newline at end of file