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