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