Key points of text objectives
National laws and regulations pay more and more attention to the problem of user information Security. For sensitive personal information, necessary encrypted storage methods are also required, such as encrypted storage for sensitive information such as mobile phone number / ID card / password. Android x provides a Security framework that can support the encryption and decryption of necessary information.
Security source code analysis
The encryption logic of Security is implemented in the Tink framework developed by google, and its key class and method information are described below.
StreamingAhead is an interface class. Its implementation classes include AesCtrHmacStreaming and AesCtrHkdfStreaming, which respectively correspond to two different encryption methods. This time, AesCtrHmacStreaming is selected as the research object to understand its encryption principle. In the StreamingAeadEncryptingStream class
The source code of encrypted write stream is as follows
NonceBasedStreamingAead streamAead, OutputStream ciphertextChannel, byte[] associatedData) throws GeneralSecurityException, IOException { super(ciphertextChannel); encrypter = streamAead.newStreamSegmentEncrypter(associatedData); plaintextSegmentSize = streamAead.getPlaintextSegmentSize(); //Plaintext Buffer ptBuffer = ByteBuffer.allocate(plaintextSegmentSize); //Ciphertext Buffer ctBuffer = ByteBuffer.allocate(streamAead.getCiphertextSegmentSize()); //Correction of plaintext Buffer size: remove Header and first frame data offset ptBuffer.limit(plaintextSegmentSize - streamAead.getCiphertextOffset()); ByteBuffer header = encrypter.getHeader(); byte[] headerBytes = new byte[header.remaining()]; header.get(headerBytes); //First write the header data of the ciphertext out.write(headerBytes); open = true; }
Write data
@Override public synchronized void write(byte[] pt, int offset, int length) throws IOException { if (!open) { throw new IOException("Trying to write to closed stream"); } int startPosition = offset; int remaining = length; //Loop data encryption while (remaining > ptBuffer.remaining()) { int sliceSize = ptBuffer.remaining(); ByteBuffer slice = ByteBuffer.wrap(pt, startPosition, sliceSize); startPosition += sliceSize; remaining -= sliceSize; try { ptBuffer.flip(); ctBuffer.clear(); //Convert the data processed this time into ciphertext encrypter.encryptSegment(ptBuffer, slice, false, ctBuffer); } catch (GeneralSecurityException ex) { throw new IOException(ex); } ctBuffer.flip(); //io write converted ciphertext fragment out.write(ctBuffer.array(), ctBuffer.position(), ctBuffer.remaining()); ptBuffer.clear(); ptBuffer.limit(plaintextSegmentSize); } //Update the starting point pointer of the plaintext to be converted, and update the remaining data length to be converted ptBuffer.put(pt, startPosition, remaining); }
Implementation of encryption algorithm
@Override public synchronized void encryptSegment( ByteBuffer plaintext, boolean isLastSegment, ByteBuffer ciphertext) throws GeneralSecurityException { int position = ciphertext.position(); byte[] nonce = nonceForSegment(noncePrefix, encryptedSegments, isLastSegment); cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(nonce)); encryptedSegments++; //Perform conversion to ciphertext cipher.doFinal(plaintext, ciphertext); ByteBuffer ctCopy = ciphertext.duplicate(); ctCopy.flip(); ctCopy.position(position); mac.init(hmacKeySpec); mac.update(nonce); mac.update(ctCopy); byte[] tag = mac.doFinal(); ciphertext.put(tag, 0, tagSizeInBytes); }
The Buffer encryption process is located in the CypherSpi class. The code is as follows. The internal core business is in the engineUpdate and engineDoFinal methods. Since the source code has not been found yet, it can not be read further.
private int bufferCrypt(ByteBuffer var1, ByteBuffer var2, boolean var3) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { if (var1 != null && var2 != null) { int var4 = var1.position(); int var5 = var1.limit(); int var6 = var5 - var4; if (var3 && var6 == 0) { return 0; } else { int var7 = this.engineGetOutputSize(var6); if (var2.remaining() < var7) { throw new ShortBufferException("Need at least " + var7 + " bytes of space in output buffer"); } else { boolean var8 = var1.hasArray(); boolean var9 = var2.hasArray(); byte[] var10; int var11; byte[] var12; int var13; int var14; int var23; if (var8 && var9) { var10 = var1.array(); var11 = var1.arrayOffset() + var4; var12 = var2.array(); var13 = var2.position(); var14 = var2.arrayOffset() + var13; if (var3) { var23 = this.engineUpdate(var10, var11, var6, var12, var14); } else { var23 = this.engineDoFinal(var10, var11, var6, var12, var14); } var1.position(var5); var2.position(var13 + var23); return var23; } else { int var16; if (!var8 && var9) { int var19 = var2.position(); byte[] var21 = var2.array(); int var20 = var2.arrayOffset() + var19; byte[] var22 = new byte[getTempArraySize(var6)]; var14 = 0; do { var23 = Math.min(var6, var22.length); if (var23 > 0) { var1.get(var22, 0, var23); } if (!var3 && var6 == var23) { var16 = this.engineDoFinal(var22, 0, var23, var21, var20); } else { var16 = this.engineUpdate(var22, 0, var23, var21, var20); } var14 += var16; var20 += var16; var6 -= var23; } while(var6 > 0); var2.position(var19 + var14); return var14; } else { if (var8) { var10 = var1.array(); var11 = var1.arrayOffset() + var4; } else { var10 = new byte[getTempArraySize(var6)]; var11 = 0; } var12 = new byte[getTempArraySize(var7)]; var13 = var12.length; var14 = 0; boolean var15 = false; do { var16 = Math.min(var6, var13 == 0 ? var10.length : var13); if (!var8 && !var15 && var16 > 0) { var1.get(var10, 0, var16); var11 = 0; } try { int var17; if (!var3 && var6 == var16) { var17 = this.engineDoFinal(var10, var11, var16, var12, 0); } else { var17 = this.engineUpdate(var10, var11, var16, var12, 0); } var15 = false; var11 += var16; var6 -= var16; if (var17 > 0) { var2.put(var12, 0, var17); var14 += var17; } } catch (ShortBufferException var18) { if (var15) { throw (ProviderException)(new ProviderException("Could not determine buffer size")).initCause(var18); } var15 = true; var13 = this.engineGetOutputSize(var16); var12 = new byte[var13]; } } while(var6 > 0); if (var8) { var1.position(var5); } return var14; } } } } } else { throw new NullPointerException("Input and output buffers must not be null"); } }
The encryption process of plaintext is implemented in the subclass of CipherSpi. Due to the limitation of self-use tools, the relevant class has not been found yet. A schematic diagram of the encryption process is placed here
Encrypted storage and decryption usage
This section briefly describes how to use Security for encryption and decryption, and will take a stored mobile phone number as an example to show its basic usage
1. Completion effect
(2) Add dependency
//Security related dependencies implementation "androidx.security:security-crypto:1.0.0"
(3) Encapsulate encryption tools
By encapsulating the encoded write and decoded read of EncryptedFile, the external call is simplified. Each stored sensitive information needs to specify a String type key as the name used to save the encrypted file. The selected encryption method is the symmetric encryption algorithm of AES256 bit length key.
/** * For the tool class for encrypted storage and decryption reading, the key value needs to be set externally for the stored encrypted string. The key value plus ". txt" suffix will be used as the encrypted file name < P / > * The encrypted files are placed in the same folder */ public class SecurityFile { private KeyGenParameterSpec mKeyGenParameterSpec = MasterKeys.AES256_GCM_SPEC; //Encrypted file storage location private final String DIR_ENCRYPTED_FILE = "/data/data/"+ SecurityApp.getInstance().getPackageName() + "/encrypted"; /** * Encrypted storage string * @param inputString String to be stored */ public void encode(String inputString, String key){ try{ String mainKeyAlias = MasterKeys.getOrCreate(mKeyGenParameterSpec); File dir = new File(DIR_ENCRYPTED_FILE); if(!dir.exists()){ dir.mkdir(); } //Create encrypted file EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(DIR_ENCRYPTED_FILE +"/" +key+".txt"), SecurityApp.getInstance(), mainKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); //Write the string to be stored to the file OutputStream outputStream = encryptedFile.openFileOutput(); outputStream.write(inputString.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); }catch (GeneralSecurityException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } } /** * Decrypt stored string * @param key String key value * @return Returns the decrypted string */ public String decode(String key){ try{ String mainKeyAlias = MasterKeys.getOrCreate(mKeyGenParameterSpec); File dir = new File(DIR_ENCRYPTED_FILE); if(!dir.exists()){ return null; } //Create encrypted file EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(DIR_ENCRYPTED_FILE, key+".txt"), SecurityApp.getInstance(), mainKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); //Read encrypted file contents InputStream inputStream = encryptedFile.openFileInput(); ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(); int nextByte = inputStream.read(); while (nextByte != -1){ arrayOutputStream.write(nextByte); nextByte = inputStream.read(); } byte[] bytes = arrayOutputStream.toByteArray(); inputStream.close(); arrayOutputStream.close(); //Convert to string return new String(bytes); }catch (GeneralSecurityException e){ e.printStackTrace(); return null; }catch (IOException e){ e.printStackTrace(); return null; } } }
(4) Add read and write permissions in AndroidManifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
(5) Call example
A very simple layout is written to show the storage and reading effects. The layout definition is not shown here. The code example of the call is as follows
public class MainActivity extends AppCompatActivity { //Objects that perform encrypted storage and reading private SecurityFile mSecurityFile; private Button mButtonOriginText, mButtonReadText; private TextView mTextViewOriginText, mTextViewReadText; //Mobile phone number to be encrypted and stored private final static String MOBILE_PHONE_NO = "13632211233"; //key used when storing mobile phone number private final static String MOBILE_KEY = "mobile"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSecurityFile = new SecurityFile(); initView(); } private void initView(){ mButtonOriginText = findViewById(R.id.button_origin_text); mButtonReadText = findViewById(R.id.button_read_text); mTextViewOriginText = findViewById(R.id.textview_origin_text); mTextViewReadText = findViewById(R.id.textview_read_text); //Write plaintext encrypted when clicked mButtonOriginText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mTextViewOriginText.setText(MOBILE_PHONE_NO); mSecurityFile.encode(MOBILE_PHONE_NO, MOBILE_KEY); Toast.makeText(MainActivity.this, "Encrypted storage of mobile phone number succeeded",Toast.LENGTH_SHORT).show(); } }); //Decrypt and read ciphertext when clicked mButtonReadText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String decodeString = mSecurityFile.decode(MOBILE_KEY); mTextViewReadText.setText(decodeString); Toast.makeText(MainActivity.this, "Decryption and reading of mobile phone number information succeeded",Toast.LENGTH_SHORT).show(); } }); } }
Learning experience
The above is the whole use process. After understanding, you can master the encrypted storage of sensitive information. The missing content is that the encryption process of plaintext is implemented in the subclass of CipherSpi. Due to the limitation of self-use tools, the relevant class has not been found yet. Relevant reading experience will be supplemented later