1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.vault; 18 19 import static com.example.android.vault.VaultProvider.TAG; 20 21 import android.os.ParcelFileDescriptor; 22 import android.provider.DocumentsContract.Document; 23 import android.util.Log; 24 25 import org.json.JSONException; 26 import org.json.JSONObject; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.FileInputStream; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.RandomAccessFile; 37 import java.net.ProtocolException; 38 import java.nio.charset.StandardCharsets; 39 import java.security.DigestException; 40 import java.security.GeneralSecurityException; 41 import java.security.SecureRandom; 42 43 import javax.crypto.Cipher; 44 import javax.crypto.Mac; 45 import javax.crypto.SecretKey; 46 import javax.crypto.spec.IvParameterSpec; 47 48 /** 49 * Represents a single encrypted document stored on disk. Handles encryption, 50 * decryption, and authentication of the document when requested. 51 * <p> 52 * Encrypted documents are stored on disk as a magic number, followed by an 53 * encrypted metadata section, followed by an encrypted content section. The 54 * content section always starts at a specific offset {@link #CONTENT_OFFSET} to 55 * allow metadata updates without rewriting the entire file. 56 * <p> 57 * Each section is encrypted using AES-128 with a random IV, and authenticated 58 * with SHA-256. Data encrypted and authenticated like this can be safely stored 59 * on untrusted storage devices, as long as the keys are stored securely. 60 * <p> 61 * Not inherently thread safe. 62 */ 63 public class EncryptedDocument { 64 65 /** 66 * Magic number to identify file; "AVLT". 67 */ 68 private static final int MAGIC_NUMBER = 0x41564c54; 69 70 /** 71 * Offset in file at which content section starts. Magic and metadata 72 * section must fully fit before this offset. 73 */ 74 private static final int CONTENT_OFFSET = 4096; 75 76 private static final boolean DEBUG_METADATA = true; 77 78 /** Key length for AES-128 */ 79 public static final int DATA_KEY_LENGTH = 16; 80 /** Key length for SHA-256 */ 81 public static final int MAC_KEY_LENGTH = 32; 82 83 private final SecureRandom mRandom; 84 private final Cipher mCipher; 85 private final Mac mMac; 86 87 private final long mDocId; 88 private final File mFile; 89 private final SecretKey mDataKey; 90 private final SecretKey mMacKey; 91 92 /** 93 * Create an encrypted document. 94 * 95 * @param docId the expected {@link Document#COLUMN_DOCUMENT_ID} to be 96 * validated when reading metadata. 97 * @param file location on disk where the encrypted document is stored. May 98 * not exist yet. 99 */ 100 public EncryptedDocument(long docId, File file, SecretKey dataKey, SecretKey macKey) 101 throws GeneralSecurityException { 102 mRandom = new SecureRandom(); 103 mCipher = Cipher.getInstance("AES/CTR/NoPadding"); 104 mMac = Mac.getInstance("HmacSHA256"); 105 106 if (dataKey.getEncoded().length != DATA_KEY_LENGTH) { 107 throw new IllegalArgumentException("Expected data key length " + DATA_KEY_LENGTH); 108 } 109 if (macKey.getEncoded().length != MAC_KEY_LENGTH) { 110 throw new IllegalArgumentException("Expected MAC key length " + MAC_KEY_LENGTH); 111 } 112 113 mDocId = docId; 114 mFile = file; 115 mDataKey = dataKey; 116 mMacKey = macKey; 117 } 118 119 public File getFile() { 120 return mFile; 121 } 122 123 @Override 124 public String toString() { 125 return mFile.getName(); 126 } 127 128 /** 129 * Decrypt and return parsed metadata section from this document. 130 * 131 * @throws DigestException if metadata fails MAC check, or if 132 * {@link Document#COLUMN_DOCUMENT_ID} recorded in metadata is 133 * unexpected. 134 */ 135 public JSONObject readMetadata() throws IOException, GeneralSecurityException { 136 final RandomAccessFile f = new RandomAccessFile(mFile, "r"); 137 try { 138 assertMagic(f); 139 140 // Only interested in metadata section 141 final ByteArrayOutputStream metaOut = new ByteArrayOutputStream(); 142 readSection(f, metaOut); 143 144 final String rawMeta = metaOut.toString(StandardCharsets.UTF_8.name()); 145 if (DEBUG_METADATA) { 146 Log.d(TAG, "Found metadata for " + mDocId + ": " + rawMeta); 147 } 148 149 final JSONObject meta = new JSONObject(rawMeta); 150 151 // Validate that metadata belongs to requested file 152 if (meta.getLong(Document.COLUMN_DOCUMENT_ID) != mDocId) { 153 throw new DigestException("Unexpected document ID"); 154 } 155 156 return meta; 157 158 } catch (JSONException e) { 159 throw new IOException(e); 160 } finally { 161 f.close(); 162 } 163 } 164 165 /** 166 * Decrypt and read content section of this document, writing it into the 167 * given pipe. 168 * <p> 169 * Pipe is left open, so caller is responsible for calling 170 * {@link ParcelFileDescriptor#close()} or 171 * {@link ParcelFileDescriptor#closeWithError(String)}. 172 * 173 * @param contentOut write end of a pipe. 174 * @throws DigestException if content fails MAC check. Some or all content 175 * may have already been written to the pipe when the MAC is 176 * validated. 177 */ 178 public void readContent(ParcelFileDescriptor contentOut) 179 throws IOException, GeneralSecurityException { 180 final RandomAccessFile f = new RandomAccessFile(mFile, "r"); 181 try { 182 assertMagic(f); 183 184 if (f.length() <= CONTENT_OFFSET) { 185 throw new IOException("Document has no content"); 186 } 187 188 // Skip over metadata section 189 f.seek(CONTENT_OFFSET); 190 readSection(f, new FileOutputStream(contentOut.getFileDescriptor())); 191 192 } finally { 193 f.close(); 194 } 195 } 196 197 /** 198 * Encrypt and write both the metadata and content sections of this 199 * document, reading the content from the given pipe. Internally uses 200 * {@link ParcelFileDescriptor#checkError()} to verify that content arrives 201 * without errors. Writes to temporary file to keep atomic view of contents, 202 * swapping into place only when write is successful. 203 * <p> 204 * Pipe is left open, so caller is responsible for calling 205 * {@link ParcelFileDescriptor#close()} or 206 * {@link ParcelFileDescriptor#closeWithError(String)}. 207 * 208 * @param contentIn read end of a pipe. 209 */ 210 public void writeMetadataAndContent(JSONObject meta, ParcelFileDescriptor contentIn) 211 throws IOException, GeneralSecurityException { 212 // Write into temporary file to provide an atomic view of existing 213 // contents during write, and also to recover from failed writes. 214 final String tempName = mFile.getName() + ".tmp_" + Thread.currentThread().getId(); 215 final File tempFile = new File(mFile.getParentFile(), tempName); 216 217 RandomAccessFile f = new RandomAccessFile(tempFile, "rw"); 218 try { 219 // Truncate any existing data 220 f.setLength(0); 221 222 // Write content first to detect size 223 if (contentIn != null) { 224 f.seek(CONTENT_OFFSET); 225 final int plainLength = writeSection( 226 f, new FileInputStream(contentIn.getFileDescriptor())); 227 meta.put(Document.COLUMN_SIZE, plainLength); 228 229 // Verify that remote side of pipe finished okay; if they 230 // crashed or indicated an error then this throws and we 231 // leave the original file intact and clean up temp below. 232 contentIn.checkError(); 233 } 234 235 meta.put(Document.COLUMN_DOCUMENT_ID, mDocId); 236 meta.put(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis()); 237 238 // Rewind and write metadata section 239 f.seek(0); 240 f.writeInt(MAGIC_NUMBER); 241 242 final ByteArrayInputStream metaIn = new ByteArrayInputStream( 243 meta.toString().getBytes(StandardCharsets.UTF_8)); 244 writeSection(f, metaIn); 245 246 if (f.getFilePointer() > CONTENT_OFFSET) { 247 throw new IOException("Metadata section was too large"); 248 } 249 250 // Everything written fine, atomically swap new data into place. 251 // fsync() before close would be overkill, since rename() is an 252 // atomic barrier. 253 f.close(); 254 tempFile.renameTo(mFile); 255 256 } catch (JSONException e) { 257 throw new IOException(e); 258 } finally { 259 // Regardless of what happens, always try cleaning up. 260 f.close(); 261 tempFile.delete(); 262 } 263 } 264 265 /** 266 * Read and decrypt the section starting at the current file offset. 267 * Validates MAC of decrypted data, throwing if mismatch. When finished, 268 * file offset is at the end of the entire section. 269 */ 270 private void readSection(RandomAccessFile f, OutputStream out) 271 throws IOException, GeneralSecurityException { 272 final long start = f.getFilePointer(); 273 274 final Section section = new Section(); 275 section.read(f); 276 277 final IvParameterSpec ivSpec = new IvParameterSpec(section.iv); 278 mCipher.init(Cipher.DECRYPT_MODE, mDataKey, ivSpec); 279 mMac.init(mMacKey); 280 281 byte[] inbuf = new byte[8192]; 282 byte[] outbuf; 283 int n; 284 while ((n = f.read(inbuf, 0, (int) Math.min(section.length, inbuf.length))) != -1) { 285 section.length -= n; 286 mMac.update(inbuf, 0, n); 287 outbuf = mCipher.update(inbuf, 0, n); 288 if (outbuf != null) { 289 out.write(outbuf); 290 } 291 if (section.length == 0) break; 292 } 293 294 section.assertMac(mMac.doFinal()); 295 296 outbuf = mCipher.doFinal(); 297 if (outbuf != null) { 298 out.write(outbuf); 299 } 300 } 301 302 /** 303 * Encrypt and write the given stream as a full section. Writes section 304 * header and encrypted data starting at the current file offset. When 305 * finished, file offset is at the end of the entire section. 306 */ 307 private int writeSection(RandomAccessFile f, InputStream in) 308 throws IOException, GeneralSecurityException { 309 final long start = f.getFilePointer(); 310 311 // Write header; we'll come back later to finalize details 312 final Section section = new Section(); 313 section.write(f); 314 315 final long dataStart = f.getFilePointer(); 316 317 mRandom.nextBytes(section.iv); 318 319 final IvParameterSpec ivSpec = new IvParameterSpec(section.iv); 320 mCipher.init(Cipher.ENCRYPT_MODE, mDataKey, ivSpec); 321 mMac.init(mMacKey); 322 323 int plainLength = 0; 324 byte[] inbuf = new byte[8192]; 325 byte[] outbuf; 326 int n; 327 while ((n = in.read(inbuf)) != -1) { 328 plainLength += n; 329 outbuf = mCipher.update(inbuf, 0, n); 330 if (outbuf != null) { 331 mMac.update(outbuf); 332 f.write(outbuf); 333 } 334 } 335 336 outbuf = mCipher.doFinal(); 337 if (outbuf != null) { 338 mMac.update(outbuf); 339 f.write(outbuf); 340 } 341 342 section.setMac(mMac.doFinal()); 343 344 final long dataEnd = f.getFilePointer(); 345 section.length = dataEnd - dataStart; 346 347 // Rewind and update header 348 f.seek(start); 349 section.write(f); 350 f.seek(dataEnd); 351 352 return plainLength; 353 } 354 355 /** 356 * Header of a single file section. 357 */ 358 private static class Section { 359 long length; 360 final byte[] iv = new byte[DATA_KEY_LENGTH]; 361 final byte[] mac = new byte[MAC_KEY_LENGTH]; 362 363 public void read(RandomAccessFile f) throws IOException { 364 length = f.readLong(); 365 f.readFully(iv); 366 f.readFully(mac); 367 } 368 369 public void write(RandomAccessFile f) throws IOException { 370 f.writeLong(length); 371 f.write(iv); 372 f.write(mac); 373 } 374 375 public void setMac(byte[] mac) { 376 if (mac.length != this.mac.length) { 377 throw new IllegalArgumentException("Unexpected MAC length"); 378 } 379 System.arraycopy(mac, 0, this.mac, 0, this.mac.length); 380 } 381 382 public void assertMac(byte[] mac) throws DigestException { 383 if (mac.length != this.mac.length) { 384 throw new IllegalArgumentException("Unexpected MAC length"); 385 } 386 byte result = 0; 387 for (int i = 0; i < mac.length; i++) { 388 result |= mac[i] ^ this.mac[i]; 389 } 390 if (result != 0) { 391 throw new DigestException(); 392 } 393 } 394 } 395 396 private static void assertMagic(RandomAccessFile f) throws IOException { 397 final int magic = f.readInt(); 398 if (magic != MAGIC_NUMBER) { 399 throw new ProtocolException("Bad magic number: " + Integer.toHexString(magic)); 400 } 401 } 402 } 403