Android encrypted storage Security source code analysis and basic usage introduction | Android sensitive information is encrypted and stored through AES256

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

Tags: Android

Posted on Sun, 10 Oct 2021 09:30:57 -0400 by teo99