Android 8.1.0锁屏密码加密算法分析

看了姜维大佬写的《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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternView.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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-shmlocksettings.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
//  /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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 // Leave it in the same place for user 0
// /data/system/gatekeeper.pattern.key
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
360 private byte[] readFile(String name) {//name="/data/system/gatekeeper.pattern.key"
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 /* userId */);
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
// /frameworks/base/core/java/android/util/ArrayMap.java
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 /**
269 * Create a new ArrayMap with a given initial capacity.
270 */
271 public ArrayMap(int capacity) {
272 this(capacity, false);
273 }
274
275 /** {@hide} */
276 public ArrayMap(int capacity, boolean identityHashCode) {
277 mIdentityHashCode = identityHashCode;//mIdentityHashCode=false
278
279 // If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
280 // instance instead of the usual EmptyArray.INT. The reference
281 // is checked later to see if the array is allowed to grow.
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 // Important fast case: if nothing is in here, nothing to look for.
118 if (N == 0) {
119 return ~0;
120 }
121
122 int index = binarySearchHashes(mHashes, N, hash);
123
124 // If the hash code wasn't found, then we have no entry for this key.
125 if (index < 0) {
126 return index;
127 }
128
129 // If the key at the returned index matches, that's what we want.
130 if (key.equals(mArray[index<<1])) {
131 return index;
132 }
133
134 // Search for a matching key after the index.
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 // Search for a matching key before the index.
141 for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
142 if (key.equals(mArray[i << 1])) return i;
143 }
144
145 // Key not found -- return negative value indicating where a
146 // new entry for this key should go. We use the end of the
147 // hash chain to reduce the number of array entries that will
148 // need to be copied when inserting.
149 return ~end;
150 }

1.8.1 peekFile()

如果本地没有该文件,则执行peekFile()

1
2
3
4
5
6
7
8
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
798 byte[] peekFile(String fileName) {
799 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
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
// /frameworks/base/core/java/android/util/ArrayMap.java
419 public V get(Object key) {
420 final int index = indexOfKey(key);
421 return index >= 0 ? (V)mArray[(index<<1)+1] : null;
422 }//返回(V)mArray[3]

376 public int indexOfKey(Object key) {
377 return key == null ? indexOfNull()
378 : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());//返回值为1
379 }

回到readPatternHashIfExists(),往下执行。 ^1ead62

1
2
3
4
5
6
7
8
9
10
11
12
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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;

// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
114 public static final int CREDENTIAL_TYPE_PATTERN = 1;
1
2
3
4
// /frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
99 private CredentialHash(byte[] hash, int type, int version) {
100 this(hash, type, version, false /* isBaseZeroPattern */);
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 /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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
// /libcore/ojluni/src/main/java/java/io/RandomAccessFile.java
138 public RandomAccessFile(String name, String mode)
139 throws FileNotFoundException
140 {
141 this(name != null ? new File(name) : null, mode);
142 }

220 public RandomAccessFile(File file, String mode)
221 throws FileNotFoundException
222 {
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 // Android-changed: different exception message in ctor when file == null.
264 // throw new NullPointerException();
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 // BEGIN Android-changed: Use IoBridge.open() instead of open.
274 fd = IoBridge.open(name, imode);
275 if (syncMetadata) {
276 try {
277 fd.sync();
278 } catch (IOException e) {
279 // Ignored
280 }
281 }
282 guard.open("close");
283 // END Android-changed: Use IoBridge.open() instead of open.
284 }

71 private boolean syncMetadata = false;
1
2
3
4
5
6
7
8
// /libcore/ojluni/src/main/java/java/io/File.java
190 final boolean isInvalid() {
191 if (status == null) {
192 status = (this.path.indexOf('\u0000') < 0) ? PathStatus.CHECKED
193 : 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
// /libcore/luni/src/main/java/libcore/io/IoBridge.java
493 public static FileDescriptor open(String path, int flags) throws FileNotFoundException {
494 FileDescriptor fd = null;
495 try {
496 // On Android, we don't want default permissions to allow global access.
497 int mode = ((flags & O_ACCMODE) == O_RDONLY) ? 0 : 0600;
498 fd = Libcore.os.open(path, flags, mode);//以“r”权限打开文件
499 // Posix open(2) fails with EISDIR only if you ask for write permission.
500 // Java disallows reading directories too.
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
// /libcore/ojluni/src/main/java/java/io/DataInputStream.java
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 // Leave it in the same place for user 0
// /data/system/gatekeeper.gesture.key
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 // Leave it in the same place for user 0
// /data/system/gesture.key
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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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 // Throw an exception rather than storing a password we'll never be able to recover
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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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_VALUE
70 };
71 private static final String[] COLUMNS_FOR_PREFETCH = {
72 COLUMN_KEY, COLUMN_VALUE
73 };

从数据库中取出符合条件的第一条记录,即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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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 // Leave it in the same place for user 0
// /data/system/gatekeeper.password.key
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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 // Leave it in the same place for user 0
// /data/system/password.key
480 return dataSystemDirectory + basename;
481 } else {
482 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
483 }
484 }

还是没有…算了,我们先假设设置的密码为1234,其它的就将源码照搬过来:

接下来就是验证结果与password.keygatekeeper.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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
658 /**
659 * Save a lock pattern.
660 * @param pattern The new pattern to save.
661 * @param userId the user whose pattern is to be saved.
662 */
663 public void saveLockPattern(List<LockPatternView.Cell> pattern, int userId) {
664 this.saveLockPattern(pattern, null, userId);
665 }
666 /**
667 * Save a lock pattern.
668 * @param pattern The new pattern to save.
669 * @param savedPattern The previously saved pattern, converted to String format
670 * @param userId the user whose pattern is to be saved.
671 */
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);//在locksettings.db中name字段为[PASSWORD_TYPE_KEY],则将它的value字段修改为[DevicePolicyManager.PASSWORD_QUALITY_SOMETHING]
680 getLockSettings().setLockCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN,
681 savedPattern, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
682
683 // Update the device encryption password.
684 if (userId == UserHandle.USER_SYSTEM
685 && 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
// /frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
1217 // This method should be called by LockPatternUtil only, all internal methods in this class
1218 // should call setLockCredentialInternal.
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
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);//存储hash
1304 // push new secret and auth token to vold
1305 GateKeeperResponse gkResponse = getGateKeeperService()
1306 .verifyChallenge(userId, 0, willStore.hash, credential.getBytes());//验证willStore.hash与GateKeeperService返回的请求payload是否一致
1307 setUserKeyProtection(userId, credential, convertResponse(gkResponse));
1308 fixateNewestUserKeyAuth(userId);
1309 // Refresh the auth token
1310 doVerifyCredential(credential, credentialType, true, 0, userId, null /* progressCallback */);
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
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);//启动GateKeeperService服务,返回响应请求
1396
1397 if (response == null) {
1398 return null;
1399 }
1400
1401 byte[] hash = response.getPayload();//响应中的payload就是明文加密后的密文数组
1402 if (hash != null) {
1403 setKeystorePassword(toEnroll, userId);//将明文字节数组存入Keystore
1404 } else {
1405 // Should not happen
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
// /frameworks/base/core/java/android/service/gatekeeper/IGateKeeperService.aidl
31 /**
32 * Enrolls a password, returning the handle to the enrollment to be stored locally.
33 * @param uid The Android user ID associated to this enrollment
34 * @param currentPasswordHandle The previously enrolled handle, or null if none
35 * @param currentPassword The previously enrolled plaintext password, or null if none.
36 * If provided, must verify against the currentPasswordHandle.
37 * @param desiredPassword The new desired password, for which a handle will be returned
38 * upon success.
39 * @return an EnrollResponse or null on failure
40 */
41 GateKeeperResponse enroll(int uid, in byte[] currentPasswordHandle, in byte[] currentPassword,
42 in byte[] desiredPassword);

3.7 setKeystorePassword()

1
2
3
4
5
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java
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
// /frameworks/base/keystore/java/android/security/KeyStore.java
575 public boolean onUserPasswordChanged(int userId, String newPassword) {
576 // Parcel.cpp doesn't support deserializing null strings and treats them as "". Make that
577 // explicit here.
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
// /frameworks/base/core/java/android/security/IKeystoreService.aidl
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 /* isBaseZeroPattern */);
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 /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
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
// /frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
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 // Leave it in the same place for user 0
// /data/system/gatekeeper.pattern.key
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 // Write the hash to file, requiring each write to be synchronized to the
396 // underlying storage device immediately to avoid data loss in case of power loss.
397 // This also ensures future secdiscard operation on the file succeeds since the
398 // file would have been allocated on flash.
399 raf = new RandomAccessFile(name, "rws");
400 // Truncate the file if pattern is null, to clear the lock
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 /* userId */);
808 }

876 static final int TYPE_FILE = 1;

823 private synchronized void put(int type, String key, Object value, int userId) {
824 // Create a new CachKey here because it may be saved in the map if the key is absent.
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
// /frameworks/base/core/java/android/service/gatekeeper/IGateKeeperService.aidl
54 /**
55 * Verifies an enrolled handle against a provided, plaintext blob.
56 * @param uid The Android user ID associated to this enrollment
57 * @param challenge a challenge to authenticate agaisnt the device credential. If successful
58 * authentication occurs, this value will be written to the returned
59 * authentication attestation.
60 * @param enrolledPasswordHandle The handle against which the provided password will be
61 * verified.
62 * @param The plaintext blob to verify against enrolledPassword.
63 * @return a VerifyResponse with an attestation, or null on failure.
64 */
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源码中的,我是真没找到啊