核心思路
由于从 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">
<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>
如何运行
- 打开 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 - 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)
如何使用
将上面的代码保存为
generate_password.py
文件。确保
SECRET_KEY
的值与你 Android 项目中的完全一样。在命令行中运行,并提供一个
deviceId
作为参数:# 赋予执行权限 (可选) chmod +x generate_password.py # 运行脚本 python3 generate_password.py "c123456789abcdef"
或者
./generate_password.py "c123456789abcdef"
脚本会输出计算得到的6位密码。