第1章 Android中锁屏密码加密算法分析
为了安全,Android设备在解锁屏幕时会有密码输入,那么这个密码存放在哪里?是否为明文存储?如果是加密存储,那么加密算法是什么?这就是本章要介绍的内容。本章的目的是带领读者踏入移动安全的大门。
1.1 锁屏密码方式
Android中现在支持的锁屏密码主要有两种:一种是手势密码,也就是常见的九宫格密码图;一种是输入密码,分为PIN密码和复杂字符密码,而PIN密码就是数字密码,比较简单。当然现在也有一个高级的指纹密码,这不是本章分析的范围,本章只分析手势密码和输入密码。
1.2 密码算法分析
在设置锁屏密码界面,用工具获取当前的View类,然后一步一步跟入,最终会跟到一个锁屏密码工具类:LockPatternUtils.java。每个版本可能实现逻辑不一样,这里用5.1版本的源码进行分析。
1.2.1 输入密码算法分析
找到源码之后,首先来分析一下输入密码算法:
public byte[] passwordToHash(String password, int userId) { if (password == null) { return null; } String algo = null; byte[] hashed = null; try { byte[] saltedPassword = (password + getSalt(userId)).getBytes(); byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1"). digest(saltedPassword); byte[] md5 = MessageDigest.getInstance(algo = "MD5"). digest(saltedPassword); hashed = (toHex(sha1) + toHex(md5)).getBytes(); } catch (NoSuchAlgorithmException e) { Log.w(TAG, "Failed to encode string because of missing algorithm: " + algo); } return hashed; }
可以看到有一个方法passwordToHash,参数为用户输入的密码和当前用户对应的id,一般设备不会有多个用户,所以这里的userId是默认值0。下面就是最为核心的加密算法了:原文密码+设备的salt值,然后分别进行MD5和SHA-1操作,转化成hex值再次拼接,得到的就是最终保存到本地的加密密码内容。而这里最重要的是如何获取设备对应的salt值,这可以一步一步跟踪代码:
private String getSalt(int userId) { long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0, userId); if (salt == 0) { try { salt = SecureRandom.getInstance("SHA1PRNG").nextLong(); setLong(LOCK_PASSWORD_SALT_KEY, salt, userId); Log.v(TAG, "Initialized lock password salt for user: " + userId); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("Couldn't get SecureRandom number", e); } } return Long.toHexString(salt); }
查看getSalt方法,首先根据字段key为lockscreen.password_salt从一个地方获取salt值,如果发现这个值为0,就随机生成一个,然后将其保存到那个地方,最后将salt转化成hex值即可。现在需要找到这个地方,继续跟踪代码:
private long getLong(String secureSettingKey, long defaultValue, int userHandle) { try { return getLockSettings().getLong(secureSettingKey, defaultValue, userHandle); } catch (RemoteException re) { return defaultValue; } }
猜想应该是保存到一个数据库中了,继续跟踪代码:
private ILockSettings getLockSettings() { if (mLockSettingsService == null) { ILockSettings service = ILockSettings.Stub.asInterface( ServiceManager.getService("lock_settings")); mLockSettingsService = service; } return mLockSettingsService; }
通过在ServiceManager中获取一个服务来进行操作,在Android中,像这种获取服务的方式最终实现逻辑都是在XXXService类中,这里是LockSettingsService.java类,找到这个类,查看它的getLong方法:
@Override public long getLong(String key, long defaultValue, int userId) throws RemoteException { checkReadPermission(key, userId); String value = mStorage.readKeyValue(key, null, userId); return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); }
其实到这里就可以看出,非常肯定是数据库中保存的,继续跟踪代码:
private final LockSettingsStorage mStorage; private LockPatternUtils mLockPatternUtils; private boolean mFirstCallToVold; public LockSettingsService(Context context) { mContext = context; // Open the database mLockPatternUtils = new LockPatternUtils(context); mFirstCallToVold = true; IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_STARTING); mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null); mStorage = new LockSettingsStorage(context, new LockSettingsStorage. Callback() { @Override public void initialize(SQLiteDatabase db) { // Get the lockscreen default from a system property, if available boolean lockScreenDisable = SystemProperties.getBoolean( "ro.lockscreen.disable.default", false); if (lockScreenDisable) { mStorage.writeKeyValue(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0); } } }); }
果然发现是保存到一个数据库中,继续查看LockSettingsStorage.java类:
class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "LockSettingsDB"; private static final String DATABASE_NAME = "locksettings.db"; private static final int DATABASE_VERSION = 2; private final Callback mCallback; public DatabaseHelper(Context context, Callback callback) { super(context, DATABASE_NAME, null, DATABASE_VERSION); setWriteAheadLoggingEnabled(true); mCallback = callback; } private void createTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE + " (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN_KEY + " TEXT, " + COLUMN_USERID + " INTEGER, " + COLUMN_VALUE + " TEXT" + "); "); } @Override public void onCreate(SQLiteDatabase db) { createTable(db); mCallback.initialize(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { int upgradeVersion = oldVersion; if (upgradeVersion == 1) { // Previously migrated lock screen widget settings. Now defunct. upgradeVersion = 2; } if (upgradeVersion ! = DATABASE_VERSION) { Log.w(TAG, "Failed to upgrade database! "); } } }
看到了,数据库名字叫作locksettings.db,那么它保存在哪里呢?继续看这个类:
private static final String SYSTEM_DIRECTORY = "/system/"; private static final String LOCK_PATTERN_FILE = "gesture.key"; private static final String LOCK_PASSWORD_FILE = "password.key";
可以看到有两个key文件,它们就是用来保存加密之后的手势密码和输入密码的,将信息保存到本地,下次开机解锁需要读取这个文件内容进行密码比对。看到一个目录是system,所以数据库和这两个key文件很可能保存到目录/data/system/下了,为了再证实一下,直接用find命令去根目录下搜索这个数据库文件也是可以的。最终确定是该目录:
这里可能会提示找不到find命令,这时需要安装busybox工具,才能使用这个命令。找到这个数据库文件就好办了,直接取出来,然后用SQLite工具进行查看即可,当然也可以直接在手机中查看。为了方便还是取出来看,如图1-1所示。
图1-1 数据库信息
这里看到了表格字段,并且获取到这个值了,那么下面就要用这个值来验证上面的分析是否正确。首先给设备设置一个简单的输入密码,这里直接输入简单的“1234”,然后会在/data/system目录下生成一个密码加密key文件/data/system/password.key,将该文件导出来,如图1-2所示。
图1-2 加密文件
下面就用简单的Java代码手动实现这个算法,看看分析是否正确。加密算法不用自己写,直接从上面的源码中拷贝出来就可以了。代码如下:
/** * 输入密码加密算法 * @param password * @return */ public static byte[] passwordToHash(String password) { if (password == null) { return null; } byte[] hashed = null; try { byte[] saltedPassword = (password + SALT).getBytes(); byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); hashed = (toHex(sha1) + toHex(md5)).getBytes(); } catch (Exception e) { } return hashed; } private static String toHex(byte[] ary) { final String hex = "0123456789ABCDEF"; String ret = ""; for (int i = 0; i < ary.length; i++) { ret += hex.charAt((ary[i] >> 4) & 0xf); ret += hex.charAt(ary[i] & 0xf); } return ret; }
这里的salt值是我们从数据库中得到的,不过要记得进行hex转化:
/** * 设备的salt值,可以利用反射获取,也可以去/data/system/locksettings.db数据库中查看 * 需要注意的是数据库中保存的是long类型,这里需要进行hex转化 */ private final static String SALT = Long.toHexString(-9167742676506383495l);
然后用“1234”原文密码去生成加密之后的信息:
System.out.println("passwordinfo:"+new String(passwordToHash("1234")));
运行结果如图1-3所示。
图1-3 运行结果
可以发现内容和上面的password.key内容完全一致,也就验证了上面的分析完全正确。到这里就分析完了输入密码的加密算法,总结一点就是:MD5(输入明文密码+设备的salt).Hex+SHA1(输入明文密码+设备的salt).Hex就是最终的加密内容。而这里最重要的是如何获取设备的salt值,可以用反射机制进行获取,新建一个简单的Android项目:
try{ Class<? > clazz1 = Class.forName("com.android.internal.widget. LockPatternUtils"); Object lockUtils = clazz1.getConstructor(Context.class).newInstance(this); Class<? > lockUtilsClazz = lockUtils.getClass(); Method getSaltM = lockUtilsClazz.getDeclaredMethod("getSalt", int.class); getSaltM.setAccessible(true); Object saltObj = getSaltM.invoke(lockUtils, 0); Log.i("jw", "salt:"+saltObj); }catch(Exception e){ Log.i("jw", "err:"+Log.getStackTraceString(e)); }
这样就不用去查看数据库获取salt值了,方便快捷,打印的日志信息如下:
这是数据库中的long类型值转化成hex之后的值。
1.2.2 手势密码算法分析
下面来分析手势密码,代码依然在LockPatternUtils.java中:
public static byte[] patternToHash(List<LockPatternView.Cell> pattern) { if (pattern == null) { return null; } final int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { LockPatternView.Cell cell = pattern.get(i); res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); } try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] hash = md.digest(res); return hash; } catch (NoSuchAlgorithmException nsa) { return res; } }
这个算法比较简单,就是九宫格图案转化成字节数组,然后用SHA1加密即可。关于九宫格不再多说了,从0开始顺时针计数到8,类似图1-4所示。
图1-4 九宫格
看一下代码,有行和列之分。比如L形状的手势密码应该是00 03 06 07 08,这样组成五个字节。这里为了验证手势密码是否正确,设置一个简单的手势密码,如图1-5所示。
图1-5 简单的手势密码
然后在/data/system目录下生成一个密码文件/data/system/gesture.key,取出来用二进制工具查看,不然可能看到的是乱码,st r.这里用的是010Editor工具查看,如图1-6所示。
图1-6 手势密码加密内容
为了最大化地还原算法,依然把源码拷贝出来,然后定义一个手势九宫格类,构造出这个手势的点数据:
/** * 手势密码加密算法 * @param pattern * @return */ public static byte[] patternToHash(List<LockPatternView.Cell> pattern) { if (pattern == null) { return null; } int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { LockPatternView.Cell cell = pattern.get(i); res[i] = (byte) (cell.row * 3 + cell.column); } try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] hash = md.digest(res); return hash; } catch (Exception nsa) { } return null; }
这是源码的加密算法,下面再构造出手势点数据:
//手势密码模拟 List<LockPatternView.Cell> pattern = new ArrayList<LockPatternView.Cell>(); LockPatternView.Cell cell1 = new LockPatternView.Cell(0, 0); pattern.add(cell1); LockPatternView.Cell cell2 = new LockPatternView.Cell(0, 3); pattern.add(cell2); LockPatternView.Cell cell3 = new LockPatternView.Cell(0, 6); pattern.add(cell3); LockPatternView.Cell cell4 = new LockPatternView.Cell(0, 7); pattern.add(cell4); LockPatternView.Cell cell5 = new LockPatternView.Cell(0, 8); pattern.add(cell5); System.out.println("pattern:"+toHex(patternToHash(pattern)));
手势点数据应该是00 01 02 05 08,打印看结果,如图1-7所示。
图1-7 运行结果
从运行结果发现,一模一样,这样就完美地分析完了手势密码加密算法。
这里再总结一下两种方式锁屏密码算法。
第一种:输入密码算法
对输入的明文密码+设备的salt值进行MD5和SHA1操作,之后转化成hex值进行拼接即可,最终加密信息保存到本地目录/data/system/password.key。
第二种:手势密码算法
将九宫格手势密码中的点数据转化成对应的字节数组,然后直接进行SHA1加密即可。最终加密信息保存到本地目录/data/system/gesture.key。
1.3 本章小结
读完本章是不是迫不及待地想动手尝试一下?在操作之前一定要记住,先得到设备的salt值,然后要注意源码版本。