【Settings】恢复出厂设置密码校验

发布于:2025-09-14 ⋅ 阅读:(14) ⋅ 点赞:(0)

核心思路

由于从 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)再生成一次密码,然后比对两个密码是否一致。如果一致,就代表输入正确。

这种方式完美符合您的需求:

  1. 加密: 从设备ID和密钥生成6位密码。
  2. 反解密: 实际上是验证过程,可以通过密钥和设备ID验证密码是否正确。
  3. 密码检测: 可以校验输入的6位密码。
  4. 唯一性: 密钥是固定的,而不同设备的 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">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="设备唯一标识符 (Android ID)"
        android:textSize="18sp"
        android:textStyle="bold" />

    <TextView
        android: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" />

    <Button
        android:id="@+id/btn_generate_password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="生成6位设备密码" />

    <TextView
        android: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" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginVertical="32dp"
        android:background="?android:attr/listDivider" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="输入6位密码进行验证"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <EditText
        android: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"/>

    <Button
        android: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. 定义加密算法为 HmacSHA256
        final 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位的前面补0
        return 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;

    @Override
    protected 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. 获取并显示设备ID
        currentDeviceId = 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">

    <application
        android: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">
        <activity
            android: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>

如何运行

  1. 打开 Android Studio
  2. 创建一个新的空项目 (Empty Views Activity)。
  3. 替换 activity_main.xml: 将上面提供的 XML 代码复制并覆盖到 app/src/main/res/layout/activity_main.xml 文件中。
  4. 创建 CryptoUtils.java: 在 com.example.yourappname 包下(与MainActivity.java同级),右键 -> New -> Java Class,命名为 CryptoUtils,然后将上面提供的 Java 代码复制进去。
  5. 修改 SECRET_KEY: 在 CryptoUtils.java 文件中,将 SECRET_KEY 的值 "Your_Unique_And_Secret_Key_Here" 修改为你自己的、独特的、保密的字符串。
  6. 替换 MainActivity.java: 将上面提供的 MainActivity.java 代码复制并覆盖到项目已有的同名文件中。
  7. 运行应用: 点击 Android Studio 的 “Run” 按钮,将应用安装到模拟器或真实设备上。

重要安全提示

密钥(SECRET_KEY)的存储

CryptoUtils.java 的代码中,我们直接将密钥硬编码为了一个字符串常量。这在生产环境中是不安全的! 任何反编译您应用的人都可以轻易地找到这个密钥。

在真实项目中,请考虑使用更安全的方式来存储和管理您的密钥,例如:

  • Android Keystore 系统:将密钥存储在硬件支持的安全容器中。
  • NDK 和 C++ 代码:将密钥存储在原生代码中,增加反编译的难度。
  • 从服务器获取:在应用启动时通过安全的网络连接从您的服务器获取密钥。

对于您的需求,保持这个密钥在应用中的唯一性和不变性是关键。

在AOSP源码环境中编译系统应用(privileged-app或platform app),你拥有比普通SDK开发高得多的权限和更灵活的手段来处理这类安全问题。将SECRET_KEY硬编码在Java代码中是绝对不推荐的做法。

从受保护的文件系统分区读取(推荐,最安全)

这是最安全的方法。在编译时,将密钥写入到一个只有你的应用(以及少数系统进程)有权读取的文件中,并使用 SELinux策略 严格限制对该文件的访问。

实现步骤:
  1. 创建密钥文件
    在你的设备目录下 (例如 device/<vendor>/<product>/) 创建一个名为 my_app_secret.key 的文件,里面只包含你的密钥字符串。

  2. 在编译时将文件复制到设备分区
    在你的产品 makefile (product.mk) 中,添加规则将该文件复制到设备的一个受保护目录,例如 /data/misc/myapp/

    PRODUCT_COPY_FILES += \
        device/<vendor>/<product>/my_app_secret.key:$(TARGET_COPY_OUT_DATA)/misc/myapp/secret.key
    
  3. 定义严格的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模式下)都无法访问。

  4. 在你的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 脚本是完全可行的,只要确保每一步的加密和数据处理逻辑都完全一致,就能为相同的 deviceIdSECRET_KEY 生成完全相同的6位密码。


Python 脚本 (generate_password.py)

Python 拥有强大的 hmachashlib 库,可以非常精确地实现这个逻辑。这是推荐的脚本方式,因为它更清晰易读。

脚本代码
#!/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 - 4
        offset_bytes = hash_bytes[-4:]

        # 4. 将这4个字节转换为一个32位的大端序 (big-endian) 整数
        truncated_hash = int.from_bytes(offset_bytes, 'big')

        # 5. 将整数的最高位(符号位)清零,确保结果为正数
        # 这等同于Java代码中的 truncatedHash &= 0x7FFFFFFF
        positive_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 None

if __name__ == "__main__":
    # 检查命令行参数是否足够
    if len(sys.argv) < 2:
        print(f"用法: python {sys.argv[0]} <device_id>")
        sys.exit(1)

    # 从命令行第一个参数获取device_id
    input_device_id = sys.argv[1]
    
    password = generate_password(input_device_id, SECRET_KEY)

    if password:
        print(password)

如何使用
  1. 将上面的代码保存为 generate_password.py 文件。

  2. 确保 SECRET_KEY 的值与你 Android 项目中的完全一样。

  3. 在命令行中运行,并提供一个 deviceId 作为参数:

    # 赋予执行权限 (可选)
    chmod +x generate_password.py
    
    # 运行脚本
    python3 generate_password.py "c123456789abcdef"
    

    或者

    ./generate_password.py "c123456789abcdef"
    

脚本会输出计算得到的6位密码。