diff --git a/kernel/apk_sign.c b/kernel/apk_sign.c index ba8b73f2..d18cfdee 100644 --- a/kernel/apk_sign.c +++ b/kernel/apk_sign.c @@ -4,6 +4,7 @@ #include #include #include +#include #ifdef CONFIG_KSU_DEBUG #include #endif @@ -17,13 +18,295 @@ #include "apk_sign.h" #include "klog.h" // IWYU pragma: keep #include "kernel_compat.h" +#include "manager_sign.h" +// Expected sizes and hashes for various APK signatures +#define DYNAMIC_SIGN_FILE_MAGIC 0x7f445347 // 'DSG', u32 +#define DYNAMIC_SIGN_FILE_VERSION 1 // u32 +#define KERNEL_SU_DYNAMIC_SIGN "/data/adb/ksu/.dynamic_sign" + +static struct dynamic_sign_config dynamic_sign = { + .size = 0x300, + .hash = "0000000000000000000000000000000000000000000000000000000000000000", + .is_set = 0 +}; + +static DEFINE_SPINLOCK(dynamic_sign_lock); +static struct work_struct ksu_save_dynamic_sign_work; +static struct work_struct ksu_load_dynamic_sign_work; +static struct work_struct ksu_clear_dynamic_sign_work; + +static void do_save_dynamic_sign(struct work_struct *work) +{ + u32 magic = DYNAMIC_SIGN_FILE_MAGIC; + u32 version = DYNAMIC_SIGN_FILE_VERSION; + struct dynamic_sign_config config_to_save; + loff_t off = 0; + unsigned long flags; + struct file *fp; + + spin_lock_irqsave(&dynamic_sign_lock, flags); + config_to_save = dynamic_sign; + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + + if (!config_to_save.is_set) { + pr_info("Dynamic sign config not set, skipping save\n"); + return; + } + + fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_SIGN, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (IS_ERR(fp)) { + pr_err("save_dynamic_sign create file failed: %ld\n", PTR_ERR(fp)); + return; + } + + if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { + pr_err("save_dynamic_sign write magic failed.\n"); + goto exit; + } + + if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + pr_err("save_dynamic_sign write version failed.\n"); + goto exit; + } + + if (ksu_kernel_write_compat(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) { + pr_err("save_dynamic_sign write config failed.\n"); + goto exit; + } + + pr_info("Dynamic sign config saved successfully\n"); + +exit: + filp_close(fp, 0); +} + +// Loading dynamic signatures from persistent storage +static void do_load_dynamic_sign(struct work_struct *work) +{ + loff_t off = 0; + ssize_t ret = 0; + struct file *fp = NULL; + u32 magic; + u32 version; + struct dynamic_sign_config loaded_config; + unsigned long flags; + int i; + + fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_SIGN, O_RDONLY, 0); + if (IS_ERR(fp)) { + if (PTR_ERR(fp) == -ENOENT) { + pr_info("No saved dynamic sign config found\n"); + } else { + pr_err("load_dynamic_sign open file failed: %ld\n", PTR_ERR(fp)); + } + return; + } + + if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic) || + magic != DYNAMIC_SIGN_FILE_MAGIC) { + pr_err("dynamic sign file invalid magic: %x!\n", magic); + goto exit; + } + + if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + pr_err("dynamic sign read version failed\n"); + goto exit; + } + + pr_info("dynamic sign file version: %d\n", version); + + ret = ksu_kernel_read_compat(fp, &loaded_config, sizeof(loaded_config), &off); + if (ret <= 0) { + pr_info("load_dynamic_sign read err: %zd\n", ret); + goto exit; + } + + if (ret != sizeof(loaded_config)) { + pr_err("load_dynamic_sign read incomplete config: %zd/%zu\n", ret, sizeof(loaded_config)); + goto exit; + } + + if (loaded_config.size < 0x100 || loaded_config.size > 0x1000) { + pr_err("Invalid saved config size: 0x%x\n", loaded_config.size); + goto exit; + } + + if (strlen(loaded_config.hash) != 64) { + pr_err("Invalid saved config hash length: %zu\n", strlen(loaded_config.hash)); + goto exit; + } + + for (i = 0; i < 64; i++) { + char c = loaded_config.hash[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { + pr_err("Invalid saved config hash character at position %d: %c\n", i, c); + goto exit; + } + } + + spin_lock_irqsave(&dynamic_sign_lock, flags); + dynamic_sign = loaded_config; + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + + pr_info("Dynamic sign config loaded: size=0x%x, hash=%.16s...\n", + loaded_config.size, loaded_config.hash); + +exit: + filp_close(fp, 0); +} + +static bool persistent_dynamic_sign(void) +{ + return ksu_queue_work(&ksu_save_dynamic_sign_work); +} + +// Clear dynamic sign config file using the same method as do_save_dynamic_sign +static void do_clear_dynamic_sign_file(struct work_struct *work) +{ + loff_t off = 0; + struct file *fp; + char zero_buffer[512]; + + memset(zero_buffer, 0, sizeof(zero_buffer)); + + fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_SIGN, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (IS_ERR(fp)) { + pr_err("clear_dynamic_sign create file failed: %ld\n", PTR_ERR(fp)); + return; + } + + // Write null bytes to overwrite the file content + if (ksu_kernel_write_compat(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) { + pr_err("clear_dynamic_sign write null bytes failed.\n"); + } else { + pr_info("Dynamic sign config file cleared successfully\n"); + } + + filp_close(fp, 0); +} + +static bool clear_dynamic_sign_file(void) +{ + return ksu_queue_work(&ksu_clear_dynamic_sign_work); +} + +int ksu_handle_dynamic_sign(struct dynamic_sign_user_config *config) +{ + unsigned long flags; + int ret = 0; + int i; + + if (!config) { + return -EINVAL; + } + + switch (config->operation) { + case DYNAMIC_SIGN_OP_SET: + if (config->size < 0x100 || config->size > 0x1000) { + pr_err("invalid size: 0x%x\n", config->size); + return -EINVAL; + } + + if (strlen(config->hash) != 64) { + pr_err("invalid hash length: %zu\n", strlen(config->hash)); + return -EINVAL; + } + + for (i = 0; i < 64; i++) { + char c = config->hash[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { + pr_err("invalid hash character at position %d: %c\n", i, c); + return -EINVAL; + } + } + + spin_lock_irqsave(&dynamic_sign_lock, flags); + dynamic_sign.size = config->size; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(dynamic_sign.hash, config->hash, sizeof(dynamic_sign.hash)); +#else + strlcpy(dynamic_sign.hash, config->hash, sizeof(dynamic_sign.hash)); +#endif + dynamic_sign.is_set = 1; + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + + persistent_dynamic_sign(); + pr_info("dynamic sign updated: size=0x%x, hash=%.16s...\n", config->size, config->hash); + break; + + case DYNAMIC_SIGN_OP_GET: + // Getting Dynamic Signatures + spin_lock_irqsave(&dynamic_sign_lock, flags); + if (dynamic_sign.is_set) { + config->size = dynamic_sign.size; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(config->hash, dynamic_sign.hash, sizeof(config->hash)); +#else + strlcpy(config->hash, dynamic_sign.hash, sizeof(config->hash)); +#endif + ret = 0; + } else { + ret = -ENODATA; + } + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + break; + + case DYNAMIC_SIGN_OP_CLEAR: + // Clearing dynamic signatures + spin_lock_irqsave(&dynamic_sign_lock, flags); + dynamic_sign.size = 0x300; + strcpy(dynamic_sign.hash, "0000000000000000000000000000000000000000000000000000000000000000"); + dynamic_sign.is_set = 0; + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + + // Clear file using the same method as save + clear_dynamic_sign_file(); + + pr_info("Dynamic sign config cleared\n"); + break; + + default: + pr_err("Invalid dynamic sign operation: %d\n", config->operation); + return -EINVAL; + } + + return ret; +} + +bool ksu_load_dynamic_sign(void) +{ + return ksu_queue_work(&ksu_load_dynamic_sign_work); +} + +void ksu_dynamic_sign_init(void) +{ + INIT_WORK(&ksu_save_dynamic_sign_work, do_save_dynamic_sign); + INIT_WORK(&ksu_load_dynamic_sign_work, do_load_dynamic_sign); + INIT_WORK(&ksu_clear_dynamic_sign_work, do_clear_dynamic_sign_file); + pr_info("Dynamic sign initialized with persistent storage\n"); +} + +void ksu_dynamic_sign_exit(void) +{ + do_save_dynamic_sign(NULL); + pr_info("Dynamic sign exited with persistent storage\n"); +} struct sdesc { struct shash_desc shash; char ctx[]; }; +static struct apk_sign_key { + unsigned size; + const char *sha256; +} apk_sign_keys[] = { + {EXPECTED_SIZE, EXPECTED_HASH}, + {EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU + {EXPECTED_SIZE_OTHER, EXPECTED_HASH_OTHER}, // Dynamic Sign +}; + static struct sdesc *init_sdesc(struct crypto_shash *alg) { struct sdesc *sdesc; @@ -71,9 +354,11 @@ static int ksu_sha256(const unsigned char *data, unsigned int datalen, return ret; } -static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, - unsigned expected_size, const char *expected_sha256) +static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset) { + int i; + struct apk_sign_key sign_key; + ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length @@ -89,7 +374,21 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length *offset += 0x4 * 2; - if (*size4 == expected_size) { + for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) { + sign_key = apk_sign_keys[i]; + + if (i == 2) { + unsigned long flags; + spin_lock_irqsave(&dynamic_sign_lock, flags); + if (dynamic_sign.is_set) { + sign_key.size = dynamic_sign.size; + sign_key.sha256 = dynamic_sign.hash; + } + spin_unlock_irqrestore(&dynamic_sign_lock, flags); + } + + if (*size4 != sign_key.size) + continue; *offset += *size4; #define CERT_MAX_LENGTH 1024 @@ -110,8 +409,8 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); pr_info("sha256: %s, expected: %s\n", hash_str, - expected_sha256); - if (strcmp(expected_sha256, hash_str) == 0) { + sign_key.sha256); + if (strcmp(sign_key.sha256, hash_str) == 0) { return true; } } @@ -171,9 +470,7 @@ static bool has_v1_signature_file(struct file *fp) return false; } -static __always_inline bool check_v2_signature(char *path, - unsigned expected_size, - const char *expected_sha256) +static __always_inline bool check_v2_signature(char *path) { unsigned char buffer[0x11] = { 0 }; u32 size4; @@ -244,9 +541,7 @@ static __always_inline bool check_v2_signature(char *path, offset = 4; if (id == 0x7109871au) { v2_signing_blocks++; - v2_signing_valid = - check_block(fp, &size4, &pos, &offset, - expected_size, expected_sha256); + v2_signing_valid = check_block(fp, &size4, &pos, &offset); } else if (id == 0xf05368c0u) { // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 v3_signing_exist = true; @@ -316,5 +611,5 @@ module_param_cb(ksu_debug_manager_uid, &expected_size_ops, bool is_manager_apk(char *path) { - return check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH); + return check_v2_signature(path); } \ No newline at end of file diff --git a/kernel/apk_sign.h b/kernel/apk_sign.h index bed501c4..5681790d 100644 --- a/kernel/apk_sign.h +++ b/kernel/apk_sign.h @@ -2,7 +2,19 @@ #define __KSU_H_APK_V2_SIGN #include +#include "ksu.h" bool is_manager_apk(char *path); +struct dynamic_sign_config { + unsigned int size; + char hash[65]; + int is_set; +}; + +int ksu_handle_dynamic_sign(struct dynamic_sign_user_config *config); +void ksu_dynamic_sign_init(void); +void ksu_dynamic_sign_exit(void); +bool ksu_load_dynamic_sign(void); + #endif diff --git a/kernel/core_hook.c b/kernel/core_hook.c index 1dd6f652..5be9f2d5 100644 --- a/kernel/core_hook.c +++ b/kernel/core_hook.c @@ -334,6 +334,36 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, return 0; } + // Allow the root manager to configure dynamic signatures + if (arg2 == CMD_DYNAMIC_SIGN) { + if (!from_root && !from_manager) { + return 0; + } + + struct dynamic_sign_user_config config; + + if (copy_from_user(&config, (void __user *)arg3, sizeof(config))) { + pr_err("copy dynamic sign config failed\n"); + return 0; + } + + int ret = ksu_handle_dynamic_sign(&config); + + if (ret == 0 && config.operation == DYNAMIC_SIGN_OP_GET) { + if (copy_to_user((void __user *)arg3, &config, sizeof(config))) { + pr_err("copy dynamic sign config back failed\n"); + return 0; + } + } + + if (ret == 0) { + if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { + pr_err("dynamic_sign: prctl reply error\n"); + } + } + return 0; + } + if (arg2 == CMD_REPORT_EVENT) { if (!from_root) { return 0; @@ -345,6 +375,10 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, post_fs_data_lock = true; pr_info("post-fs-data triggered\n"); on_post_fs_data(); + // Initializing Dynamic Signatures + ksu_dynamic_sign_init(); + ksu_load_dynamic_sign(); + pr_info("Dynamic sign config loaded during post-fs-data\n"); } break; } diff --git a/kernel/core_hook.h b/kernel/core_hook.h index 616951e8..6ed328a0 100644 --- a/kernel/core_hook.h +++ b/kernel/core_hook.h @@ -2,6 +2,7 @@ #define __KSU_H_KSU_CORE #include +#include "apk_sign.h" void __init ksu_core_init(void); void ksu_core_exit(void); diff --git a/kernel/ksu.h b/kernel/ksu.h index 341c436f..2f234c1d 100644 --- a/kernel/ksu.h +++ b/kernel/ksu.h @@ -27,6 +27,7 @@ #define CMD_GET_FULL_VERSION 0xC0FFEE1A #define CMD_ENABLE_KPM 100 +#define CMD_DYNAMIC_SIGN 103 #define EVENT_POST_FS_DATA 1 #define EVENT_BOOT_COMPLETED 2 @@ -44,6 +45,16 @@ #endif #define KSU_FULL_VERSION_STRING 255 +#define DYNAMIC_SIGN_OP_SET 0 +#define DYNAMIC_SIGN_OP_GET 1 +#define DYNAMIC_SIGN_OP_CLEAR 2 + +struct dynamic_sign_user_config { + unsigned int operation; + unsigned int size; + char hash[65]; +}; + struct root_profile { int32_t uid; int32_t gid; diff --git a/kernel/manager_sign.h b/kernel/manager_sign.h new file mode 100644 index 00000000..b7a8e8c9 --- /dev/null +++ b/kernel/manager_sign.h @@ -0,0 +1,13 @@ +#ifndef MANAGER_SIGN_H +#define MANAGER_SIGN_H + +// ShirkNeko/SukiSU +#define EXPECTED_SIZE_SHIRKNEKO 0x35c +#define EXPECTED_HASH_SHIRKNEKO "947ae944f3de4ed4c21a7e4f7953ecf351bfa2b36239da37a34111ad29993eef" + +// Dynamic Sign +#define EXPECTED_SIZE_OTHER 0x300 +#define EXPECTED_HASH_OTHER "0000000000000000000000000000000000000000000000000000000000000000" + + +#endif /* MANAGER_SIGN_H */ \ No newline at end of file diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index bfd50141..2efdf314 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -351,4 +351,47 @@ NativeBridgeNP(getFullVersion, jstring) { char buff[255] = { 0 }; get_full_version((char *) &buff); return GetEnvironment()->NewStringUTF(env, buff); +} + +NativeBridge(setDynamicSign, jboolean, jint size, jstring hash) { + if (!hash) { + LogDebug("setDynamicSign: hash is null"); + return false; + } + + const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr); + bool result = set_dynamic_sign((unsigned int)size, chash); + GetEnvironment()->ReleaseStringUTFChars(env, hash, chash); + + LogDebug("setDynamicSign: size=0x%x, result=%d", size, result); + return result; +} + +NativeBridgeNP(getDynamicSign, jobject) { + struct dynamic_sign_user_config config; + bool result = get_dynamic_sign(&config); + + if (!result) { + LogDebug("getDynamicSign: failed to get dynamic sign config"); + return NULL; + } + + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicSignConfig"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); + jobject obj = GetEnvironment()->NewObject(env, cls, constructor); + + jfieldID sizeField = GetEnvironment()->GetFieldID(env, cls, "size", "I"); + jfieldID hashField = GetEnvironment()->GetFieldID(env, cls, "hash", "Ljava/lang/String;"); + + GetEnvironment()->SetIntField(env, obj, sizeField, (jint)config.size); + GetEnvironment()->SetObjectField(env, obj, hashField, GetEnvironment()->NewStringUTF(env, config.hash)); + + LogDebug("getDynamicSign: size=0x%x, hash=%.16s...", config.size, config.hash); + return obj; +} + +NativeBridgeNP(clearDynamicSign, jboolean) { + bool result = clear_dynamic_sign(); + LogDebug("clearDynamicSign: result=%d", result); + return result; } \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index cd28f2b1..ccd39319 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -36,6 +36,11 @@ #define CMD_ENABLE_KPM 100 #define CMD_HOOK_TYPE 101 #define CMD_GET_SUSFS_FEATURE_STATUS 102 +#define CMD_DYNAMIC_SIGN 103 + +#define DYNAMIC_SIGN_OP_SET 0 +#define DYNAMIC_SIGN_OP_GET 1 +#define DYNAMIC_SIGN_OP_CLEAR 2 static bool ksuctl(int cmd, void* arg1, void* arg2) { int32_t result = 0; @@ -140,3 +145,32 @@ bool get_susfs_feature_status(struct susfs_feature_status* status) { return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL); } + +bool set_dynamic_sign(unsigned int size, const char* hash) { + if (hash == NULL) { + return false; + } + + struct dynamic_sign_user_config config; + config.operation = DYNAMIC_SIGN_OP_SET; + config.size = size; + strncpy(config.hash, hash, sizeof(config.hash) - 1); + config.hash[sizeof(config.hash) - 1] = '\0'; + + return ksuctl(CMD_DYNAMIC_SIGN, &config, NULL); +} + +bool get_dynamic_sign(struct dynamic_sign_user_config* config) { + if (config == NULL) { + return false; + } + + config->operation = DYNAMIC_SIGN_OP_GET; + return ksuctl(CMD_DYNAMIC_SIGN, config, NULL); +} + +bool clear_dynamic_sign() { + struct dynamic_sign_user_config config; + config.operation = DYNAMIC_SIGN_OP_CLEAR; + return ksuctl(CMD_DYNAMIC_SIGN, &config, NULL); +} \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 6cebfccd..7524c1ed 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -28,6 +28,16 @@ bool is_lkm_mode(); #define KSU_MAX_GROUPS 32 #define KSU_SELINUX_DOMAIN 64 +#define DYNAMIC_SIGN_OP_SET 0 +#define DYNAMIC_SIGN_OP_GET 1 +#define DYNAMIC_SIGN_OP_CLEAR 2 + +struct dynamic_sign_user_config { + unsigned int operation; + unsigned int size; + char hash[65]; +}; + // SUSFS Functional State Structures struct susfs_feature_status { bool status_sus_path; @@ -109,4 +119,10 @@ bool get_hook_type(char* hook_type, size_t size); bool get_susfs_feature_status(struct susfs_feature_status* status); +bool set_dynamic_sign(unsigned int size, const char* hash); + +bool get_dynamic_sign(struct dynamic_sign_user_config* config); + +bool clear_dynamic_sign(); + #endif //KERNELSU_KSU_H \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt index e2f16358..1e2000b7 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt @@ -95,6 +95,27 @@ object Natives { */ external fun getSusfsFeatureStatus(): SusfsFeatureStatus? + /** + * Set dynamic signature configuration + * @param size APK signature size + * @param hash APK signature hash (64 character hex string) + * @return true if successful, false otherwise + */ + external fun setDynamicSign(size: Int, hash: String): Boolean + + + /** + * Get current dynamic signature configuration + * @return DynamicSignConfig object containing current configuration, or null if not set + */ + external fun getDynamicSign(): DynamicSignConfig? + + /** + * Clear dynamic signature configuration + * @return true if successful, false otherwise + */ + external fun clearDynamicSign(): Boolean + private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NOBODY_UID = 9999 @@ -147,6 +168,21 @@ object Natives { val statusSusSu: Boolean = false ) : Parcelable + @Immutable + @Parcelize + @Keep + data class DynamicSignConfig( + val size: Int = 0, + val hash: String = "" + ) : Parcelable { + constructor() : this(0, "") + + fun isValid(): Boolean { + return size > 0 && hash.length == 64 && hash.all { + it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' + } + } + } @Immutable @Parcelize diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt index 0d12e2a2..826d8005 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt @@ -145,6 +145,13 @@ fun MoreSettingsScreen( var showDpiConfirmDialog by remember { mutableStateOf(false) } var showImageEditor by remember { mutableStateOf(false) } + // 动态签名配置状态 + var dynamicSignConfig by remember { mutableStateOf(null) } + var isDynamicSignEnabled by remember { mutableStateOf(false) } + var dynamicSignSize by remember { mutableStateOf("") } + var dynamicSignHash by remember { mutableStateOf("") } + var showDynamicSignDialog by remember { mutableStateOf(false) } + // 主题模式选项 val themeOptions = listOf( stringResource(R.string.theme_follow_system), @@ -652,6 +659,147 @@ fun MoreSettingsScreen( ) } + LaunchedEffect(Unit) { + // 初始化动态签名配置 + dynamicSignConfig = Natives.getDynamicSign() + dynamicSignConfig?.let { config -> + if (config.isValid()) { + isDynamicSignEnabled = true + dynamicSignSize = config.size.toString() + dynamicSignHash = config.hash + } + } + } + + // 动态签名配置对话框 + if (showDynamicSignDialog) { + AlertDialog( + onDismissRequest = { showDynamicSignDialog = false }, + title = { Text(stringResource(R.string.dynamic_sign_title)) }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + // 启用开关 + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isDynamicSignEnabled = !isDynamicSignEnabled } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Switch( + checked = isDynamicSignEnabled, + onCheckedChange = { isDynamicSignEnabled = it } + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.enable_dynamic_sign)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 签名大小输入 + androidx.compose.material3.OutlinedTextField( + value = dynamicSignSize, + onValueChange = { dynamicSignSize = it }, + label = { Text(stringResource(R.string.signature_size)) }, + enabled = isDynamicSignEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = androidx.compose.ui.text.input.KeyboardType.Number + ) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 签名哈希输入 + androidx.compose.material3.OutlinedTextField( + value = dynamicSignHash, + onValueChange = { hash -> + // 只允许十六进制字符 + if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { + dynamicSignHash = hash + } + }, + label = { Text(stringResource(R.string.signature_hash)) }, + enabled = isDynamicSignEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + supportingText = { + Text(stringResource(R.string.hash_must_be_64_chars)) + }, + isError = isDynamicSignEnabled && dynamicSignHash.isNotEmpty() && dynamicSignHash.length != 64 + ) + } + }, + confirmButton = { + Button( + onClick = { + if (isDynamicSignEnabled) { + val size = dynamicSignSize.toIntOrNull() + if (size != null && size > 0 && dynamicSignHash.length == 64) { + val success = Natives.setDynamicSign(size, dynamicSignHash) + if (success) { + dynamicSignConfig = Natives.DynamicSignConfig(size, dynamicSignHash) + Toast.makeText( + context, + context.getString(R.string.dynamic_sign_set_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_sign_set_failed), + Toast.LENGTH_SHORT + ).show() + } + } else { + Toast.makeText( + context, + context.getString(R.string.invalid_sign_config), + Toast.LENGTH_SHORT + ).show() + return@Button + } + } else { + val success = Natives.clearDynamicSign() + if (success) { + dynamicSignConfig = null + dynamicSignSize = "" + dynamicSignHash = "" + Toast.makeText( + context, + context.getString(R.string.dynamic_sign_disabled_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_sign_clear_failed), + Toast.LENGTH_SHORT + ).show() + return@Button + } + } + showDynamicSignDialog = false + }, + enabled = if (isDynamicSignEnabled) { + dynamicSignSize.toIntOrNull()?.let { it > 0 } == true && + dynamicSignHash.length == 64 + } else true + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = { showDynamicSignDialog = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -1226,6 +1374,17 @@ fun MoreSettingsScreen( } ) } + // 动态签名设置 + SettingItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.dynamic_sign_title), + subtitle = if (isDynamicSignEnabled) { + stringResource(R.string.dynamic_sign_enabled_summary, dynamicSignSize) + } else { + stringResource(R.string.dynamic_sign_disabled) + }, + onClick = { showDynamicSignDialog = true } + ) } } } diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index ecdc0e97..326ba1f4 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -566,4 +566,16 @@ %1$d 个已选应用 %1$d 个已添加应用 所有应用均已添加 + 动态签名配置 + 已启用 (大小: %s) + 未启用 + 启用动态签名 + 签名大小 + 签名哈希值 + 哈希值必须是64位十六进制字符 + 动态签名配置设置成功 + 动态签名配置设置失败 + 无效的签名配置 + 动态签名已禁用 + 清除动态签名错误 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index e99fb62a..bbfdff63 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -568,4 +568,16 @@ %1$d apps selected %1$d apps already added All apps have been added + Dynamic Signature Configuration + Enabled (Size: %s) + Disabled + Enable Dynamic Signature + Signature Size + Signature Hash + Hash must be 64 hexadecimal characters + Dynamic signature configuration set successfully + Failed to set dynamic signature configuration + Invalid signature configuration + Dynamic signature disabled + Failed to clear dynamic signature