/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.security.crypto;
import static androidx.security.crypto.MasterKey.KEYSTORE_PATH_URI;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.crypto.tink.KeyTemplate;
import com.google.crypto.tink.KeyTemplates;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.StreamingAead;
import com.google.crypto.tink.integration.android.AndroidKeysetManager;
import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.security.GeneralSecurityException;
/**
* Class used to create and read encrypted files.
*
*
* WARNING: The encrypted file should not be backed up with Auto Backup. When restoring the
* file it is likely the key used to encrypt it will no longer be present. You should exclude all
* EncryptedFile
s from backup using
* backup rules.
* Be aware that if you are not explicitly calling setKeysetPrefName()
there is also a
* silently-created default preferences file created at
*
* ApplicationProvider * .getApplicationContext() * .getFilesDir() * .getParent() + "/shared_prefs/__androidx_security_crypto_encrypted_file_pref__" ** * This preferences file (or any others created with a custom specified location) also should be * excluded from backups. *
* MasterKey masterKey = new MasterKey.Builder(context) * .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) * .build(); * * File file = new File(context.getFilesDir(), "secret_data"); * EncryptedFile encryptedFile = EncryptedFile.Builder( * context, * file, * masterKey, * EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB * ).build(); * * // write to the encrypted file * FileOutputStream encryptedOutputStream = encryptedFile.openFileOutput(); * * // read the encrypted file * FileInputStream encryptedInputStream = encryptedFile.openFileInput(); **/ public final class EncryptedFile { private static final String KEYSET_PREF_NAME = "__androidx_security_crypto_encrypted_file_pref__"; private static final String KEYSET_ALIAS = "__androidx_security_crypto_encrypted_file_keyset__"; final File mFile; final Context mContext; final String mMasterKeyAlias; final StreamingAead mStreamingAead; EncryptedFile( @NonNull File file, @NonNull String masterKeyAlias, @NonNull StreamingAead streamingAead, @NonNull Context context) { mFile = file; mContext = context; mMasterKeyAlias = masterKeyAlias; mStreamingAead = streamingAead; } /** * The encryption scheme to encrypt files. */ public enum FileEncryptionScheme { /** * The file content is encrypted using StreamingAead with AES-GCM, with the file name as * associated data. * *
For more information please see the Tink documentation: * *
AesGcmHkdfStreamingKeyManager.aes256GcmHkdf4KBTemplate() */ AES256_GCM_HKDF_4KB("AES256_GCM_HKDF_4KB"); private final String mKeyTemplateName; FileEncryptionScheme(String keyTemplateName) { mKeyTemplateName = keyTemplateName; } KeyTemplate getKeyTemplate() throws GeneralSecurityException { return KeyTemplates.get(mKeyTemplateName); } } /** * Builder class to configure EncryptedFile */ public static final class Builder { private static final Object sLock = new Object(); /** * Builder for an EncryptedFile. * *
If the masterKeyAlias
used here is for a key that is not yet
* created, this method will not be thread safe. Use the alternate signature that is not
* deprecated for multi-threaded contexts.
*
* @deprecated Use {@link #Builder(Context, File, MasterKey, FileEncryptionScheme)} instead.
*/
@Deprecated
public Builder(@NonNull File file,
@NonNull Context context,
@NonNull String masterKeyAlias,
@NonNull FileEncryptionScheme fileEncryptionScheme) {
mFile = file;
mFileEncryptionScheme = fileEncryptionScheme;
mContext = context.getApplicationContext();
mMasterKeyAlias = masterKeyAlias;
}
/**
* Builder for an EncryptedFile.
*/
// [StreamFiles]: Because the contents of EncryptedFile are encrypted the use of
// a FileDescriptor or Streams are intentionally not supported for the following reasons:
// - The encrypted content is tightly coupled to the current installation of the app. If
// the app is uninstalled, even if the data remained (such as being stored in a public
// directory or another DocumentProvider) it would be (intentionally) unrecoverable.
// - If the API did accept either an already opened FileDescriptor or a stream, then it
// would be possible for the developer to inadvertently commingle encrypted and plain
// text data, which, due to the way the API is structured, could render both encrypted
// and unencrypted data irrecoverable.
@SuppressLint("StreamFiles")
public Builder(@NonNull Context context,
@NonNull File file,
@NonNull MasterKey masterKey,
@NonNull FileEncryptionScheme fileEncryptionScheme) {
mFile = file;
mFileEncryptionScheme = fileEncryptionScheme;
mContext = context.getApplicationContext();
mMasterKeyAlias = masterKey.getKeyAlias();
}
// Required parameters
File mFile;
final FileEncryptionScheme mFileEncryptionScheme;
final Context mContext;
final String mMasterKeyAlias;
// Optional parameters
String mKeysetPrefName = KEYSET_PREF_NAME;
String mKeysetAlias = KEYSET_ALIAS;
/**
* @param keysetPrefName The SharedPreferences file to store the keyset.
* @return This Builder
*/
@NonNull
public Builder setKeysetPrefName(@NonNull String keysetPrefName) {
mKeysetPrefName = keysetPrefName;
return this;
}
/**
* @param keysetAlias The alias in the SharedPreferences file to store the keyset.
* @return This Builder
*/
@NonNull
public Builder setKeysetAlias(@NonNull String keysetAlias) {
mKeysetAlias = keysetAlias;
return this;
}
/**
* @return An EncryptedFile with the specified parameters.
*/
@NonNull
public EncryptedFile build() throws GeneralSecurityException, IOException {
StreamingAeadConfig.register();
AndroidKeysetManager.Builder keysetManagerBuilder = new AndroidKeysetManager.Builder()
.withKeyTemplate(mFileEncryptionScheme.getKeyTemplate())
.withSharedPref(mContext, mKeysetAlias, mKeysetPrefName)
.withMasterKeyUri(KEYSTORE_PATH_URI + mMasterKeyAlias);
// Building the keyset manager involves shared pref filesystem operations. To control
// access to this global state in multi-threaded contexts we need to ensure mutual
// exclusion of the build() function.
AndroidKeysetManager androidKeysetManager;
synchronized (sLock) {
androidKeysetManager = keysetManagerBuilder.build();
}
KeysetHandle streamingAeadKeysetHandle = androidKeysetManager.getKeysetHandle();
StreamingAead streamingAead =
streamingAeadKeysetHandle.getPrimitive(StreamingAead.class);
return new EncryptedFile(mFile, mKeysetAlias, streamingAead, mContext);
}
}
/**
* Opens a FileOutputStream for writing that automatically encrypts the data based on the
* provided settings.
*
*
Please ensure that the same master key and keyset are used to decrypt or it * will cause failures. * * @return The FileOutputStream that encrypts all data. * @throws GeneralSecurityException when a bad master key or keyset has been used * @throws IOException when the file already exists or is not available for writing */ @NonNull public FileOutputStream openFileOutput() throws GeneralSecurityException, IOException { if (mFile.exists()) { throw new IOException("output file already exists, please use a new file: " + mFile.getName()); } FileOutputStream fileOutputStream = new FileOutputStream(mFile); OutputStream encryptingStream = mStreamingAead.newEncryptingStream(fileOutputStream, mFile.getName().getBytes(UTF_8)); return new EncryptedFileOutputStream(fileOutputStream.getFD(), encryptingStream); } /** * Opens a FileInputStream that reads encrypted files based on the previous settings. * *
Please ensure that the same master key and keyset are used to decrypt or it * will cause failures. * * @return The input stream to read previously encrypted data. * @throws GeneralSecurityException when a bad master key or keyset has been used * @throws FileNotFoundException when the file was not found * @throws IOException when other I/O errors occur */ @NonNull public FileInputStream openFileInput() throws GeneralSecurityException, IOException, FileNotFoundException { if (!mFile.exists()) { throw new FileNotFoundException("file doesn't exist: " + mFile.getName()); } FileInputStream fileInputStream = new FileInputStream(mFile); InputStream decryptingStream = mStreamingAead.newDecryptingStream(fileInputStream, mFile.getName().getBytes(UTF_8)); return new EncryptedFileInputStream(fileInputStream.getFD(), decryptingStream); } /** * Encrypted file output stream */ private static final class EncryptedFileOutputStream extends FileOutputStream { private final OutputStream mEncryptedOutputStream; EncryptedFileOutputStream(FileDescriptor descriptor, OutputStream encryptedOutputStream) { super(descriptor); mEncryptedOutputStream = encryptedOutputStream; } @Override public void write(@NonNull byte[] b) throws IOException { mEncryptedOutputStream.write(b); } @Override public void write(int b) throws IOException { mEncryptedOutputStream.write(b); } @Override public void write(@NonNull byte[] b, int off, int len) throws IOException { mEncryptedOutputStream.write(b, off, len); } @Override public void close() throws IOException { mEncryptedOutputStream.close(); } @NonNull @Override public FileChannel getChannel() { throw new UnsupportedOperationException("For encrypted files, please open the " + "relevant FileInput/FileOutputStream."); } @Override public void flush() throws IOException { mEncryptedOutputStream.flush(); } } /** * Encrypted file input stream */ private static final class EncryptedFileInputStream extends FileInputStream { private final InputStream mEncryptedInputStream; private final Object mLock = new Object(); EncryptedFileInputStream(FileDescriptor descriptor, InputStream encryptedInputStream) { super(descriptor); mEncryptedInputStream = encryptedInputStream; } @Override public int read() throws IOException { return mEncryptedInputStream.read(); } @Override public int read(@NonNull byte[] b) throws IOException { return mEncryptedInputStream.read(b); } @Override public int read(@NonNull byte[] b, int off, int len) throws IOException { return mEncryptedInputStream.read(b, off, len); } @Override public long skip(long n) throws IOException { return mEncryptedInputStream.skip(n); } @Override public int available() throws IOException { return mEncryptedInputStream.available(); } @Override public void close() throws IOException { mEncryptedInputStream.close(); } @Override public FileChannel getChannel() { throw new UnsupportedOperationException("For encrypted files, please open the " + "relevant FileInput/FileOutputStream."); } @Override public void mark(int readLimit) { synchronized (mLock) { mEncryptedInputStream.mark(readLimit); } } @Override public void reset() throws IOException { synchronized (mLock) { mEncryptedInputStream.reset(); } } @Override public boolean markSupported() { return mEncryptedInputStream.markSupported(); } } }