diff --git a/core/src/main/cpp/main.c b/core/src/main/cpp/main.c index 20c5a46b83..61e431c8a1 100644 --- a/core/src/main/cpp/main.c +++ b/core/src/main/cpp/main.c @@ -253,6 +253,73 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeSetAgeSecretKey(JNIEnv *env setAgeSecretKey(_key); } +JNIEXPORT jstring JNICALL +Java_com_github_kr328_clash_core_bridge_Bridge_nativeGenX25519KeyPair(JNIEnv *env, jobject thiz) { + TRACE_METHOD(); + + scoped_string response = genX25519KeyPair(); + + if (response == NULL) + return NULL; + + return new_string(response); +} + +JNIEXPORT jstring JNICALL +Java_com_github_kr328_clash_core_bridge_Bridge_nativeGenHybridKeyPair(JNIEnv *env, jobject thiz) { + TRACE_METHOD(); + + scoped_string response = genHybridKeyPair(); + + if (response == NULL) + return NULL; + + return new_string(response); +} + +JNIEXPORT jboolean JNICALL +Java_com_github_kr328_clash_core_bridge_Bridge_nativeVeritySecretKeys(JNIEnv *env, jobject thiz, + jstring secret_keys) { + TRACE_METHOD(); + + if (secret_keys == NULL) + return 0; + + scoped_string _secret_keys = get_string(secret_keys); + + return (jboolean) veritySecretKeys(_secret_keys); +} + +JNIEXPORT jstring JNICALL +Java_com_github_kr328_clash_core_bridge_Bridge_nativeToPublicKeys(JNIEnv *env, jobject thiz, + jstring secret_keys) { + TRACE_METHOD(); + + if (secret_keys == NULL) + return NULL; + + scoped_string _secret_keys = get_string(secret_keys); + scoped_string response = toPublicKeys(_secret_keys); + + if (response == NULL) + return NULL; + + return new_string(response); +} + +JNIEXPORT jboolean JNICALL +Java_com_github_kr328_clash_core_bridge_Bridge_nativeVerityPublicKeys(JNIEnv *env, jobject thiz, + jstring public_keys) { + TRACE_METHOD(); + + if (public_keys == NULL) + return 0; + + scoped_string _public_keys = get_string(public_keys); + + return (jboolean) verityPublicKeys(_public_keys); +} + JNIEXPORT jstring JNICALL Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryProviders(JNIEnv *env, jobject thiz) { TRACE_METHOD(); diff --git a/core/src/main/golang/native/config.go b/core/src/main/golang/native/config.go index 6338bd138c..43e099b683 100644 --- a/core/src/main/golang/native/config.go +++ b/core/src/main/golang/native/config.go @@ -18,6 +18,11 @@ func (r *remoteValidCallback) reportStatus(json string) { C.fetch_report(r.callback, marshalString(json)) } +type ageKeyPair struct { + SecretKey string `json:"secretKey"` + PublicKey string `json:"publicKey"` +} + //export fetchAndValid func fetchAndValid(callback unsafe.Pointer, path, url C.c_string, force C.int) { go func(path, url string, callback unsafe.Pointer) { @@ -71,3 +76,51 @@ func setAgeSecretKey(key C.c_string) { k := C.GoString(key) config.SetGlobalSecretKeys(k) } + +//export genX25519KeyPair +func genX25519KeyPair() *C.char { + secretKey, publicKey, err := config.GenX25519KeyPair() + if err != nil { + return nil + } + + return marshalJson(ageKeyPair{SecretKey: secretKey, PublicKey: publicKey}) +} + +//export genHybridKeyPair +func genHybridKeyPair() *C.char { + secretKey, publicKey, err := config.GenHybridKeyPair() + if err != nil { + return nil + } + + return marshalJson(ageKeyPair{SecretKey: secretKey, PublicKey: publicKey}) +} + +//export veritySecretKeys +func veritySecretKeys(secretKeys C.c_string) C.int { + if config.VeritySecretKeys(C.GoString(secretKeys)) != nil { + return 0 + } + + return 1 +} + +//export toPublicKeys +func toPublicKeys(secretKeys C.c_string) *C.char { + publicKeys, err := config.ToPublicKeys(C.GoString(secretKeys)) + if err != nil { + return nil + } + + return marshalJson(publicKeys) +} + +//export verityPublicKeys +func verityPublicKeys(publicKeys C.c_string) C.int { + if config.VerityPublicKeys(C.GoString(publicKeys)) != nil { + return 0 + } + + return 1 +} diff --git a/core/src/main/golang/native/config/age.go b/core/src/main/golang/native/config/age.go index 5348dd8370..7b8b6d716b 100644 --- a/core/src/main/golang/native/config/age.go +++ b/core/src/main/golang/native/config/age.go @@ -5,3 +5,23 @@ import "github.com/metacubex/mihomo/component/age" func SetGlobalSecretKeys(secretKeys ...string) { age.SetGlobalSecretKeys(secretKeys...) } + +func GenX25519KeyPair() (secretKey string, publicKey string, err error) { + return age.GenX25519KeyPair() +} + +func GenHybridKeyPair() (secretKey string, publicKey string, err error) { + return age.GenHybridKeyPair() +} + +func ToPublicKeys(secretKeys ...string) (publicKeys []string, err error) { + return age.ToPublicKeys(secretKeys...) +} + +func VeritySecretKeys(secretKeys ...string) error { + return age.VeritySecretKeys(secretKeys...) +} + +func VerityPublicKeys(publicKeys ...string) error { + return age.VerityPublicKeys(publicKeys...) +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index bc87822325..c3a2be684a 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -6,6 +6,8 @@ import com.github.kr328.clash.core.util.parseInetSocketAddress import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonPrimitive @@ -229,4 +231,30 @@ object Clash { fun setAgeSecretKey(key: String?) { Bridge.nativeSetAgeSecretKey(key) } + + fun genX25519KeyPair(): AgeKeyPair { + return parseAgeKeyPair(checkNotNull(Bridge.nativeGenX25519KeyPair())) + } + + fun genHybridKeyPair(): AgeKeyPair { + return parseAgeKeyPair(checkNotNull(Bridge.nativeGenHybridKeyPair())) + } + + fun veritySecretKeys(vararg secretKeys: String): Boolean { + return Bridge.nativeVeritySecretKeys(secretKeys.firstOrNull() ?: "") + } + + fun toPublicKeys(vararg secretKeys: String): List { + return Bridge.nativeToPublicKeys(secretKeys.firstOrNull() ?: "") + ?.let { Json.Default.decodeFromString(ListSerializer(String.serializer()), it) } + ?: emptyList() + } + + fun verityPublicKeys(vararg publicKeys: String): Boolean { + return Bridge.nativeVerityPublicKeys(publicKeys.firstOrNull() ?: "") + } + + private fun parseAgeKeyPair(value: String): AgeKeyPair { + return Json.Default.decodeFromString(AgeKeyPair.serializer(), value) + } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt index 6007ff1d0d..fa3b9247ef 100644 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt +++ b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt @@ -51,6 +51,11 @@ object Bridge { external fun nativeCoreVersion(): String external fun nativeSetAgeSecretKey(key: String?) + external fun nativeGenX25519KeyPair(): String? + external fun nativeGenHybridKeyPair(): String? + external fun nativeVeritySecretKeys(secretKeys: String): Boolean + external fun nativeToPublicKeys(secretKeys: String): String? + external fun nativeVerityPublicKeys(publicKeys: String): Boolean private external fun nativeInit(home: String, versionName: String, sdkVersion: Int) diff --git a/core/src/main/java/com/github/kr328/clash/core/model/AgeKeyPair.kt b/core/src/main/java/com/github/kr328/clash/core/model/AgeKeyPair.kt new file mode 100644 index 0000000000..6b594e2783 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/AgeKeyPair.kt @@ -0,0 +1,9 @@ +package com.github.kr328.clash.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AgeKeyPair( + val secretKey: String, + val publicKey: String +) diff --git a/design/src/main/java/com/github/kr328/clash/design/MetaFeatureSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/MetaFeatureSettingsDesign.kt index b24a1bb90b..d2b634f1d4 100644 --- a/design/src/main/java/com/github/kr328/clash/design/MetaFeatureSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/MetaFeatureSettingsDesign.kt @@ -1,12 +1,21 @@ package com.github.kr328.clash.design +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.view.View +import androidx.core.content.getSystemService +import androidx.core.widget.doOnTextChanged +import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.model.ConfigurationOverride import com.github.kr328.clash.design.databinding.DesignSettingsMetaFeatureBinding +import com.github.kr328.clash.design.databinding.DialogAgeKeyHelperBinding import com.github.kr328.clash.design.preference.* +import com.github.kr328.clash.design.ui.ToastDuration import com.github.kr328.clash.design.util.* import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -63,6 +72,26 @@ class MetaFeatureSettingsDesign( ) val screen = preferenceScreen(context) { + category(R.string.age_key_category) + + clickable( + title = R.string.age_key_type_x25519, + summary = R.string.age_key_generate_summary, + ) { + clicked { + requestAgeKeyHelper(hybrid = false) + } + } + + clickable( + title = R.string.age_key_type_hybrid, + summary = R.string.age_key_generate_summary, + ) { + clicked { + requestAgeKeyHelper(hybrid = true) + } + } + category(R.string.settings) selectableList( @@ -317,6 +346,75 @@ class MetaFeatureSettingsDesign( binding.content.addView(screen.root) } + private fun requestAgeKeyHelper(hybrid: Boolean) { + launch(Dispatchers.Main) { + val binding = DialogAgeKeyHelperBinding + .inflate(context.layoutInflater, context.root, false) + val dialog = MaterialAlertDialogBuilder(context) + .setTitle(if (hybrid) R.string.age_key_type_hybrid else R.string.age_key_type_x25519) + .setView(binding.root) + .create() + + fun copy(label: String, value: String) { + if (value.isBlank()) + return + + val data = ClipData.newPlainText(label, value) + context.getSystemService()?.setPrimaryClip(data) + + launch { showToast(R.string.copied, ToastDuration.Short) } + } + + fun patchSecretKeyState() { + val secretKey = binding.secretKeyView.text?.toString() ?: "" + val valid = secretKey.isBlank() || Clash.veritySecretKeys(secretKey) + + binding.secretKeyLayout.error = if (valid) null else context.getText(R.string.age_secret_key_error) + } + + fun patchPublicKeyState() { + val publicKey = binding.publicKeyView.text?.toString() ?: "" + val valid = publicKey.isBlank() || Clash.verityPublicKeys(publicKey) + + binding.publicKeyLayout.error = if (valid) null else context.getText(R.string.age_public_key_error) + } + + dialog.setOnShowListener { + binding.secretKeyView.doOnTextChanged { _, _, _, _ -> patchSecretKeyState() } + binding.publicKeyView.doOnTextChanged { _, _, _, _ -> patchPublicKeyState() } + + binding.generateView.setOnClickListener { + val keyPair = if (hybrid) { + Clash.genHybridKeyPair() + } else { + Clash.genX25519KeyPair() + } + + binding.secretKeyView.setText(keyPair.secretKey) + binding.publicKeyView.setText(keyPair.publicKey) + } + + binding.toPublicKeyView.setOnClickListener { + val publicKey = Clash.toPublicKeys(binding.secretKeyView.text?.toString() ?: "") + .firstOrNull() + ?: "" + + binding.publicKeyView.setText(publicKey) + } + + binding.copySecretKeyView.setOnClickListener { + copy("age_secret_key", binding.secretKeyView.text?.toString() ?: "") + } + + binding.copyPublicKeyView.setOnClickListener { + copy("age_public_key", binding.publicKeyView.text?.toString() ?: "") + } + } + + dialog.show() + } + } + fun requestClear() { requests.trySend(Request.ResetOverride) } diff --git a/design/src/main/java/com/github/kr328/clash/design/preference/Clickable.kt b/design/src/main/java/com/github/kr328/clash/design/preference/Clickable.kt index 5e2d9a0775..c9f594c6af 100644 --- a/design/src/main/java/com/github/kr328/clash/design/preference/Clickable.kt +++ b/design/src/main/java/com/github/kr328/clash/design/preference/Clickable.kt @@ -31,6 +31,7 @@ fun PreferenceScreen.clickable( get() = binding.iconView.background set(value) { binding.iconView.background = value + binding.iconView.visibility = if (value == null) View.GONE else View.VISIBLE } override var title: CharSequence get() = binding.titleView.text diff --git a/design/src/main/java/com/github/kr328/clash/design/util/Validator.kt b/design/src/main/java/com/github/kr328/clash/design/util/Validator.kt index 8fa8723251..8b316d57bb 100644 --- a/design/src/main/java/com/github/kr328/clash/design/util/Validator.kt +++ b/design/src/main/java/com/github/kr328/clash/design/util/Validator.kt @@ -1,6 +1,7 @@ package com.github.kr328.clash.design.util import com.github.kr328.clash.common.util.PatternFileName +import com.github.kr328.clash.core.Clash typealias Validator = (String) -> Boolean @@ -25,5 +26,5 @@ val ValidatorAutoUpdateInterval: Validator = { } val ValidatorAgeSecretKey: Validator = { - it.isEmpty() || it.startsWith("AGE-SECRET-KEY-", ignoreCase = true) + it.isEmpty() || Clash.veritySecretKeys(it) } \ No newline at end of file diff --git a/design/src/main/res/layout/dialog_age_key_helper.xml b/design/src/main/res/layout/dialog_age_key_helper.xml new file mode 100644 index 0000000000..181c3b08f5 --- /dev/null +++ b/design/src/main/res/layout/dialog_age_key_helper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + +