Home | History | Annotate | Download | only in vault
      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