1 /*
2  * Copyright 2018 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 androidx.security.crypto;
18 
19 import static java.nio.charset.StandardCharsets.UTF_8;
20 
21 import android.annotation.SuppressLint;
22 import android.content.Context;
23 
24 import com.google.crypto.tink.KeyTemplate;
25 import com.google.crypto.tink.KeyTemplates;
26 import com.google.crypto.tink.KeysetHandle;
27 import com.google.crypto.tink.StreamingAead;
28 import com.google.crypto.tink.integration.android.AndroidKeysetManager;
29 import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
30 
31 import org.jspecify.annotations.NonNull;
32 
33 import java.io.File;
34 import java.io.FileDescriptor;
35 import java.io.FileInputStream;
36 import java.io.FileNotFoundException;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.OutputStream;
41 import java.nio.channels.FileChannel;
42 import java.security.GeneralSecurityException;
43 
44 /**
45  * Class used to create and read encrypted files.
46  * <br />
47  * <br />
48  * <b>WARNING</b>: The encrypted file should not be backed up with Auto Backup. When restoring the
49  * file it is likely the key used to encrypt it will no longer be present. You should exclude all
50  * <code>EncryptedFile</code>s from backup using
51  * <a href="https://developer.android.com/guide/topics/data/autobackup#IncludingFiles">backup rules</a>.
52  * Be aware that if you are not explicitly calling <code>setKeysetPrefName()</code> there is also a
53  * silently-created default preferences file created at
54  * <pre>
55  *     ApplicationProvider
56  *          .getApplicationContext()
57  *          .getFilesDir()
58  *          .getParent() + "/shared_prefs/__androidx_security_crypto_encrypted_file_pref__"
59  * </pre>
60  *
61  * This preferences file (or any others created with a custom specified location) also should be
62  * excluded from backups.
63  * <br />
64  * <br />
65  * Basic use of the class:
66  *
67  * <pre>
68  *  MasterKey masterKey = new MasterKey.Builder(context)
69  *      .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
70  *      .build();
71  *
72  *  File file = new File(context.getFilesDir(), "secret_data");
73  *  EncryptedFile encryptedFile = EncryptedFile.Builder(
74  *      context,
75  *      file,
76  *      masterKey,
77  *      EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
78  *  ).build();
79  *
80  *  // write to the encrypted file
81  *  FileOutputStream encryptedOutputStream = encryptedFile.openFileOutput();
82  *
83  *  // read the encrypted file
84  *  FileInputStream encryptedInputStream = encryptedFile.openFileInput();
85  * </pre>
86  * @deprecated Use {@link java.io.File} instead.
87  */
88 @Deprecated
89 @SuppressWarnings("deprecation")
90 public final class EncryptedFile {
91 
92     private static final String KEYSET_PREF_NAME =
93             "__androidx_security_crypto_encrypted_file_pref__";
94     private static final String KEYSET_ALIAS =
95             "__androidx_security_crypto_encrypted_file_keyset__";
96 
97     final File mFile;
98     final Context mContext;
99     final String mMasterKeyAlias;
100     final StreamingAead mStreamingAead;
101 
EncryptedFile( @onNull File file, @NonNull String masterKeyAlias, @NonNull StreamingAead streamingAead, @NonNull Context context)102     EncryptedFile(
103             @NonNull File file,
104             @NonNull String masterKeyAlias,
105             @NonNull StreamingAead streamingAead,
106             @NonNull Context context) {
107         mFile = file;
108         mContext = context;
109         mMasterKeyAlias = masterKeyAlias;
110         mStreamingAead = streamingAead;
111     }
112 
113     /**
114      * The encryption scheme to encrypt files.
115      * @deprecated Use {@link java.io.File} instead.
116      */
117     @Deprecated
118     public enum FileEncryptionScheme {
119         /**
120          * The file content is encrypted using StreamingAead with AES-GCM, with the file name as
121          * associated data.
122          *
123          * <p>For more information please see the Tink documentation:
124          *
125          * <p><a href="https://google.github.io/tink/javadoc/tink/1.7.0/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.html">AesGcmHkdfStreamingKeyManager</a>.aes256GcmHkdf4KBTemplate()
126          */
127         AES256_GCM_HKDF_4KB("AES256_GCM_HKDF_4KB");
128 
129         private final String mKeyTemplateName;
130 
FileEncryptionScheme(String keyTemplateName)131         FileEncryptionScheme(String keyTemplateName) {
132             mKeyTemplateName = keyTemplateName;
133         }
134 
getKeyTemplate()135         KeyTemplate getKeyTemplate() throws GeneralSecurityException {
136             return KeyTemplates.get(mKeyTemplateName);
137         }
138     }
139 
140     /**
141      * Builder class to configure EncryptedFile
142      * @deprecated Use {@link java.io.File} instead.
143      */
144     @Deprecated
145     public static final class Builder {
146         private static final Object sLock = new Object();
147 
148         /**
149          * Builder for an EncryptedFile.
150          *
151          * <p>If the <code>masterKeyAlias</code> used here is for a key that is not yet
152          * created, this method will not be thread safe. Use the alternate signature that is not
153          * deprecated for multi-threaded contexts.
154          *
155          * @deprecated Use {@link #Builder(Context, File, MasterKey, FileEncryptionScheme)} instead.
156          */
157         @Deprecated
Builder(@onNull File file, @NonNull Context context, @NonNull String masterKeyAlias, @NonNull FileEncryptionScheme fileEncryptionScheme)158         public Builder(@NonNull File file,
159                 @NonNull Context context,
160                 @NonNull String masterKeyAlias,
161                 @NonNull FileEncryptionScheme fileEncryptionScheme) {
162             mFile = file;
163             mFileEncryptionScheme = fileEncryptionScheme;
164             mContext = context.getApplicationContext();
165             mMasterKeyAlias = masterKeyAlias;
166         }
167 
168         /**
169          * Builder for an EncryptedFile.
170          */
171         // [StreamFiles]: Because the contents of EncryptedFile are encrypted the use of
172         // a FileDescriptor or Streams are intentionally not supported for the following reasons:
173         // - The encrypted content is tightly coupled to the current installation of the app. If
174         // the app is uninstalled, even if the data remained (such as being stored in a public
175         // directory or another DocumentProvider) it would be (intentionally) unrecoverable.
176         // - If the API did accept either an already opened FileDescriptor or a stream, then it
177         // would be possible for the developer to inadvertently commingle encrypted and plain
178         // text data, which, due to the way the API is structured, could render both encrypted
179         // and unencrypted data irrecoverable.
180         @SuppressLint("StreamFiles")
Builder(@onNull Context context, @NonNull File file, @NonNull MasterKey masterKey, @NonNull FileEncryptionScheme fileEncryptionScheme)181         public Builder(@NonNull Context context,
182                 @NonNull File file,
183                 @NonNull MasterKey masterKey,
184                 @NonNull FileEncryptionScheme fileEncryptionScheme) {
185             mFile = file;
186             mFileEncryptionScheme = fileEncryptionScheme;
187             mContext = context.getApplicationContext();
188             mMasterKeyAlias = masterKey.getKeyAlias();
189         }
190 
191         // Required parameters
192         File mFile;
193         final FileEncryptionScheme mFileEncryptionScheme;
194         final Context mContext;
195         final String mMasterKeyAlias;
196 
197         // Optional parameters
198         String mKeysetPrefName = KEYSET_PREF_NAME;
199         String mKeysetAlias = KEYSET_ALIAS;
200 
201         /**
202          * @param keysetPrefName The SharedPreferences file to store the keyset.
203          * @return This Builder
204          */
setKeysetPrefName(@onNull String keysetPrefName)205         public @NonNull Builder setKeysetPrefName(@NonNull String keysetPrefName) {
206             mKeysetPrefName = keysetPrefName;
207             return this;
208         }
209 
210         /**
211          * @param keysetAlias The alias in the SharedPreferences file to store the keyset.
212          * @return This Builder
213          */
setKeysetAlias(@onNull String keysetAlias)214         public @NonNull Builder setKeysetAlias(@NonNull String keysetAlias) {
215             mKeysetAlias = keysetAlias;
216             return this;
217         }
218 
219         /**
220          * @return An EncryptedFile with the specified parameters.
221          */
build()222         public @NonNull EncryptedFile build() throws GeneralSecurityException, IOException {
223             StreamingAeadConfig.register();
224 
225             AndroidKeysetManager.Builder keysetManagerBuilder = new AndroidKeysetManager.Builder()
226                     .withKeyTemplate(mFileEncryptionScheme.getKeyTemplate())
227                     .withSharedPref(mContext, mKeysetAlias, mKeysetPrefName)
228                     .withMasterKeyUri(MasterKey.KEYSTORE_PATH_URI + mMasterKeyAlias);
229 
230             // Building the keyset manager involves shared pref filesystem operations. To control
231             // access to this global state in multi-threaded contexts we need to ensure mutual
232             // exclusion of the build() function.
233             AndroidKeysetManager androidKeysetManager;
234             synchronized (sLock) {
235                 androidKeysetManager = keysetManagerBuilder.build();
236             }
237 
238             KeysetHandle streamingAeadKeysetHandle = androidKeysetManager.getKeysetHandle();
239             StreamingAead streamingAead =
240                     streamingAeadKeysetHandle.getPrimitive(StreamingAead.class);
241 
242             return new EncryptedFile(mFile, mKeysetAlias, streamingAead, mContext);
243         }
244     }
245 
246     /**
247      * Opens a FileOutputStream for writing that automatically encrypts the data based on the
248      * provided settings.
249      *
250      * <p>Please ensure that the same master key and keyset are  used to decrypt or it
251      * will cause failures.
252      *
253      * @return The FileOutputStream that encrypts all data.
254      * @throws GeneralSecurityException when a bad master key or keyset has been used
255      * @throws IOException              when the file already exists or is not available for writing
256      */
openFileOutput()257     public @NonNull FileOutputStream openFileOutput()
258             throws GeneralSecurityException, IOException {
259         if (mFile.exists()) {
260             throw new IOException("output file already exists, please use a new file: "
261                     + mFile.getName());
262         }
263         FileOutputStream fileOutputStream = new FileOutputStream(mFile);
264         OutputStream encryptingStream = mStreamingAead.newEncryptingStream(fileOutputStream,
265                 mFile.getName().getBytes(UTF_8));
266         return new EncryptedFileOutputStream(fileOutputStream.getFD(), encryptingStream);
267     }
268 
269     /**
270      * Opens a FileInputStream that reads encrypted files based on the previous settings.
271      *
272      * <p>Please ensure that the same master key and keyset are  used to decrypt or it
273      * will cause failures.
274      *
275      * @return The input stream to read previously encrypted data.
276      * @throws GeneralSecurityException when a bad master key or keyset has been used
277      * @throws FileNotFoundException    when the file was not found
278      * @throws IOException              when other I/O errors occur
279      */
openFileInput()280     public @NonNull FileInputStream openFileInput()
281             throws GeneralSecurityException, IOException, FileNotFoundException {
282         if (!mFile.exists()) {
283             throw new FileNotFoundException("file doesn't exist: " + mFile.getName());
284         }
285         FileInputStream fileInputStream = new FileInputStream(mFile);
286         InputStream decryptingStream = mStreamingAead.newDecryptingStream(fileInputStream,
287                 mFile.getName().getBytes(UTF_8));
288         return new EncryptedFileInputStream(fileInputStream.getFD(), decryptingStream);
289     }
290 
291     /**
292      * Encrypted file output stream
293      */
294     private static final class EncryptedFileOutputStream extends FileOutputStream {
295 
296         private final OutputStream mEncryptedOutputStream;
297 
EncryptedFileOutputStream(FileDescriptor descriptor, OutputStream encryptedOutputStream)298         EncryptedFileOutputStream(FileDescriptor descriptor, OutputStream encryptedOutputStream) {
299             super(descriptor);
300             mEncryptedOutputStream = encryptedOutputStream;
301         }
302 
303         @Override
write(byte @NonNull [] b)304         public void write(byte @NonNull [] b) throws IOException {
305             mEncryptedOutputStream.write(b);
306         }
307 
308         @Override
write(int b)309         public void write(int b) throws IOException {
310             mEncryptedOutputStream.write(b);
311         }
312 
313         @Override
write(byte @NonNull [] b, int off, int len)314         public void write(byte @NonNull [] b, int off, int len) throws IOException {
315             mEncryptedOutputStream.write(b, off, len);
316         }
317 
318         @Override
close()319         public void close() throws IOException {
320             mEncryptedOutputStream.close();
321         }
322 
323         @Override
getChannel()324         public @NonNull FileChannel getChannel() {
325             throw new UnsupportedOperationException("For encrypted files, please open the "
326                     + "relevant FileInput/FileOutputStream.");
327         }
328 
329         @Override
flush()330         public void flush() throws IOException {
331             mEncryptedOutputStream.flush();
332         }
333 
334     }
335 
336     /**
337      * Encrypted file input stream
338      */
339     private static final class EncryptedFileInputStream extends FileInputStream {
340 
341         private final InputStream mEncryptedInputStream;
342 
343         private final Object mLock = new Object();
344 
EncryptedFileInputStream(FileDescriptor descriptor, InputStream encryptedInputStream)345         EncryptedFileInputStream(FileDescriptor descriptor,
346                 InputStream encryptedInputStream) {
347             super(descriptor);
348             mEncryptedInputStream = encryptedInputStream;
349         }
350 
351         @Override
read()352         public int read() throws IOException {
353             return mEncryptedInputStream.read();
354         }
355 
356         @Override
read(byte @NonNull [] b)357         public int read(byte @NonNull [] b) throws IOException {
358             return mEncryptedInputStream.read(b);
359         }
360 
361         @Override
read(byte @NonNull [] b, int off, int len)362         public int read(byte @NonNull [] b, int off, int len) throws IOException {
363             return mEncryptedInputStream.read(b, off, len);
364         }
365 
366         @Override
skip(long n)367         public long skip(long n) throws IOException {
368             return mEncryptedInputStream.skip(n);
369         }
370 
371         @Override
available()372         public int available() throws IOException {
373             return mEncryptedInputStream.available();
374         }
375 
376         @Override
close()377         public void close() throws IOException {
378             mEncryptedInputStream.close();
379         }
380 
381         @Override
getChannel()382         public FileChannel getChannel() {
383             throw new UnsupportedOperationException("For encrypted files, please open the "
384                     + "relevant FileInput/FileOutputStream.");
385         }
386 
387         @Override
mark(int readLimit)388         public void mark(int readLimit) {
389             synchronized (mLock) {
390                 mEncryptedInputStream.mark(readLimit);
391             }
392         }
393 
394         @Override
reset()395         public void reset() throws IOException {
396             synchronized (mLock) {
397                 mEncryptedInputStream.reset();
398             }
399         }
400 
401         @Override
markSupported()402         public boolean markSupported() {
403             return mEncryptedInputStream.markSupported();
404         }
405 
406     }
407 
408 }
409