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      */
EncryptedDocument(long docId, File file, SecretKey dataKey, SecretKey macKey)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 
getFile()119     public File getFile() {
120         return mFile;
121     }
122 
123     @Override
toString()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      */
readMetadata()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      */
readContent(ParcelFileDescriptor contentOut)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      */
writeMetadataAndContent(JSONObject meta, ParcelFileDescriptor contentIn)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      */
readSection(RandomAccessFile f, OutputStream out)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      */
writeSection(RandomAccessFile f, InputStream in)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 
read(RandomAccessFile f)363         public void read(RandomAccessFile f) throws IOException {
364             length = f.readLong();
365             f.readFully(iv);
366             f.readFully(mac);
367         }
368 
write(RandomAccessFile f)369         public void write(RandomAccessFile f) throws IOException {
370             f.writeLong(length);
371             f.write(iv);
372             f.write(mac);
373         }
374 
setMac(byte[] mac)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 
assertMac(byte[] mac)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 
assertMagic(RandomAccessFile f)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