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