看了姜维大佬写的《Android应用安全防护和逆向分析》一书中对Android 5.1的锁屏密码加密算法分析,我也依样画葫芦对我手上的Android 8.1.0锁屏密码加密算法进行分析,其实源码并没有多大改变,我这仅仅只是做一个复现过程。
1. 手势密码加密算法分析 将手机与电脑连接,手机运行至锁屏界面,使用D:\Java\Android\sdk\tools\bin\uiautomatorviewer.bat
工具截屏手机当前界面,选中图案锁屏可以看到它的resource-id。(找不到有可能tools目录下)
由于是找密码算法,所以有关加密的类需要格外注意。
1.1 patternToHash() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1047 public static byte [] patternToHash(List<LockPatternView.Cell> pattern) {1048 if (pattern == null ) {1049 return null ;1050 }1051 1052 final int patternSize = pattern.size();1053 byte [] res = new byte [patternSize];1054 for (int i = 0 ; i < patternSize; i++) {1055 LockPatternView.Cell cell = pattern.get(i);1056 res[i] = (byte ) (cell.getRow() * 3 + cell.getColumn());1057 }1058 try {1059 MessageDigest md = MessageDigest.getInstance("SHA-1" );1060 byte [] hash = md.digest(res);1061 return hash;1062 } catch (NoSuchAlgorithmException nsa) {1063 return res;1064 }1065 }
1.2 verifyCredential() 这个pattern怎么来的呢?查看哪里调用了patternToHash()
。看名字这里已经是验证图案的过程了。patternToHash()
的参数是经过stringToPattern()
方法来的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1638 private VerifyCredentialResponse verifyCredential (int userId, CredentialHash storedHash, 1639 String credential, boolean hasChallenge, long challenge,1640 ICheckCredentialProgressCallback progressCallback) throws RemoteException {... 1654 if (storedHash.version == CredentialHash.VERSION_LEGACY) {1655 final byte [] hash;1656 if (storedHash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {1657 hash = LockPatternUtils.patternToHash(LockPatternUtils.stringToPattern(credential));1658 } else {1659 hash = mLockPatternUtils.passwordToHash(credential, userId);1660 }1661 if (Arrays.equals(hash, storedHash.hash)) {1662 if (storedHash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {1663 unlockKeystore(LockPatternUtils.patternStringToBaseZero(credential), userId);1664 } else {1665 unlockKeystore(credential, userId);1666 }
1.3 stringToPattern() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 992 public static List<LockPatternView.Cell> stringToPattern(String string) {993 if (string == null ) {994 return null ;995 }996 997 List<LockPatternView.Cell> result = Lists.newArrayList();998 999 final byte [] bytes = string.getBytes();1000 for (int i = 0 ; i < bytes.length; i++) {1001 byte b = (byte ) (bytes[i] - '1' );1002 result.add(LockPatternView.Cell.of(b / 3 , b % 3 ));1003 }1004 return result;1005 }
这个传入的string又是什么呢?搜索一下发现stringToPattern()
可以这样用:
也就是传入数字字符串。根据传入的字符串,可以稍微看一下字节型变量b存的是什么。我们自己编写一下代码查看结果:
1.4 of() 得到了b后,继续进入查看of()
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 189 public static Cell of (int row, int column) {190 checkRange(row, column);191 return sCells[row][column];192 }194 private static void checkRange (int row, int column) {195 if (row < 0 || row > 2 ) {196 throw new IllegalArgumentException ("row must be in range 0-2" );197 }198 if (column < 0 || column > 2 ) {199 throw new IllegalArgumentException ("column must be in range 0-2" );200 }201 }159 private static final Cell[][] sCells = createCells();160 161 private static Cell[][] createCells() {162 Cell[][] res = new Cell [3 ][3 ];163 for (int i = 0 ; i < 3 ; i++) {164 for (int j = 0 ; j < 3 ; j++) {165 res[i][j] = new Cell (i, j);166 }167 }168 return res;169 }
这就很清楚了,九宫格九个点分别代表字符1~9,将依次连起来的字符传入stringToPattern()
,再将每个点转化为二维Cell数组的形式,依次存进result列表中。这个result列表就是patternToHash()
中的参数pattern。
patternToHash()
中的res数组内容范围我们就知道了,是数字0~8的字节形式,将点依次存入res数组,再进行SHA-1加密。举个例子,输入的string为“1234”: ^7da60a
分析完patternToHash()
后,回到verifyCredential()
,比较hash和storedHash.hash是否相等,如果相等则解锁。也就是说我们预先存的密码在storedHash中。下面我们就来找storedHash。
1.5 readCredentialHash() storedHash是从readCredentialHash()
方法中来的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 300 public CredentialHash readCredentialHash (int userId) {301 CredentialHash passwordHash = readPasswordHashIfExists(userId);302 CredentialHash patternHash = readPatternHashIfExists(userId);303 if (passwordHash != null && patternHash != null ) {304 if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {305 return passwordHash;306 } else {307 return patternHash;308 }309 } else if (passwordHash != null ) {310 return passwordHash;311 } else if (patternHash != null ) {312 return patternHash;313 } else {314 return CredentialHash.createEmptyHash();315 }316 }
1.6 prefetchUser() 又哪里调用到了readCredentialHash()
呢?可以看到prefetchUser()
方法进行了数据库请求。
尝试找一下数据库的名字,因为它是系统应用,所以它的所有文件名都是有特定名字的。
这样我们就知道了它的数据库是locksettings.db
,可以在手机中搜索:
1 find / -name locksettings.db
路径为/data/system/locksettings.db
。将它取出来,用DB Browser for SQLite打开。
1 2 3 bullhead:/ # cp /data/system/locksettings.db /sdcard/Download/ C:\Users\dell\Desktop>adb pull /sdcard/Download/locksettings.db /sdcard/Download/locksettings.db: 1 file pulled, 0 skipped. 2.7 MB/s (20480 bytes in 0.007s)
数据库中的userId都为0,其它貌似看不出什么,回到源码中去,数据库请求完后,执行readCredentialHash()
。
这里与Andorid 5.1不一样的就是,Android 5.1单纯pull locksettings.db
是没数据的,需要一同把locksettings.db-shm
和locksettings.db-wal
拷贝下来。先打开DB Browser for SQLite再载入locksettings.db
即可看到数据。
1.7 readPatternHashIfExists() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 280 private CredentialHash readPatternHashIfExists (int userId) {281 byte [] stored = readFile(getLockPatternFilename(userId));282 if (!ArrayUtils.isEmpty(stored)) {283 return new CredentialHash (stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,284 CredentialHash.VERSION_GATEKEEPER);285 }286 287 stored = readFile(getBaseZeroLockPatternFilename(userId));288 if (!ArrayUtils.isEmpty(stored)) {289 return CredentialHash.createBaseZeroPattern(stored);290 }291 292 stored = readFile(getLegacyLockPatternFilename(userId));293 if (!ArrayUtils.isEmpty(stored)) {294 return new CredentialHash (stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,295 CredentialHash.VERSION_LEGACY);296 }297 return null ;298 }447 String getLockPatternFilename (int userId) {448 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);449 }75 private static final String SYSTEM_DIRECTORY = "/system/" ;76 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }
1.8 readFile() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 360 private byte [] readFile(String name) {361 int version;362 synchronized (mCache) {363 if (mCache.hasFile(name)) {364 return mCache.peekFile(name);365 }366 version = mCache.getVersion();367 }368 369 RandomAccessFile raf = null ;370 byte [] stored = null ;371 try {372 raf = new RandomAccessFile (name, "r" );373 stored = new byte [(int ) raf.length()];374 raf.readFully(stored, 0 , stored.length);375 raf.close();376 } catch (IOException e) {377 Slog.e(TAG, "Cannot read file " + e);378 } finally {379 if (raf != null ) {380 try {381 raf.close();382 } catch (IOException e) {383 Slog.e(TAG, "Error closing file " + e);384 }385 }386 }387 mCache.putFileIfUnchanged(name, stored, version);388 return stored;389 }802 boolean hasFile (String fileName) {803 return contains(CacheKey.TYPE_FILE, fileName, -1 );804 }876 static final int TYPE_FILE = 1 ;836 private synchronized boolean contains (int type, String key, int userId) {837 return mCache.containsKey(mCacheKey.set(type, key, userId));838 }883 public CacheKey set (int type, String key, int userId) {884 this .type = type;885 this .key = key;886 this .userId = userId;887 return this ;888 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 366 public boolean containsKey (Object key) {367 return indexOfKey(key) >= 0 ;368 }376 public int indexOfKey (Object key) {377 return key = = null ? indexOfNull()378 : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());379 }264 public ArrayMap () {265 this (0 , false );266 }267 268 271 public ArrayMap (int capacity) {272 this (capacity, false );273 }274 275 276 public ArrayMap (int capacity, boolean identityHashCode) {277 mIdentityHashCode = identityHashCode;278 279 280 281 282 if (capacity < 0 ) {283 mHashes = EMPTY_IMMUTABLE_INTS;284 mArray = EmptyArray.OBJECT;285 } else if (capacity == 0 ) {286 mHashes = EmptyArray.INT;287 mArray = EmptyArray.OBJECT;288 } else {289 allocArrays(capacity);290 }291 mSize = 0 ;292 }754 public int hashCode () {755 final int [] hashes = mHashes;756 final Object[] array = mArray;757 int result = 0 ;758 for (int i = 0 , v = 1 , s = mSize; i < s; i++, v+=2 ) {759 Object value = array[v];760 result += hashes[i] ^ (value == null ? 0 : value.hashCode());761 }762 return result;763 }114 int indexOf (Object key, int hash) {115 final int N = mSize;116 117 118 if (N == 0 ) {119 return ~0 ;120 }121 122 int index = binarySearchHashes(mHashes, N, hash);123 124 125 if (index < 0 ) {126 return index;127 }128 129 130 if (key.equals(mArray[index<<1 ])) {131 return index;132 }133 134 135 int end;136 for (end = index + 1 ; end < N && mHashes[end] == hash; end++) {137 if (key.equals(mArray[end << 1 ])) return end;138 }139 140 141 for (int i = index - 1 ; i >= 0 && mHashes[i] == hash; i--) {142 if (key.equals(mArray[i << 1 ])) return i;143 }144 145 146 147 148 149 return ~end;150 }
1.8.1 peekFile() 如果本地没有该文件,则执行peekFile()
。
1 2 3 4 5 6 7 8 798 byte [] peekFile(String fileName) {799 return (byte []) peek(CacheKey.TYPE_FILE, fileName, -1 );800 }840 private synchronized Object peek (int type, String key, int userId) {841 return mCache.get(mCacheKey.set(type, key, userId));842 }
1 2 3 4 5 6 7 8 9 10 419 public V get (Object key) {420 final int index = indexOfKey(key);421 return index >= 0 ? (V)mArray[(index<<1 )+1 ] : null ;422 }376 public int indexOfKey (Object key) {377 return key = = null ? indexOfNull()378 : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());379 }
回到readPatternHashIfExists()
,往下执行。 ^1ead62
1 2 3 4 5 6 7 8 9 10 11 12 280 private CredentialHash readPatternHashIfExists (int userId) {281 byte [] stored = readFile(getLockPatternFilename(userId));282 if (!ArrayUtils.isEmpty(stored)) {283 return new CredentialHash (stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,284 CredentialHash.VERSION_GATEKEEPER);285 }97 static final int VERSION_GATEKEEPER = 1 ;114 public static final int CREDENTIAL_TYPE_PATTERN = 1 ;
1 2 3 4 160 public static boolean isEmpty (@Nullable byte [] array) {161 return array = = null || array.length == 0 ;162 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 99 private CredentialHash (byte [] hash, int type, int version) {100 this (hash, type, version, false );101 }102 103 private CredentialHash (byte [] hash, int type, int version, boolean isBaseZeroPattern) {104 if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {105 if (hash == null ) {106 throw new RuntimeException ("Empty hash for CredentialHash" );107 }108 } else {109 if (hash != null ) {110 throw new RuntimeException ("None type CredentialHash should not have hash" );111 }112 }113 this .hash = hash;114 this .type = type;115 this .version = version;116 this .isBaseZeroPattern = isBaseZeroPattern;117 }
最终回到readCredentialHash()
。[[锁屏密码加密算法分析#1.5 readCredentialHash()]]
1.8.2 RandomAccessFile() 如果有该文件,继续执行readFile()
。
1 2 3 4 5 6 844 private synchronized int getVersion () {845 return mVersion;846 }779 private int mVersion = 0 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 138 public RandomAccessFile (String name, String mode) 139 throws FileNotFoundException140 {141 this (name != null ? new File (name) : null , mode);142 }220 public RandomAccessFile (File file, String mode) 221 throws FileNotFoundException222 {223 String name = (file != null ? file.getPath() : null );224 int imode = -1 ;225 if (mode.equals("r" )) {226 imode = O_RDONLY;... 262 if (name == null ) {263 264 265 throw new NullPointerException ("file == null" );266 }267 if (file.isInvalid()) {268 throw new FileNotFoundException ("Invalid file path" );269 }270 this .path = name;271 this .mode = imode;272 273 274 fd = IoBridge.open(name, imode);275 if (syncMetadata) {276 try {277 fd.sync();278 } catch (IOException e) {279 280 }281 }282 guard.open("close" );283 284 }71 private boolean syncMetadata = false ;
1 2 3 4 5 6 7 8 190 final boolean isInvalid () {191 if (status == null ) {192 status = (this .path.indexOf('\u0000' ) < 0 ) ? PathStatus.CHECKED193 : PathStatus.INVALID;194 }195 return status = = PathStatus.INVALID;196 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 493 public static FileDescriptor open (String path, int flags) throws FileNotFoundException {494 FileDescriptor fd = null ;495 try {496 497 int mode = ((flags & O_ACCMODE) == O_RDONLY) ? 0 : 0600 ;498 fd = Libcore.os.open(path, flags, mode);499 500 501 if (S_ISDIR(Libcore.os.fstat(fd).st_mode)) {502 throw new ErrnoException ("open" , EISDIR);503 }504 return fd;505 } catch (ErrnoException errnoException) {506 try {507 if (fd != null ) {508 IoUtils.close(fd);509 }510 } catch (IOException ignored) {511 }512 FileNotFoundException ex = new FileNotFoundException (path + ": " + errnoException.getMessage());513 ex.initCause(errnoException);514 throw ex;515 }516 }
读好以后将返回值存到stored字节数组中。[[锁屏密码加密算法分析#1.8 readFile()]]
1.8.3 readFully() 将stored字节数组存入到一个随机访问文件当中。
1 2 3 4 5 6 7 8 9 10 11 12 193 public final void readFully (byte b[], int off, int len) throws IOException {194 if (len < 0 )195 throw new IndexOutOfBoundsException ();196 int n = 0 ;197 while (n < len) {198 int count = in.read(b, off + n, len - n);199 if (count < 0 )200 throw new EOFException ();201 n += count;202 }203 }
最后返回stored到readPatternHashIfExists()
,新建一个实例CredentialHash()
。[[锁屏密码加密算法分析#^1ead62]]
这样就可以得到patternHash,也就是storedHash了。
但经过查找目录发现手机中并没有/data/system/gatekeeper.pattern.key
。那就进入getBaseZeroLockPatternFilename()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 466 private String getBaseZeroLockPatternFilename (int userId) {467 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);468 }77 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }
也没有/data/system/gatekeeper.gesture.key
文件,继续看getLegacyLockPatternFilename()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 457 String getLegacyLockPatternFilename (int userId) {458 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);459 }78 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }
还是没有啊…
使用Android 5.0系统的模拟器,发现确实有gesture.key
,而且与我们算的数据一样。[[锁屏密码加密算法分析#^7da60a]] 也就是说Android 8.1.0虽然保留了这些函数,但是没有用到,而是用的另一套API来实现密码功能。
2. 数字密码加密算法分析 2.1 passwordToHash() 在我们分析手势密码的过程中应该见到不少有关数字密码的信息,所以直接找passwordToHash()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 1091 public byte [] passwordToHash(String password, int userId) {1092 if (password == null ) {1093 return null ;1094 }1095 1096 try {1097 byte [] saltedPassword = (password + getSalt(userId)).getBytes();1098 byte [] sha1 = MessageDigest.getInstance("SHA-1" ).digest(saltedPassword);1099 byte [] md5 = MessageDigest.getInstance("MD5" ).digest(saltedPassword);1100 1101 byte [] combined = new byte [sha1.length + md5.length];1102 System.arraycopy(sha1, 0 , combined, 0 , sha1.length);1103 System.arraycopy(md5, 0 , combined, sha1.length, md5.length);1104 1105 final char [] hexEncoded = HexEncoding.encode(combined);1106 return new String (hexEncoded).getBytes(StandardCharsets.UTF_8);1107 } catch (NoSuchAlgorithmException e) {1108 throw new AssertionError ("Missing digest algorithm: " , e);1109 }1110 }
传入的参数password就是我们输入的数字密码,密码与getSalt(userId)
拼接转换为字节数组赋值给saltedPassword。对saltedPassword进行SHA-1和MD5加密,将加密后的结果拼接转换为十六进制的形式。
2.2 getSalt() 看getSalt()
的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1067 private String getSalt (int userId) {1068 long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0 , userId);1069 if (salt == 0 ) {1070 try {1071 salt = SecureRandom.getInstance("SHA1PRNG" ).nextLong();1072 setLong(LOCK_PASSWORD_SALT_KEY, salt, userId);1073 Log.v(TAG, "Initialized lock password salt for user: " + userId);1074 } catch (NoSuchAlgorithmException e) {1075 1076 throw new IllegalStateException ("Couldn't get SecureRandom number" , e);1077 }1078 }1079 return Long.toHexString(salt);1080 }131 public final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt" ;
2.3 getLong() 继续看getLong()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1286 private long getLong (String secureSettingKey, long defaultValue, int userHandle) {1287 try {1288 return getLockSettings().getLong(secureSettingKey, defaultValue, userHandle);1289 } catch (RemoteException re) {1290 return defaultValue;1291 }1292 }256 public ILockSettings getLockSettings () {257 if (mLockSettingsService == null ) {258 ILockSettings service = ILockSettings.Stub.asInterface(259 ServiceManager.getService("lock_settings" ));260 mLockSettingsService = service;261 }262 return mLockSettingsService;263 }
通过在ServiceManager中获取一个服务来进行操作,在Android中,像这种获取服务的方式最终实现逻辑都是在XXXService类中,这里是LockSettingsService.java
类,找到这个类,查看它的getLong()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 929 public long getLong (String key, long defaultValue, int userId) {930 checkReadPermission(key, userId);931 String value = getStringUnchecked(key, null , userId);932 return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value);933 }941 public String getStringUnchecked (String key, String defaultValue, int userId) {942 if (Settings.Secure.LOCK_PATTERN_ENABLED.equals(key)) {943 long ident = Binder.clearCallingIdentity();944 try {945 return mLockPatternUtils.isLockPatternEnabled(userId) ? "1" : "0" ;946 } finally {947 Binder.restoreCallingIdentity(ident);948 }949 }950 951 if (userId == USER_FRP) {952 return getFrpStringUnchecked(key);953 }954 955 if (LockPatternUtils.LEGACY_LOCK_PATTERN_ENABLED.equals(key)) {956 key = Settings.Secure.LOCK_PATTERN_ENABLED;957 }958 959 return mStorage.readKeyValue(key, defaultValue, userId);960 }
由于我们的key值是lockscreen.password_salt
,所以执行mStorage.readKeyValue()
。mStorage的数据类型是LockSettingsStorage。
2.4 readKeyValue() 查看这个类的readKeyValue()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 212 public String readKeyValue (String key, String defaultValue, int userId) {213 int version;214 synchronized (mCache) {215 if (mCache.hasKeyValue(key, userId)) {216 return mCache.peekKeyValue(key, defaultValue, userId);217 }218 version = mCache.getVersion();219 }220 221 Cursor cursor;222 Object result = DEFAULT;223 SQLiteDatabase db = mOpenHelper.getReadableDatabase();224 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,225 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?" ,226 new String [] { Integer.toString(userId), key },227 null , null , null )) != null ) {228 if (cursor.moveToFirst()) {229 result = cursor.getString(0 );230 }231 cursor.close();232 }233 mCache.putKeyValueIfUnchanged(key, result, userId, version);234 return result = = DEFAULT ? defaultValue : (String) result;235 }60 private static final String TAG = "LockSettingsStorage" ;61 private static final String TABLE = "locksettings" ;62 private static final boolean DEBUG = false ;63 64 private static final String COLUMN_KEY = "name" ;65 private static final String COLUMN_USERID = "user" ;66 private static final String COLUMN_VALUE = "value" ;67 68 private static final String[] COLUMNS_FOR_QUERY = {69 COLUMN_VALUE70 };71 private static final String[] COLUMNS_FOR_PREFETCH = {72 COLUMN_KEY, COLUMN_VALUE73 };
从数据库中取出符合条件的第一条记录,即userId为0,key为lockscreen.password_salt
的一条记录。
层层回调,这个value从字符串转换成了长整型,回调到getSalt()
中,再将这个值转换为十六进制字符串的形式。最后再进行加密操作。
2.5 readPasswordHashIfExists() 现在要找哪里调用了passwordToHash()
,同样是在verifyCredential()
中被调用的,设置的password也是用storedHash存储。跟手势密码一样来到readCredentialHash()
。不过数字密码调用的是readPasswordHashIfExists()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 265 private CredentialHash readPasswordHashIfExists (int userId) {266 byte [] stored = readFile(getLockPasswordFilename(userId));267 if (!ArrayUtils.isEmpty(stored)) {268 return new CredentialHash (stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,269 CredentialHash.VERSION_GATEKEEPER);270 }271 272 stored = readFile(getLegacyLockPasswordFilename(userId));273 if (!ArrayUtils.isEmpty(stored)) {274 return new CredentialHash (stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,275 CredentialHash.VERSION_LEGACY);276 }277 return null ;278 }451 @VisibleForTesting 452 String getLockPasswordFilename (int userId) {453 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);454 }79 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }
没有,继续看getLegacyLockPasswordFilename()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 462 String getLegacyLockPasswordFilename (int userId) {463 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);464 }80 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }
还是没有…算了,我们先假设设置的密码为1234,其它的就将源码照搬过来:
接下来就是验证结果与password.key
或gatekeeper.password.key
中的值是否相等。
由于Android 8.1.0中没有这些文件,又Android 5.1的passwordToHash()
与Android 8.1.0的源码有出入,这里就不再进行演示了。
3. 手势密码保存算法分析 3.1 saveLockPattern() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 658 663 public void saveLockPattern (List<LockPatternView.Cell> pattern, int userId) {664 this .saveLockPattern(pattern, null , userId);665 }666 672 public void saveLockPattern (List<LockPatternView.Cell> pattern, String savedPattern, int userId) {673 try {674 if (pattern == null || pattern.size() < MIN_LOCK_PATTERN_SIZE) {675 throw new IllegalArgumentException ("pattern must not be null and at least " 676 + MIN_LOCK_PATTERN_SIZE + " dots long." );677 }678 679 setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);680 getLockSettings().setLockCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN,681 savedPattern, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);682 683 684 if (userId == UserHandle.USER_SYSTEM685 && LockPatternUtils.isDeviceEncryptionEnabled()) {686 if (!shouldEncryptWithCredentials(true )) {687 clearEncryptionPassword();688 } else {689 String stringPattern = patternToString(pattern);690 updateEncryptionPassword(StorageManager.CRYPT_TYPE_PATTERN, stringPattern);691 }692 }693 694 reportPatternWasChosen(userId);695 onAfterChangingPassword(userId);696 } catch (RemoteException re) {697 Log.e(TAG, "Couldn't save lock pattern " + re);698 }699 }
3.2 patternToString() patternToString()
是1.3的stringToPattern()
的逆算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1012 public static String patternToString (List<LockPatternView.Cell> pattern) {1013 if (pattern == null ) {1014 return "" ;1015 }1016 final int patternSize = pattern.size();1017 1018 byte [] res = new byte [patternSize];1019 for (int i = 0 ; i < patternSize; i++) {1020 LockPatternView.Cell cell = pattern.get(i);1021 res[i] = (byte ) (cell.getRow() * 3 + cell.getColumn() + '1' );1022 }1023 return new String (res);1024 }
3.3 setLockCredential() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1217 1218 1219 @Override 1220 public void setLockCredential (String credential, int type, String savedCredential, 1221 int requestedQuality, int userId)1222 throws RemoteException {1223 checkWritePermission(userId);1224 synchronized (mSeparateChallengeLock) {1225 setLockCredentialInternal(credential, type, savedCredential, requestedQuality, userId);1226 setSeparateProfileChallengeEnabled(userId, true , null );1227 notifyPasswordChanged(userId);1228 }1229 }
3.4 setLockCredentialInternal() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1231 private void setLockCredentialInternal (String credential, int credentialType, 1232 String savedCredential, int requestedQuality, int userId) throws RemoteException {... 1298 if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId);1299 byte [] enrolledHandle = enrollCredential(currentHandle.hash, savedCredential, credential,1300 userId);1301 if (enrolledHandle != null ) {1302 CredentialHash willStore = CredentialHash.create(enrolledHandle, credentialType);1303 mStorage.writeCredentialHash(willStore, userId);1304 1305 GateKeeperResponse gkResponse = getGateKeeperService()1306 .verifyChallenge(userId, 0 , willStore.hash, credential.getBytes());1307 setUserKeyProtection(userId, credential, convertResponse(gkResponse));1308 fixateNewestUserKeyAuth(userId);1309 1310 doVerifyCredential(credential, credentialType, true , 0 , userId, null );1311 synchronizeUnifiedWorkChallengeForProfiles(userId, null );1312 } else {1313 throw new RemoteException ("Failed to enroll " +1314 (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD ? "password" 1315 : "pattern" ));1316 }1317 }
3.5 enrollCredential() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 1384 private byte [] enrollCredential(byte [] enrolledHandle,1385 String enrolledCredential, String toEnroll, int userId)1386 throws RemoteException {1387 checkWritePermission(userId);1388 byte [] enrolledCredentialBytes = enrolledCredential == null 1389 ? null 1390 : enrolledCredential.getBytes();1391 byte [] toEnrollBytes = toEnroll == null 1392 ? null 1393 : toEnroll.getBytes();1394 GateKeeperResponse response = getGateKeeperService().enroll(userId, enrolledHandle,1395 enrolledCredentialBytes, toEnrollBytes);1396 1397 if (response == null ) {1398 return null ;1399 }1400 1401 byte [] hash = response.getPayload();1402 if (hash != null ) {1403 setKeystorePassword(toEnroll, userId);1404 } else {1405 1406 Slog.e(TAG, "Throttled while enrolling a password" );1407 }1408 return hash;1409 }
3.6 enroll() 这里如何注册,给了一个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 31 41 GateKeeperResponse enroll (int uid, in byte [] currentPasswordHandle, in byte [] currentPassword, 42 in byte [] desiredPassword) ;
3.7 setKeystorePassword() 1 2 3 4 5 1016 private void setKeystorePassword (String password, int userHandle) {1017 final KeyStore ks = KeyStore.getInstance();1018 ks.onUserPasswordChanged(userHandle, password);1019 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 575 public boolean onUserPasswordChanged (int userId, String newPassword) {576 577 578 if (newPassword == null ) {579 newPassword = "" ;580 }581 try {582 return mBinder.onUserPasswordChanged(userId, newPassword) == NO_ERROR;583 } catch (RemoteException e) {584 Log.w(TAG, "Cannot connect to keystore" , e);585 return false ;586 }587 }
1 2 41 int onUserPasswordChanged (int userId, String newPassword) ;
3.8 create() 注册完凭据,继续setLockCredentialInternal()
往下执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 124 static CredentialHash create (byte [] hash, int type) {125 if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) {126 throw new RuntimeException ("Bad type for CredentialHash" );127 }128 return new CredentialHash (hash, type, VERSION_GATEKEEPER);129 }97 static final int VERSION_GATEKEEPER = 1 ;99 private CredentialHash (byte [] hash, int type, int version) {100 this (hash, type, version, false );101 }102 103 private CredentialHash (byte [] hash, int type, int version, boolean isBaseZeroPattern) {104 if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {105 if (hash == null ) {106 throw new RuntimeException ("Empty hash for CredentialHash" );107 }108 } else {109 if (hash != null ) {110 throw new RuntimeException ("None type CredentialHash should not have hash" );111 }112 }113 this .hash = hash;114 this .type = type;115 this .version = version;116 this .isBaseZeroPattern = isBaseZeroPattern;117 }
3.9 writeCredentialHash() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 433 public void writeCredentialHash (CredentialHash hash, int userId) {434 byte [] patternHash = null ;435 byte [] passwordHash = null ;436 437 if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {438 passwordHash = hash.hash;439 } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {440 patternHash = hash.hash;441 }442 writeFile(getLockPasswordFilename(userId), passwordHash);443 writeFile(getLockPatternFilename(userId), patternHash);444 }445 446 @VisibleForTesting 447 String getLockPatternFilename (int userId) {448 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);449 }76 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key" ;475 private String getLockCredentialFilePathForUser (int userId, String basename) {476 String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +477 SYSTEM_DIRECTORY;478 if (userId == 0 ) {479 480 return dataSystemDirectory + basename;481 } else {482 return new File (Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();483 }484 }75 private static final String SYSTEM_DIRECTORY = "/system/" ;391 private void writeFile (String name, byte [] hash) {392 synchronized (mFileWriteLock) {393 RandomAccessFile raf = null ;394 try {395 396 397 398 399 raf = new RandomAccessFile (name, "rws" );400 401 if (hash == null || hash.length == 0 ) {402 raf.setLength(0 );403 } else {404 raf.write(hash, 0 , hash.length);405 }406 raf.close();407 } catch (IOException e) {408 Slog.e(TAG, "Error writing to file " + e);409 } finally {410 if (raf != null ) {411 try {412 raf.close();413 } catch (IOException e) {414 Slog.e(TAG, "Error closing file " + e);415 }416 }417 }418 mCache.putFile(name, hash);419 }420 }806 void putFile (String key, byte [] value) {807 put(CacheKey.TYPE_FILE, key, value, -1 );808 }876 static final int TYPE_FILE = 1 ;823 private synchronized void put (int type, String key, Object value, int userId) {824 825 mCache.put(new CacheKey ().set(type, key, userId), value);826 mVersion++;827 }883 public CacheKey set (int type, String key, int userId) {884 this .type = type;885 this .key = key;886 this .userId = userId;887 return this ;888 }
继续返回setLockCredentialInternal()
。
3.10 verifyChallenge() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 54 65 GateKeeperResponse verifyChallenge (int uid, long challenge, in byte [] enrolledPasswordHandle, 66 in byte [] providedPassword) ;
4. 疑惑点 4.1 找不到对应文件 话说在3.9 writeCredentialHash()时,把密钥hash写到了/data/system/gatekeeper.pattern.key
中,而且在1.5 readCredentialHash()中,也需要将密钥从/data/system/gatekeeper.pattern.key
取出来,所以为什么在手机中没有找到这个文件呢?
4.2 接口函数 在3中,每次走到如何加密明文时,都是启动一个服务,给它发送明文,返回的响应请求就是密文了。这些函数难道是接口函数,让开发者自定义的吗?还是从源码中就能找到,只是我没找到?可是既然有源码,所以这些函数的实现肯定是Android源码中的,我是真没找到啊