OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.webapk.lib.client; | |
6 | |
7 import static java.nio.ByteOrder.BIG_ENDIAN; | |
8 import static java.nio.ByteOrder.LITTLE_ENDIAN; | |
9 | |
10 import android.support.annotation.IntDef; | |
11 import android.util.Log; | |
12 | |
13 import java.io.UnsupportedEncodingException; | |
14 import java.nio.ByteBuffer; | |
15 import java.security.InvalidKeyException; | |
16 import java.security.NoSuchAlgorithmException; | |
17 import java.security.PublicKey; | |
18 import java.security.Signature; | |
19 import java.security.SignatureException; | |
20 import java.util.ArrayList; | |
21 import java.util.Collections; | |
22 import java.util.regex.Matcher; | |
23 import java.util.regex.Pattern; | |
24 | |
25 /** | |
26 * WebApkVerifySignature reads in the APK file and verifies the WebApk signature . It reads the | |
27 * signature from the zip comment and verifies that it was signed by the public key passed. | |
28 */ | |
29 public class WebApkVerifySignature { | |
30 private static final String TAG = "WebApkVerifySignature"; | |
31 | |
32 /** End Of Central Directory Signature */ | |
33 private static final long EOCD_SIG = 0x06054b50; | |
34 | |
35 /** Central Directory Signature */ | |
36 private static final long CD_SIG = 0x02014b50; | |
37 | |
38 /** Local File Header Signature */ | |
39 private static final long LFH_SIG = 0x04034b50; | |
40 | |
41 /** Max end-of-central-directory size, including variable length file commen t.. */ | |
pkotwicz
2017/03/31 03:59:25
How about: "Minimum end-of-central-directory size.
ScottK
2017/04/03 17:44:19
Done.
| |
42 private static final int MIN_EOCD_SIZE = 22; | |
43 | |
44 /** Max local file header size, including long filename. */ | |
45 private static final int MAX_HEADER_SIZE = 64 * 8192; | |
46 | |
47 /** Maximum number of META-INF/ files (allowing for dual signing). */ | |
48 private static final int MAX_META_INF_FILES = 5; | |
49 | |
50 /** The signature algorithm used (must also match with HASH) */ | |
51 private static final String SIGNING_ALGORITHM = "SHA256withECDSA"; | |
52 | |
53 /** | |
54 * The pattern we look for in the APK/zip comment for singing key. | |
55 * An example is "webapk:0000:<hexvalues>". This pattern can appear anywher in the comment but | |
56 * must be separated from any other parts with a separator that doesn't look like a hex | |
57 * character. | |
pkotwicz
2017/03/31 03:59:26
Cool! The signature checking code has a "singing f
ScottK
2017/04/03 17:44:18
Done.
| |
58 */ | |
59 private static final Pattern WEBAPK_COMMENT_PATTERN = | |
60 Pattern.compile("webapk:(\\d+):([a-fA-F0-9]+)"); | |
pkotwicz
2017/03/31 03:59:24
Can you remove the brackets around \\d+ (You don't
| |
61 | |
62 /** Maximum comment length permitted */ | |
63 private static final int MAX_COMMENT_LENGTH = 0; | |
64 | |
65 /** Maximum extra field length permitted */ | |
66 private static final int MAX_EXTRA_LENGTH = 8; | |
67 | |
68 /** The memory buffer we are going to read the zip from */ | |
pkotwicz
2017/03/31 03:59:25
Nit: "." at the end of the sentence (Here and in o
ScottK
2017/04/03 17:44:18
Done.
ScottK
2017/04/03 17:44:19
Done.
| |
69 private final ByteBuffer mBuffer; | |
70 | |
71 /** Number of total central directory (zip entry) records */ | |
72 private int mRecordCount; | |
73 | |
74 /** Byte offset from the start where the central directory is found */ | |
75 private int mCentralDirOffset; | |
76 | |
77 /** The zip archive comment as a UTF-8 strings */ | |
pkotwicz
2017/03/31 03:59:25
"strings" -> "string."
ScottK
2017/04/03 17:44:19
Done.
| |
78 private String mComment; | |
79 | |
80 /** Errors codes */ | |
81 @IntDef({ | |
82 ERROR_OK, ERROR_BAD_APK, ERROR_EXTRA_FIELD_TOO_LARGE, ERROR_COMMENT_ TOO_LARGE, | |
83 ERROR_INCORRECT_SIGNATURE, ERROR_SIGNATURE_NOT_FOUND, ERROR_TOO_MANY _META_INF_FILES, | |
84 }) | |
85 public @interface Error {} | |
86 public static final int ERROR_OK = 0; | |
87 public static final int ERROR_BAD_APK = 1; | |
88 public static final int ERROR_EXTRA_FIELD_TOO_LARGE = 2; | |
89 public static final int ERROR_COMMENT_TOO_LARGE = 3; | |
90 public static final int ERROR_INCORRECT_SIGNATURE = 4; | |
91 public static final int ERROR_SIGNATURE_NOT_FOUND = 5; | |
92 public static final int ERROR_TOO_MANY_META_INF_FILES = 6; | |
pkotwicz
2017/03/31 03:59:24
Nit: public static constants should be declared fi
ScottK
2017/04/03 17:44:19
Moved to the top.
| |
93 | |
94 /** | |
95 * Sorted list of 'blocks' of memory we will cryptographically hash. We sort the blocks by | |
96 * filename to ensure a repeatable order. | |
97 */ | |
98 private ArrayList<Block> mBlocks; | |
99 | |
100 /** Block is the offset and size of a compressed zip entry. */ | |
pkotwicz
2017/03/31 03:59:26
How about: Block contains meta data about a zip en
ScottK
2017/04/03 17:44:20
Done.
| |
101 private static class Block implements Comparable<Block> { | |
102 Block(String filename, int position, int compressedSize) { | |
103 mFilename = filename; | |
104 mPosition = position; | |
105 mHeaderSize = 0; | |
106 mCompressedSize = compressedSize; | |
107 } | |
108 | |
109 /** added for Comparable, sort lexicographically. */ | |
pkotwicz
2017/03/31 03:59:25
Nit: added -> Added
ScottK
2017/04/03 17:44:20
Done.
| |
110 @Override | |
111 public int compareTo(Block o) { | |
112 return mFilename.compareTo(o.mFilename); | |
113 } | |
114 | |
115 @Override | |
116 public boolean equals(Object o) { | |
117 if (!(o instanceof Block)) return false; | |
118 return mFilename.equals(((Block) o).mFilename); | |
119 } | |
pkotwicz
2017/03/31 03:59:24
Nit: New line
ScottK
2017/04/03 17:44:20
Done.
| |
120 @Override | |
121 public int hashCode() { | |
122 return mFilename.hashCode(); | |
123 } | |
124 | |
125 String mFilename; | |
126 int mPosition; | |
127 int mHeaderSize; | |
128 int mCompressedSize; | |
pkotwicz
2017/03/31 03:59:26
Member variables should come before functions.
ScottK
2017/04/03 17:44:19
Getting confused with C++ :-)
| |
129 } | |
130 | |
131 /** CTOR simply 'connects' to buffer passed. */ | |
pkotwicz
2017/03/31 03:59:26
CTOR -> Constructor
ScottK
2017/04/03 17:44:18
Done.
| |
132 public WebApkVerifySignature(ByteBuffer buffer) { | |
133 mBuffer = buffer; | |
134 mBuffer.order(LITTLE_ENDIAN); | |
135 } | |
136 | |
137 /** | |
138 * Read in the comment and directory. If there is no parseable comment we wo n't read the | |
139 * directory as there is no point (for speed). On success, all of our privat e variables will be | |
140 * set. | |
141 * | |
pkotwicz
2017/03/31 03:59:26
Nit: remove new line (here and in other function c
ScottK
2017/04/03 17:44:18
Done.
| |
142 * @return OK on success. | |
143 */ | |
144 public int read() { | |
145 try { | |
146 @Error | |
pkotwicz
2017/03/31 03:59:25
I haven't seen other places which annotate ints wh
ScottK
2017/04/03 17:44:19
Removed.
| |
147 int err = readEOCD(); | |
148 if (err != ERROR_OK) { | |
149 Log.d(TAG, "Missing EOCD Signature"); | |
pkotwicz
2017/03/31 03:59:25
Log is unnecessary because calling function will l
ScottK
2017/04/03 17:44:19
Removed.
| |
150 return err; | |
151 } | |
152 // Short circuit if no comment found. | |
153 if (parseCommentSignature(mComment) == null) { | |
154 return ERROR_SIGNATURE_NOT_FOUND; | |
155 } | |
156 err = readDirectory(); | |
157 if (err != ERROR_OK) { | |
158 Log.d(TAG, "Error reading directory"); | |
pkotwicz
2017/03/31 03:59:26
Log is unnecessary because calling function will l
ScottK
2017/04/03 17:44:19
Removed.
| |
159 return err; | |
160 } | |
161 } catch (Exception e) { | |
162 Log.e(TAG, "Error reading directory", e); | |
163 return ERROR_BAD_APK; | |
164 } | |
165 return ERROR_OK; | |
166 } | |
167 | |
168 /** | |
169 * verifySignature hashes all the files and then verifies the signature. | |
170 * | |
171 * @param pub The public key that it should be verified against. | |
172 * @return ERROR_OK if the public key signature verifies. | |
173 * @throws SignatureException for various reasons. | |
pkotwicz
2017/03/31 03:59:25
This function should not throw an exception. If an
ScottK
2017/04/03 17:44:19
Done.
| |
174 */ | |
175 public int verifySignature(PublicKey pub) throws SignatureException { | |
176 byte[] sig = parseCommentSignature(mComment); | |
177 if (sig == null || sig.length == 0) { | |
178 return ERROR_SIGNATURE_NOT_FOUND; | |
179 } | |
180 try { | |
181 Signature signature = Signature.getInstance(SIGNING_ALGORITHM); | |
182 signature.initVerify(pub); | |
183 @Error | |
184 int err = calculateHash(signature); | |
185 if (err != ERROR_OK) { | |
186 return err; | |
187 } | |
188 return signature.verify(sig) ? ERROR_OK : ERROR_INCORRECT_SIGNATURE; | |
189 } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureExcep tion | |
190 | UnsupportedEncodingException e) { | |
pkotwicz
2017/03/31 03:59:25
catch (Exception e) {} instead for the sake of san
ScottK
2017/04/03 17:44:20
Done.
| |
191 throw new SignatureException( | |
192 "Failed to verify generated signature using public key from certificate", e); | |
193 } | |
194 } | |
195 | |
196 /** @return Full APK/zip comment string. */ | |
197 public String comment() { | |
198 return mComment; | |
199 } | |
200 | |
201 /** | |
202 * calculateHash goes through each file listed in blocks and calculates the SHA-256 | |
pkotwicz
2017/03/31 03:59:24
calculateHash -> calculateHash()
ScottK
2017/04/03 17:44:19
Done.
| |
203 * cryptographic hash. | |
204 * | |
205 * @param sig Signature object you can call update on. | |
206 */ | |
207 public int calculateHash(Signature sig) | |
208 throws SignatureException, IllegalArgumentException, UnsupportedEnco dingException { | |
pkotwicz
2017/03/31 03:59:25
This function should throw Exception only for the
ScottK
2017/04/03 17:44:19
Done.
| |
209 byte[] filename; | |
210 ByteBuffer slice; | |
211 Collections.sort(mBlocks); | |
212 int metaInfCount = 0; | |
213 for (Block block : mBlocks) { | |
214 if (block.mFilename.startsWith("META-INF/")) { | |
215 metaInfCount++; | |
216 if (metaInfCount > MAX_META_INF_FILES) { | |
217 return ERROR_TOO_MANY_META_INF_FILES; | |
218 } | |
219 | |
220 // Files that begin with META-INF/ are not part of the hash. | |
221 // This is because these signatures are added after we comment s igned the rest of | |
222 // the APK. | |
223 continue; | |
224 } | |
225 | |
226 // Hash the filename length and filename as well to prevent Horton p rinciple | |
227 // violation. | |
pkotwicz
2017/03/31 03:59:25
Remove 'as well'
ScottK
2017/04/03 17:44:19
Done.
| |
228 filename = block.mFilename.getBytes("UTF-8"); | |
pkotwicz
2017/03/31 03:59:25
- Is it worth storing mFilename as a String? Can i
ScottK
2017/04/03 17:44:20
It sounds like a good idea but ended up having som
| |
229 sig.update(toUInt32BigEndian(filename.length)); | |
230 sig.update(filename); | |
231 | |
232 // Also hash the block length for the same reason. | |
233 sig.update(toUInt32BigEndian(block.mCompressedSize)); | |
234 | |
235 seek(block.mPosition + block.mHeaderSize); | |
236 slice = mBuffer.slice(); | |
pkotwicz
2017/03/31 03:59:25
Might as well move the declaration of the |slice|
ScottK
2017/04/03 17:44:19
Moved.
| |
237 slice.limit(block.mCompressedSize); | |
238 sig.update(slice); | |
239 } | |
240 return ERROR_OK; | |
241 } | |
242 | |
243 /** | |
244 * toUInt32BigEndian converts an integer to a big endian array of bytes. | |
245 * | |
246 * @param value Integer value to convert. | |
247 * @return Array of bytes. | |
248 */ | |
249 private byte[] toUInt32BigEndian(int value) { | |
250 ByteBuffer buffer = ByteBuffer.allocate(4); | |
251 buffer.order(BIG_ENDIAN); // Since the ZIP is all little endian, adding this for clarity. | |
252 buffer.putInt(value); | |
253 return buffer.array(); | |
254 } | |
255 | |
256 /** | |
257 * Extract the bytes of the signature from the comment. We expect | |
258 * "chrome-webapk:0000:<hexvalues>" comment followed by hex values. Currentl y we ignore the key | |
pkotwicz
2017/03/31 03:59:25
How about: "The signature should have the format w
ScottK
2017/04/03 17:44:19
Done.
| |
259 * id which is always "0000". | |
pkotwicz
2017/03/31 03:59:25
Nit: Add quotes around "key id"
ScottK
2017/04/03 17:44:19
Done.
| |
260 * | |
261 * @return the bytes of the signature. | |
262 */ | |
263 static byte[] parseCommentSignature(String comment) { | |
264 Matcher m = WEBAPK_COMMENT_PATTERN.matcher(comment); | |
265 if (!m.find()) { | |
266 return null; | |
267 } | |
268 String s = m.group(2); | |
269 if (s.length() == 0) { | |
pkotwicz
2017/03/31 03:59:24
Can you remove this conditional?
- It should be i
ScottK
2017/04/03 17:44:19
Good point.
| |
270 return null; | |
271 } | |
272 return hexToBytes(s); | |
273 } | |
274 | |
275 /** | |
276 * Reads the End of Central Directory Records, returns false if it can't fin d it. | |
pkotwicz
2017/03/31 03:59:25
Records -> Record
Remove the part of the comment
ScottK
2017/04/03 17:44:19
Done.
| |
277 * | |
278 * @return ERROR_OK on success. | |
279 */ | |
280 private int readEOCD() { | |
281 int start = findEOCDStart(); | |
282 if (start < 0) { | |
283 return ERROR_BAD_APK; | |
284 } | |
285 // Signature(4), Disk Number(2), Start disk number(2), Records on this disk (2) | |
286 seek(start + 10); | |
287 mRecordCount = read2(); // Number of Central Directory records | |
288 read4(); // Size of central directory | |
pkotwicz
2017/03/31 03:59:25
seekDelta(4)
ScottK
2017/04/03 17:44:20
Oops missed one,thanks.
| |
289 mCentralDirOffset = read4(); // as bytes from start of file. | |
290 int commentLength = read2(); | |
291 mComment = readString(commentLength); | |
292 return ERROR_OK; | |
293 } | |
294 | |
295 /** | |
296 * readDirectory goes through the central directory and the local file heade r blocks. This is | |
297 * used to calculate the offset and size of each block. We also get the file names for sorting | |
298 * purposes. | |
pkotwicz
2017/03/31 03:59:25
How about: Reads the central directory and populat
ScottK
2017/04/03 17:44:19
Done.
| |
299 * | |
300 * @return ERROR_OK on success. | |
301 */ | |
302 @Error | |
303 int readDirectory() { | |
304 mBlocks = new ArrayList<>(mRecordCount); | |
305 int curr = mCentralDirOffset; | |
306 for (int i = 0; i < mRecordCount; i++) { | |
307 seek(curr); | |
308 int signature = read4(); | |
309 if (signature != CD_SIG) { | |
310 Log.d(TAG, "Missing Central Directory Signature"); | |
311 return ERROR_BAD_APK; | |
312 } | |
313 // CreatorVersion(2), ReaderVersion(2), Flags(2), CompressionMethod( 2) | |
314 // ModifiedTime(2), ModifiedDate(2), CRC32(4) = 16 bytes | |
315 seekDelta(16); | |
316 int compressedSize = read4(); | |
317 seekDelta(4); // uncompressed size | |
318 int fileNameLength = read2(); | |
319 int extraLen = read2(); | |
320 int fileCommentLength = read2(); | |
321 seekDelta(8); // DiskNumberStart(2), Internal Attrs(2), External Att rs(4) | |
322 int offset = read4(); | |
323 String filename = readString(fileNameLength); | |
324 curr = mBuffer.position() + extraLen + fileCommentLength; | |
325 if (extraLen > MAX_EXTRA_LENGTH) { | |
326 Log.w(TAG, | |
327 String.format( | |
328 "Extra field too large for file %s: %d bytes", f ilename, extraLen)); | |
329 return ERROR_EXTRA_FIELD_TOO_LARGE; | |
330 } | |
331 if (fileCommentLength > MAX_COMMENT_LENGTH) { | |
332 Log.w(TAG, | |
333 String.format("Unexpected comment field file %s: %d byte s", filename, | |
334 fileCommentLength)); | |
335 return ERROR_COMMENT_TOO_LARGE; | |
336 } | |
337 mBlocks.add(new Block(filename, offset, compressedSize)); | |
338 } | |
339 | |
340 // Read the 'local file header' block to the size of the header in bytes . | |
341 for (Block block : mBlocks) { | |
342 curr = block.mPosition; | |
343 seek(curr); | |
344 int signature = read4(); | |
345 if (signature != LFH_SIG) { | |
346 Log.d(TAG, "LFH Signature missing"); | |
347 return ERROR_BAD_APK; | |
348 } | |
349 // ReaderVersion(2), Flags(2), Method(2), | |
350 // ModifiedTime (2), ModifiedDate(2), CRC32(4), CompressedSize(4), | |
351 // UncompressedSize(4) = 22 bytes | |
352 seekDelta(22); | |
353 int fileNameLength = read2(); | |
354 int extraFieldLength = read2(); | |
355 if (extraFieldLength > MAX_EXTRA_LENGTH) { | |
356 Log.w(TAG, String.format("Extra field too large: %d bytes", extr aFieldLength)); | |
357 return ERROR_EXTRA_FIELD_TOO_LARGE; | |
358 } | |
359 | |
360 // 26 + 4 = 30 bytes | |
361 block.mHeaderSize = 30 + fileNameLength + extraFieldLength; | |
362 } | |
363 return ERROR_OK; | |
364 } | |
365 | |
366 /** | |
367 * We search buffer for EOCD_SIG and return the location where we found it. If the file has no | |
368 * comment it should seek only once. | |
369 * | |
370 * @return Offset from start of buffer or -1 if not found. | |
371 */ | |
372 private int findEOCDStart() { | |
373 int offset = mBuffer.limit() - MIN_EOCD_SIZE; | |
pkotwicz
2017/03/31 03:59:25
Nit: You need to handle |offset| being smaller tha
ScottK
2017/04/03 17:44:20
My unit tests do cover this, however being explici
| |
374 seek(offset); | |
375 for (int count = 0; count < MAX_HEADER_SIZE; count++) { | |
pkotwicz
2017/03/31 03:59:26
Can you do this:
int minSearchOffset = Math.max(0
ScottK
2017/04/03 17:44:19
Done, much cleaner.
| |
376 if (read4() == EOCD_SIG) { | |
377 // found! | |
378 return offset; | |
379 } | |
380 offset--; | |
381 if (offset < 0) { | |
382 return -1; | |
383 } | |
384 seek(offset); | |
385 } | |
386 return -1; | |
387 } | |
388 | |
389 /** | |
390 * Seek to this position. | |
391 * | |
392 * @param offset offset from start of file. | |
393 */ | |
394 private void seek(int offset) { | |
395 mBuffer.position(offset); | |
396 } | |
397 | |
398 /** | |
399 * Skip forward this number of bytes. | |
400 * | |
401 * @param delta number of bytes to seek forward. | |
402 */ | |
403 private void seekDelta(int delta) { | |
404 mBuffer.position(mBuffer.position() + delta); | |
405 } | |
406 | |
407 /** | |
408 * Reads two bytes in little endian format. | |
409 * @return short value read (as an int). | |
410 */ | |
411 private int read2() { | |
412 return mBuffer.getShort(); | |
413 } | |
414 | |
415 /** | |
416 * Reads four bytes in little endian format. | |
417 * @return value read. | |
418 */ | |
419 private int read4() { | |
420 return mBuffer.getInt(); | |
421 } | |
422 | |
423 /** Read {@link length} many bytes into a string */ | |
424 private String readString(int length) { | |
425 if (length <= 0) { | |
426 return ""; | |
427 } | |
428 byte[] bytes = new byte[length]; | |
429 mBuffer.get(bytes); | |
430 return new String(bytes); | |
431 } | |
432 | |
433 /** Convert a hex string into bytes. We store hex in the signature as zip to ols often don't * | |
434 * like binary strings. */ | |
435 static byte[] hexToBytes(String s) { | |
436 int len = s.length(); | |
437 if (len % 2 != 0) { | |
438 Log.d(TAG, "Got an odd number of hex nibbles."); | |
pkotwicz
2017/03/31 03:59:25
Can we remove this log message?
We are already lo
ScottK
2017/04/03 17:44:20
Done.
| |
439 return null; | |
440 } | |
441 if (len == 0) { | |
pkotwicz
2017/03/31 03:59:25
Can we remove this conditional? (For the reasons m
ScottK
2017/04/03 17:44:19
Done.
| |
442 Log.d(TAG, "Got no hex values."); | |
443 return null; | |
444 } | |
445 byte[] data = new byte[len / 2]; | |
446 for (int i = 0; i < len; i += 2) { | |
447 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) | |
448 + Character.digit(s.charAt(i + 1), 16)); | |
449 } | |
450 return data; | |
451 } | |
452 } | |
OLD | NEW |