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.EncryptedDocument.DATA_KEY_LENGTH;
     20 import static com.example.android.vault.EncryptedDocument.MAC_KEY_LENGTH;
     21 import static com.example.android.vault.Utils.closeQuietly;
     22 import static com.example.android.vault.Utils.closeWithErrorQuietly;
     23 import static com.example.android.vault.Utils.readFully;
     24 import static com.example.android.vault.Utils.writeFully;
     25 
     26 import android.content.Context;
     27 import android.content.SharedPreferences;
     28 import android.database.Cursor;
     29 import android.database.MatrixCursor;
     30 import android.database.MatrixCursor.RowBuilder;
     31 import android.os.Bundle;
     32 import android.os.CancellationSignal;
     33 import android.os.ParcelFileDescriptor;
     34 import android.provider.DocumentsContract;
     35 import android.provider.DocumentsContract.Document;
     36 import android.provider.DocumentsContract.Root;
     37 import android.provider.DocumentsProvider;
     38 import android.security.KeyChain;
     39 import android.util.Log;
     40 
     41 import org.json.JSONArray;
     42 import org.json.JSONException;
     43 import org.json.JSONObject;
     44 
     45 import java.io.File;
     46 import java.io.FileNotFoundException;
     47 import java.io.IOException;
     48 import java.nio.charset.StandardCharsets;
     49 import java.security.GeneralSecurityException;
     50 import java.security.KeyStore;
     51 import java.security.SecureRandom;
     52 
     53 import javax.crypto.Mac;
     54 import javax.crypto.SecretKey;
     55 import javax.crypto.spec.SecretKeySpec;
     56 
     57 /**
     58  * Provider that encrypts both metadata and contents of documents stored inside.
     59  * Each document is stored as described by {@link EncryptedDocument} with
     60  * separate metadata and content sections. Directories are just
     61  * {@link EncryptedDocument} instances without a content section, and a list of
     62  * child documents included in the metadata section.
     63  * <p>
     64  * All content is encrypted/decrypted on demand through pipes, using
     65  * {@link ParcelFileDescriptor#createReliablePipe()} to detect and recover from
     66  * remote crashes and errors.
     67  * <p>
     68  * Our symmetric encryption key is stored on disk only after using
     69  * {@link SecretKeyWrapper} to "wrap" it using another public/private key pair
     70  * stored in the platform {@link KeyStore}. This allows us to protect our
     71  * symmetric key with hardware-backed keys, if supported. Devices without
     72  * hardware support still encrypt their keys while at rest, and the platform
     73  * always requires a user to present a PIN, password, or pattern to unlock the
     74  * KeyStore before use.
     75  */
     76 public class VaultProvider extends DocumentsProvider {
     77     public static final String TAG = "Vault";
     78 
     79     static final String AUTHORITY = "com.example.android.vault.provider";
     80 
     81     static final String DEFAULT_ROOT_ID = "vault";
     82     static final String DEFAULT_DOCUMENT_ID = "0";
     83 
     84     /** JSON key storing array of all children documents in a directory. */
     85     private static final String KEY_CHILDREN = "vault:children";
     86 
     87     /** Key pointing to next available document ID. */
     88     private static final String PREF_NEXT_ID = "next_id";
     89 
     90     /** Blob used to derive {@link #mDataKey} from our secret key. */
     91     private static final byte[] BLOB_DATA = "DATA".getBytes(StandardCharsets.UTF_8);
     92     /** Blob used to derive {@link #mMacKey} from our secret key. */
     93     private static final byte[] BLOB_MAC = "MAC".getBytes(StandardCharsets.UTF_8);
     94 
     95     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     96             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
     97             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_SUMMARY
     98     };
     99 
    100     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
    101             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
    102             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
    103     };
    104 
    105     private static String[] resolveRootProjection(String[] projection) {
    106         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
    107     }
    108 
    109     private static String[] resolveDocumentProjection(String[] projection) {
    110         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
    111     }
    112 
    113     private final Object mIdLock = new Object();
    114 
    115     /**
    116      * Flag indicating that the {@link SecretKeyWrapper} public/private key is
    117      * hardware-backed. A software keystore is more vulnerable to offline
    118      * attacks if the device is compromised.
    119      */
    120     private boolean mHardwareBacked;
    121 
    122     /** File where wrapped symmetric key is stored. */
    123     private File mKeyFile;
    124     /** Directory where all encrypted documents are stored. */
    125     private File mDocumentsDir;
    126 
    127     private SecretKey mDataKey;
    128     private SecretKey mMacKey;
    129 
    130     @Override
    131     public boolean onCreate() {
    132         mHardwareBacked = KeyChain.isBoundKeyAlgorithm("RSA");
    133 
    134         mKeyFile = new File(getContext().getFilesDir(), "vault.key");
    135         mDocumentsDir = new File(getContext().getFilesDir(), "documents");
    136         mDocumentsDir.mkdirs();
    137 
    138         try {
    139             // Load secret key and ensure our root document is ready.
    140             loadOrGenerateKeys(getContext(), mKeyFile);
    141             initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
    142 
    143         } catch (IOException e) {
    144             throw new IllegalStateException(e);
    145         } catch (GeneralSecurityException e) {
    146             throw new IllegalStateException(e);
    147         }
    148 
    149         return true;
    150     }
    151 
    152     /**
    153      * Used for testing.
    154      */
    155     void wipeAllContents() throws IOException, GeneralSecurityException {
    156         for (File f : mDocumentsDir.listFiles()) {
    157             f.delete();
    158         }
    159 
    160         initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
    161     }
    162 
    163     /**
    164      * Load our symmetric secret key and use it to derive two different data and
    165      * MAC keys. The symmetric secret key is stored securely on disk by wrapping
    166      * it with a public/private key pair, possibly backed by hardware.
    167      */
    168     private void loadOrGenerateKeys(Context context, File keyFile)
    169             throws GeneralSecurityException, IOException {
    170         final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, TAG);
    171 
    172         // Generate secret key if none exists
    173         if (!keyFile.exists()) {
    174             final byte[] raw = new byte[DATA_KEY_LENGTH];
    175             new SecureRandom().nextBytes(raw);
    176 
    177             final SecretKey key = new SecretKeySpec(raw, "AES");
    178             final byte[] wrapped = wrapper.wrap(key);
    179 
    180             writeFully(keyFile, wrapped);
    181         }
    182 
    183         // Even if we just generated the key, always read it back to ensure we
    184         // can read it successfully.
    185         final byte[] wrapped = readFully(keyFile);
    186         final SecretKey key = wrapper.unwrap(wrapped);
    187 
    188         final Mac mac = Mac.getInstance("HmacSHA256");
    189         mac.init(key);
    190 
    191         // Derive two different keys for encryption and authentication.
    192         final byte[] rawDataKey = new byte[DATA_KEY_LENGTH];
    193         final byte[] rawMacKey = new byte[MAC_KEY_LENGTH];
    194 
    195         System.arraycopy(mac.doFinal(BLOB_DATA), 0, rawDataKey, 0, rawDataKey.length);
    196         System.arraycopy(mac.doFinal(BLOB_MAC), 0, rawMacKey, 0, rawMacKey.length);
    197 
    198         mDataKey = new SecretKeySpec(rawDataKey, "AES");
    199         mMacKey = new SecretKeySpec(rawMacKey, "HmacSHA256");
    200     }
    201 
    202     @Override
    203     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
    204         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
    205         final RowBuilder row = result.newRow();
    206         row.add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID);
    207         row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY);
    208         row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_label));
    209         row.add(Root.COLUMN_DOCUMENT_ID, DEFAULT_DOCUMENT_ID);
    210         row.add(Root.COLUMN_ICON, R.drawable.ic_lock_lock);
    211 
    212         // Notify user in storage UI when key isn't hardware-backed
    213         if (!mHardwareBacked) {
    214             row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.info_software));
    215         }
    216 
    217         return result;
    218     }
    219 
    220     private EncryptedDocument getDocument(long docId) throws GeneralSecurityException {
    221         final File file = new File(mDocumentsDir, String.valueOf(docId));
    222         return new EncryptedDocument(docId, file, mDataKey, mMacKey);
    223     }
    224 
    225     /**
    226      * Include metadata for a document in the given result cursor.
    227      */
    228     private void includeDocument(MatrixCursor result, long docId)
    229             throws IOException, GeneralSecurityException {
    230         final EncryptedDocument doc = getDocument(docId);
    231         if (!doc.getFile().exists()) {
    232             throw new FileNotFoundException("Missing document " + docId);
    233         }
    234 
    235         final JSONObject meta = doc.readMetadata();
    236 
    237         int flags = 0;
    238 
    239         final String mimeType = meta.optString(Document.COLUMN_MIME_TYPE);
    240         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    241             flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
    242         } else {
    243             flags |= Document.FLAG_SUPPORTS_WRITE;
    244         }
    245         flags |= Document.FLAG_SUPPORTS_DELETE;
    246 
    247         final RowBuilder row = result.newRow();
    248         row.add(Document.COLUMN_DOCUMENT_ID, meta.optLong(Document.COLUMN_DOCUMENT_ID));
    249         row.add(Document.COLUMN_DISPLAY_NAME, meta.optString(Document.COLUMN_DISPLAY_NAME));
    250         row.add(Document.COLUMN_SIZE, meta.optLong(Document.COLUMN_SIZE));
    251         row.add(Document.COLUMN_MIME_TYPE, mimeType);
    252         row.add(Document.COLUMN_FLAGS, flags);
    253         row.add(Document.COLUMN_LAST_MODIFIED, meta.optLong(Document.COLUMN_LAST_MODIFIED));
    254     }
    255 
    256     @Override
    257     public String createDocument(String parentDocumentId, String mimeType, String displayName)
    258             throws FileNotFoundException {
    259         final long parentDocId = Long.parseLong(parentDocumentId);
    260 
    261         // Allocate the next available ID
    262         final long childDocId;
    263         synchronized (mIdLock) {
    264             final SharedPreferences prefs = getContext()
    265                     .getSharedPreferences(PREF_NEXT_ID, Context.MODE_PRIVATE);
    266             childDocId = prefs.getLong(PREF_NEXT_ID, 1);
    267             if (!prefs.edit().putLong(PREF_NEXT_ID, childDocId + 1).commit()) {
    268                 throw new IllegalStateException("Failed to allocate document ID");
    269             }
    270         }
    271 
    272         try {
    273             initDocument(childDocId, mimeType, displayName);
    274 
    275             // Update parent to reference new child
    276             final EncryptedDocument parentDoc = getDocument(parentDocId);
    277             final JSONObject parentMeta = parentDoc.readMetadata();
    278             parentMeta.accumulate(KEY_CHILDREN, childDocId);
    279             parentDoc.writeMetadataAndContent(parentMeta, null);
    280 
    281             return String.valueOf(childDocId);
    282 
    283         } catch (IOException e) {
    284             throw new IllegalStateException(e);
    285         } catch (GeneralSecurityException e) {
    286             throw new IllegalStateException(e);
    287         } catch (JSONException e) {
    288             throw new IllegalStateException(e);
    289         }
    290     }
    291 
    292     /**
    293      * Create document on disk, writing an initial metadata section. Someone
    294      * might come back later to write contents.
    295      */
    296     private void initDocument(long docId, String mimeType, String displayName)
    297             throws IOException, GeneralSecurityException {
    298         final EncryptedDocument doc = getDocument(docId);
    299         if (doc.getFile().exists()) return;
    300 
    301         try {
    302             final JSONObject meta = new JSONObject();
    303             meta.put(Document.COLUMN_DOCUMENT_ID, docId);
    304             meta.put(Document.COLUMN_MIME_TYPE, mimeType);
    305             meta.put(Document.COLUMN_DISPLAY_NAME, displayName);
    306             if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    307                 meta.put(KEY_CHILDREN, new JSONArray());
    308             }
    309 
    310             doc.writeMetadataAndContent(meta, null);
    311         } catch (JSONException e) {
    312             throw new IOException(e);
    313         }
    314     }
    315 
    316     @Override
    317     public void deleteDocument(String documentId) throws FileNotFoundException {
    318         final long docId = Long.parseLong(documentId);
    319 
    320         try {
    321             // Delete given document, any children documents under it, and any
    322             // references to it from parents.
    323             deleteDocumentTree(docId);
    324             deleteDocumentReferences(docId);
    325 
    326         } catch (IOException e) {
    327             throw new IllegalStateException(e);
    328         } catch (GeneralSecurityException e) {
    329             throw new IllegalStateException(e);
    330         }
    331     }
    332 
    333     /**
    334      * Recursively delete the given document and any children under it.
    335      */
    336     private void deleteDocumentTree(long docId) throws IOException, GeneralSecurityException {
    337         final EncryptedDocument doc = getDocument(docId);
    338         final JSONObject meta = doc.readMetadata();
    339         try {
    340             if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
    341                 final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
    342                 for (int i = 0; i < children.length(); i++) {
    343                     final long childDocId = children.getLong(i);
    344                     deleteDocumentTree(childDocId);
    345                 }
    346             }
    347         } catch (JSONException e) {
    348             throw new IOException(e);
    349         }
    350 
    351         if (!doc.getFile().delete()) {
    352             throw new IOException("Failed to delete " + docId);
    353         }
    354     }
    355 
    356     /**
    357      * Remove any references to the given document, usually when included as a
    358      * child of another directory.
    359      */
    360     private void deleteDocumentReferences(long docId) {
    361         for (String name : mDocumentsDir.list()) {
    362             try {
    363                 final long parentDocId = Long.parseLong(name);
    364                 final EncryptedDocument parentDoc = getDocument(parentDocId);
    365                 final JSONObject meta = parentDoc.readMetadata();
    366 
    367                 if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
    368                     final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
    369                     if (maybeRemove(children, docId)) {
    370                         Log.d(TAG, "Removed " + docId + " reference from " + name);
    371                         parentDoc.writeMetadataAndContent(meta, null);
    372 
    373                         getContext().getContentResolver().notifyChange(
    374                                 DocumentsContract.buildChildDocumentsUri(AUTHORITY, name), null,
    375                                 false);
    376                     }
    377                 }
    378             } catch (NumberFormatException ignored) {
    379             } catch (IOException e) {
    380                 Log.w(TAG, "Failed to examine " + name, e);
    381             } catch (GeneralSecurityException e) {
    382                 Log.w(TAG, "Failed to examine " + name, e);
    383             } catch (JSONException e) {
    384                 Log.w(TAG, "Failed to examine " + name, e);
    385             }
    386         }
    387     }
    388 
    389     @Override
    390     public Cursor queryDocument(String documentId, String[] projection)
    391             throws FileNotFoundException {
    392         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    393         try {
    394             includeDocument(result, Long.parseLong(documentId));
    395         } catch (GeneralSecurityException e) {
    396             throw new IllegalStateException(e);
    397         } catch (IOException e) {
    398             throw new IllegalStateException(e);
    399         }
    400         return result;
    401     }
    402 
    403     @Override
    404     public Cursor queryChildDocuments(
    405             String parentDocumentId, String[] projection, String sortOrder)
    406             throws FileNotFoundException {
    407         final ExtrasMatrixCursor result = new ExtrasMatrixCursor(
    408                 resolveDocumentProjection(projection));
    409         result.setNotificationUri(getContext().getContentResolver(),
    410                 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId));
    411 
    412         // Notify user in storage UI when key isn't hardware-backed
    413         if (!mHardwareBacked) {
    414             result.putString(DocumentsContract.EXTRA_INFO,
    415                     getContext().getString(R.string.info_software_detail));
    416         }
    417 
    418         try {
    419             final EncryptedDocument doc = getDocument(Long.parseLong(parentDocumentId));
    420             final JSONObject meta = doc.readMetadata();
    421             final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
    422             for (int i = 0; i < children.length(); i++) {
    423                 final long docId = children.getLong(i);
    424                 includeDocument(result, docId);
    425             }
    426 
    427         } catch (IOException e) {
    428             throw new IllegalStateException(e);
    429         } catch (GeneralSecurityException e) {
    430             throw new IllegalStateException(e);
    431         } catch (JSONException e) {
    432             throw new IllegalStateException(e);
    433         }
    434 
    435         return result;
    436     }
    437 
    438     @Override
    439     public ParcelFileDescriptor openDocument(
    440             String documentId, String mode, CancellationSignal signal)
    441             throws FileNotFoundException {
    442         final long docId = Long.parseLong(documentId);
    443 
    444         try {
    445             final EncryptedDocument doc = getDocument(docId);
    446             if ("r".equals(mode)) {
    447                 return startRead(doc);
    448             } else if ("w".equals(mode) || "wt".equals(mode)) {
    449                 return startWrite(doc);
    450             } else {
    451                 throw new IllegalArgumentException("Unsupported mode: " + mode);
    452             }
    453         } catch (IOException e) {
    454             throw new IllegalStateException(e);
    455         } catch (GeneralSecurityException e) {
    456             throw new IllegalStateException(e);
    457         }
    458     }
    459 
    460     /**
    461      * Kick off a thread to handle a read request for the given document.
    462      * Internally creates a pipe and returns the read end for returning to a
    463      * remote process.
    464      */
    465     private ParcelFileDescriptor startRead(final EncryptedDocument doc) throws IOException {
    466         final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
    467         final ParcelFileDescriptor readEnd = pipe[0];
    468         final ParcelFileDescriptor writeEnd = pipe[1];
    469 
    470         new Thread() {
    471             @Override
    472             public void run() {
    473                 try {
    474                     doc.readContent(writeEnd);
    475                     Log.d(TAG, "Success reading " + doc);
    476                     closeQuietly(writeEnd);
    477                 } catch (IOException e) {
    478                     Log.w(TAG, "Failed reading " + doc, e);
    479                     closeWithErrorQuietly(writeEnd, e.toString());
    480                 } catch (GeneralSecurityException e) {
    481                     Log.w(TAG, "Failed reading " + doc, e);
    482                     closeWithErrorQuietly(writeEnd, e.toString());
    483                 }
    484             }
    485         }.start();
    486 
    487         return readEnd;
    488     }
    489 
    490     /**
    491      * Kick off a thread to handle a write request for the given document.
    492      * Internally creates a pipe and returns the write end for returning to a
    493      * remote process.
    494      */
    495     private ParcelFileDescriptor startWrite(final EncryptedDocument doc) throws IOException {
    496         final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
    497         final ParcelFileDescriptor readEnd = pipe[0];
    498         final ParcelFileDescriptor writeEnd = pipe[1];
    499 
    500         new Thread() {
    501             @Override
    502             public void run() {
    503                 try {
    504                     final JSONObject meta = doc.readMetadata();
    505                     doc.writeMetadataAndContent(meta, readEnd);
    506                     Log.d(TAG, "Success writing " + doc);
    507                     closeQuietly(readEnd);
    508                 } catch (IOException e) {
    509                     Log.w(TAG, "Failed writing " + doc, e);
    510                     closeWithErrorQuietly(readEnd, e.toString());
    511                 } catch (GeneralSecurityException e) {
    512                     Log.w(TAG, "Failed writing " + doc, e);
    513                     closeWithErrorQuietly(readEnd, e.toString());
    514                 }
    515             }
    516         }.start();
    517 
    518         return writeEnd;
    519     }
    520 
    521     /**
    522      * Maybe remove the given value from a {@link JSONArray}.
    523      *
    524      * @return if the array was mutated.
    525      */
    526     private static boolean maybeRemove(JSONArray array, long value) throws JSONException {
    527         boolean mutated = false;
    528         int i = 0;
    529         while (i < array.length()) {
    530             if (value == array.getLong(i)) {
    531                 array.remove(i);
    532                 mutated = true;
    533             } else {
    534                 i++;
    535             }
    536         }
    537         return mutated;
    538     }
    539 
    540     /**
    541      * Simple extension of {@link MatrixCursor} that makes it easy to provide a
    542      * {@link Bundle} of extras.
    543      */
    544     private static class ExtrasMatrixCursor extends MatrixCursor {
    545         private Bundle mExtras;
    546 
    547         public ExtrasMatrixCursor(String[] columnNames) {
    548             super(columnNames);
    549         }
    550 
    551         public void putString(String key, String value) {
    552             if (mExtras == null) {
    553                 mExtras = new Bundle();
    554             }
    555             mExtras.putString(key, value);
    556         }
    557 
    558         @Override
    559         public Bundle getExtras() {
    560             return mExtras;
    561         }
    562     }
    563 }
    564