【Settings】恢复出厂设置密码校验
核心思路
由于从 Android 10 (API 级别 29) 开始,出于隐私保护的考虑,普通应用无法再获取设备的 IMEI。尝试获取会抛出 SecurityException
。
因此,我们将采用一个同样具有唯一性的设备标识符 ANDROID_ID
来替代 IMEI。ANDROID_ID
是一个在设备首次启动时随机生成的64位数字,对于应用的每个签名密钥、用户和设备组合都是唯一的。这同样能满足您“不同设备的ID不同”的需求。
加密方式选择:
我们将使用 HMAC-SHA256 算法。这是一种基于密钥的哈希算法。
- 加密 (生成密码): 使用一个密钥(Secret Key)对
ANDROID_ID
进行 HMAC-SHA256 运算,得到一个哈希摘要。然后,我们将这个较长的摘要转换为一个6位的数字密码。这个过程是单向的,但对于相同的ANDROID_ID
和密钥,结果始终是相同的。 - 解密 (验证密码): “解密”在这里实际上是“验证”。当用户输入一个6位密码时,我们用同样的方法(使用密钥和
ANDROID_ID
)再生成一次密码,然后比对两个密码是否一致。如果一致,就代表输入正确。
这种方式完美符合您的需求:
- 加密: 从设备ID和密钥生成6位密码。
- 反解密: 实际上是验证过程,可以通过密钥和设备ID验证密码是否正确。
- 密码检测: 可以校验输入的6位密码。
- 唯一性: 密钥是固定的,而不同设备的
ANDROID_ID
不同,因此生成的密码也不同。
完整代码实现
这是一个可以直接在 Android Studio 中运行的项目代码。
1. 项目结构
app
└── src└── main├── java│ └── com│ └── example│ └── deviceidapp│ ├── CryptoUtils.java // 加密工具类│ └── MainActivity.java // 主界面Activity├── res│ └── layout│ └── activity_main.xml // 界面布局└── AndroidManifest.xml
2. 界面布局 (activity_main.xml
)
这个文件定义了应用的界面,包含显示ID、显示密码、输入密码和两个操作按钮。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="24dp"android:gravity="center_horizontal"tools:context=".MainActivity"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="设备唯一标识符 (Android ID)"android:textSize="18sp"android:textStyle="bold" /><TextViewandroid:id="@+id/tv_device_id"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:textSize="16sp"tools:text="c123456789abcdef" /><Buttonandroid:id="@+id/btn_generate_password"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="24dp"android:text="生成6位设备密码" /><TextViewandroid:id="@+id/tv_generated_password"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:textSize="28sp"android:textStyle="bold"android:textColor="@android:color/holo_blue_dark"tools:text="123456" /><Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:layout_marginVertical="32dp"android:background="?android:attr/listDivider" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="输入6位密码进行验证"android:textSize="18sp"android:textStyle="bold"/><EditTextandroid:id="@+id/et_password_input"android:layout_width="200dp"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:hint="请输入6位密码"android:inputType="numberPassword"android:maxLength="6"android:gravity="center"android:textSize="24sp"/><Buttonandroid:id="@+id/btn_verify_password"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:text="验证密码" /></LinearLayout>
3. 加密工具类 (CryptoUtils.java
)
这个类包含了获取设备ID和生成密码的核心逻辑。
package com.example.deviceidapp;import android.annotation.SuppressLint;
import android.content.Context;
import android.provider.Settings;import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.Objects;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;/*** 加密和设备ID工具类*/
public class CryptoUtils {// !!! 重要 !!!// 这是一个密钥,必须妥善保管。在真实项目中,不要硬编码在这里。// 可以考虑从服务器获取,或者使用更安全的存储方式。// 密钥必须是唯一的且保持不变。private static final String SECRET_KEY = "Your_Unique_And_Secret_Key_Here";/*** 获取设备的 ANDROID_ID** @param context 上下文* @return 设备的ANDROID_ID,如果获取失败则返回 "unknown"*/@SuppressLint("HardwareIds")public static String getDeviceID(Context context) {try {String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);return Objects.requireNonNull(androidId);} catch (Exception e) {e.printStackTrace();return "unknown";}}/*** 根据设备ID和密钥生成6位密码** @param deviceId 设备ID (例如 ANDROID_ID)* @return 生成的6位数字密码,格式为 "XXXXXX"*/public static String generatePassword(String deviceId) throws NoSuchAlgorithmException, InvalidKeyException {// 1. 定义加密算法为 HmacSHA256final String ALGORITHM = "HmacSHA256";Mac mac = Mac.getInstance(ALGORITHM);// 2. 使用我们的密钥初始化Mac实例SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);mac.init(secretKeySpec);// 3. 对设备ID进行加密,得到字节数组形式的哈希摘要byte[] hash = mac.doFinal(deviceId.getBytes(StandardCharsets.UTF_8));// 4. 将哈希摘要转换为6位数字// 我们取哈希结果的前4个字节,将其转换为一个正整数int offset = hash.length - 4; // 从末尾取4字节,增加随机性long truncatedHash = 0;for (int i = 0; i < 4; ++i) {truncatedHash <<= 8;truncatedHash |= (hash[offset + i] & 0xFF);}// 确保结果为正数truncatedHash &= 0x7FFFFFFF;// 5. 对这个整数取模,得到一个 0 到 999999 之间的数long sixDigitNumber = truncatedHash % 1000000;// 6. 格式化为6位数字,不足6位的前面补0return String.format("%06d", sixDigitNumber);}
}
4. 主界面 (MainActivity.java
)
这个文件负责处理界面交互逻辑,调用 CryptoUtils
来完成功能。
package com.example.deviceidapp;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;public class MainActivity extends AppCompatActivity {private TextView tvDeviceId;private TextView tvGeneratedPassword;private EditText etPasswordInput;private Button btnGeneratePassword;private Button btnVerifyPassword;private String currentDeviceId;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化视图tvDeviceId = findViewById(R.id.tv_device_id);tvGeneratedPassword = findViewById(R.id.tv_generated_password);etPasswordInput = findViewById(R.id.et_password_input);btnGeneratePassword = findViewById(R.id.btn_generate_password);btnVerifyPassword = findViewById(R.id.btn_verify_password);// 1. 获取并显示设备IDcurrentDeviceId = CryptoUtils.getDeviceID(this);tvDeviceId.setText(currentDeviceId);// 2. 设置“生成密码”按钮的点击事件btnGeneratePassword.setOnClickListener(v -> {try {// 使用工具类生成密码String password = CryptoUtils.generatePassword(currentDeviceId);// 显示生成的密码tvGeneratedPassword.setText(password);Toast.makeText(this, "密码已生成", Toast.LENGTH_SHORT).show();} catch (NoSuchAlgorithmException | InvalidKeyException e) {// 异常处理e.printStackTrace();Toast.makeText(this, "错误:无法生成密码", Toast.LENGTH_LONG).show();}});// 3. 设置“验证密码”按钮的点击事件btnVerifyPassword.setOnClickListener(v -> {// 获取用户输入的密码String userInput = etPasswordInput.getText().toString();// 检查输入是否为6位if (userInput.length() != 6) {Toast.makeText(this, "请输入6位数字密码进行验证", Toast.LENGTH_SHORT).show();return; // 提前退出}try {// 再次生成正确的密码,用于比对String correctPassword = CryptoUtils.generatePassword(currentDeviceId);// 比对用户输入和正确的密码if (userInput.equals(correctPassword)) {// 验证成功Toast.makeText(this, "验证成功!密码正确。", Toast.LENGTH_LONG).show();} else {// 验证失败Toast.makeText(this, "验证失败!密码错误。", Toast.LENGTH_LONG).show();}} catch (NoSuchAlgorithmException | InvalidKeyException e) {// 异常处理e.printStackTrace();Toast.makeText(this, "错误:无法进行验证", Toast.LENGTH_LONG).show();}});}
}
5. Android 清单文件 (AndroidManifest.xml
)
这是应用必需的配置文件,声明了应用的入口。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/dataExtractionRules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.DeviceIDApp"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
如何运行
- 打开 Android Studio。
- 创建一个新的空项目 (Empty Views Activity)。
- 替换
activity_main.xml
: 将上面提供的 XML 代码复制并覆盖到app/src/main/res/layout/activity_main.xml
文件中。 - 创建
CryptoUtils.java
: 在com.example.yourappname
包下(与MainActivity.java
同级),右键 -> New -> Java Class,命名为CryptoUtils
,然后将上面提供的 Java 代码复制进去。 - 修改
SECRET_KEY
: 在CryptoUtils.java
文件中,将SECRET_KEY
的值"Your_Unique_And_Secret_Key_Here"
修改为你自己的、独特的、保密的字符串。 - 替换
MainActivity.java
: 将上面提供的MainActivity.java
代码复制并覆盖到项目已有的同名文件中。 - 运行应用: 点击 Android Studio 的 “Run” 按钮,将应用安装到模拟器或真实设备上。
重要安全提示
密钥(SECRET_KEY
)的存储:
在 CryptoUtils.java
的代码中,我们直接将密钥硬编码为了一个字符串常量。这在生产环境中是不安全的! 任何反编译您应用的人都可以轻易地找到这个密钥。
在真实项目中,请考虑使用更安全的方式来存储和管理您的密钥,例如:
- Android Keystore 系统:将密钥存储在硬件支持的安全容器中。
- NDK 和 C++ 代码:将密钥存储在原生代码中,增加反编译的难度。
- 从服务器获取:在应用启动时通过安全的网络连接从您的服务器获取密钥。
对于您的需求,保持这个密钥在应用中的唯一性和不变性是关键。
在AOSP源码环境中编译系统应用(privileged-app或platform app),你拥有比普通SDK开发高得多的权限和更灵活的手段来处理这类安全问题。将SECRET_KEY
硬编码在Java代码中是绝对不推荐的做法。
从受保护的文件系统分区读取(推荐,最安全)
这是最安全的方法。在编译时,将密钥写入到一个只有你的应用(以及少数系统进程)有权读取的文件中,并使用 SELinux策略 严格限制对该文件的访问。
实现步骤:
-
创建密钥文件
在你的设备目录下 (例如device/<vendor>/<product>/
) 创建一个名为my_app_secret.key
的文件,里面只包含你的密钥字符串。 -
在编译时将文件复制到设备分区
在你的产品 makefile (product.mk
) 中,添加规则将该文件复制到设备的一个受保护目录,例如/data/misc/myapp/
。PRODUCT_COPY_FILES += \device/<vendor>/<product>/my_app_secret.key:$(TARGET_COPY_OUT_DATA)/misc/myapp/secret.key
-
定义严格的SELinux策略
这是最关键的一步,它保证了文件的安全性。
a. 为文件定义一个类型 (file_contexts):在sepolicy
目录下,通常在file_contexts
文件中添加:
/data/misc/myapp(/.*)? u:object_r:myapp_key_file:s0
b. 为你的应用定义一个域 (seapp_contexts):确保你的应用在一个独立的SELinux域中运行,例如myapp_app
。
c. 授予你的应用域读写权限 (app.te):在你的应用的策略文件myapp_app.te
中,添加如下规则:
# 允许 myapp_app 域搜索 /data/misc/myapp 目录 allow myapp_app myapp_key_file:dir search; # 允许 myapp_app 域读取 myapp_key_file 类型的文件 allow myapp_app myapp_key_file:file { read open getattr };
这条规则意味着,只有被标记为myapp_app
域的进程才能读取被标记为myapp_key_file
类型的文件。其他任何应用(即使是root用户启动的进程,在SELinux Enforcing模式下)都无法访问。 -
在你的App中读取文件
现在你的应用可以直接通过标准的文件IO操作来读取这个密钥。import java.io.File; import java.nio.file.Files; import java.nio.file.Paths;private String readSecretKeyFromFile() {File keyFile = new File("/data/misc/myapp/secret.key");try {// 需要确保你的应用有权限创建/访问这个目录// 在init.rc中添加 chown 和 chmod 命令String key = new String(Files.readAllBytes(keyFile.toPath()));return key.trim();} catch (IOException e) {e.printStackTrace();// 处理异常,密钥获取失败return null;} }
你还需要在
init.rc
文件中确保/data/misc/myapp
目录被创建并且有正确的属主和权限。
优缺点:
- 优点:
- 极高的安全性。密钥既不在APK中,也不在任何人都能读的属性里。访问被强大的SELinux强制访问控制(MAC)机制所保护。
- 完全将密钥的配置和应用代码解耦。
- 缺点:
- 实现最为复杂,需要你对AOSP的编译系统、init系统以及SELinux策略有深入的了解。
将Java中的 generatePassword
方法的逻辑翻译成Python 脚本是完全可行的,只要确保每一步的加密和数据处理逻辑都完全一致,就能为相同的 deviceId
和 SECRET_KEY
生成完全相同的6位密码。
Python 脚本 (generate_password.py
)
Python 拥有强大的 hmac
和 hashlib
库,可以非常精确地实现这个逻辑。这是推荐的脚本方式,因为它更清晰易读。
脚本代码
#!/usr/bin/env python3
import sys
import hmac
import hashlib# !!! 重要 !!!
# 这个密钥必须和你的Java代码/其他脚本中的密钥完全一致
SECRET_KEY = "Your_Unique_And_Secret_Key_Here"def generate_password(device_id: str, secret_key: str) -> str:"""根据设备ID和密钥生成6位密码,逻辑与Java版本完全相同。Args:device_id: 设备唯一标识符 (例如 ANDROID_ID).secret_key: 用于HMAC加密的密钥.Returns:一个6位的数字密码字符串."""try:# 1. 将密钥和设备ID转换为UTF-8编码的字节key_bytes = secret_key.encode('utf-8')data_bytes = device_id.encode('utf-8')# 2. 使用HmacSHA256计算哈希摘要# hmac.new().digest() 返回的是原始字节 (raw bytes)hash_bytes = hmac.new(key_bytes, data_bytes, hashlib.sha256).digest()# 3. 截取哈希摘要的最后4个字节# 这等同于Java代码中的 offset = hash.length - 4offset_bytes = hash_bytes[-4:]# 4. 将这4个字节转换为一个32位的大端序 (big-endian) 整数truncated_hash = int.from_bytes(offset_bytes, 'big')# 5. 将整数的最高位(符号位)清零,确保结果为正数# 这等同于Java代码中的 truncatedHash &= 0x7FFFFFFFpositive_hash = truncated_hash & 0x7FFFFFFF# 6. 对1,000,000取模,得到一个0到999999之间的数字six_digit_number = positive_hash % 1000000# 7. 格式化为6位数字,不足的前面补0# 这等同于Java代码中的 String.format("%06d", ...)return f'{six_digit_number:06d}'except Exception as e:print(f"发生错误: {e}", file=sys.stderr)return Noneif __name__ == "__main__":# 检查命令行参数是否足够if len(sys.argv) < 2:print(f"用法: python {sys.argv[0]} <device_id>")sys.exit(1)# 从命令行第一个参数获取device_idinput_device_id = sys.argv[1]password = generate_password(input_device_id, SECRET_KEY)if password:print(password)
如何使用
-
将上面的代码保存为
generate_password.py
文件。 -
确保
SECRET_KEY
的值与你 Android 项目中的完全一样。 -
在命令行中运行,并提供一个
deviceId
作为参数:# 赋予执行权限 (可选) chmod +x generate_password.py# 运行脚本 python3 generate_password.py "c123456789abcdef"
或者
./generate_password.py "c123456789abcdef"
脚本会输出计算得到的6位密码。