篇章目标要点
国家法律法规越来越注重用户信息安全的问题,针对敏感个人信息也要求实行必要的加密存储手段,比如针对手机号/身份证/密码等敏感信息需要进行加密存储。Androidx提供了Security框架可以支持必要信息的加密和解密。
Security源码分析
Security的加密逻辑是在google开发的Tink框架中实现的,其关键的类和方法信息如下文所述。 其中StreamingAhead是一个接口类,其实现类包括AesCtrHmacStreaming和AesCtrHkdfStreaming分别对应的是两种不同的加密方式。这次选择AesCtrHmacStreaming作为了解其加密原理的研究对象。StreamingAeadEncryptingStream类中 加密写入流源码如下
NonceBasedStreamingAead streamAead, OutputStream ciphertextChannel, byte[] associatedData)
throws GeneralSecurityException, IOException {
super(ciphertextChannel);
encrypter = streamAead.newStreamSegmentEncrypter(associatedData);
plaintextSegmentSize = streamAead.getPlaintextSegmentSize();
//明文Buffer
ptBuffer = ByteBuffer.allocate(plaintextSegmentSize);
//密文Buffer
ctBuffer = ByteBuffer.allocate(streamAead.getCiphertextSegmentSize());
//明文Buffer大小的修正:去除Header和首帧数据offset
ptBuffer.limit(plaintextSegmentSize - streamAead.getCiphertextOffset());
ByteBuffer header = encrypter.getHeader();
byte[] headerBytes = new byte[header.remaining()];
header.get(headerBytes);
//先写密文的头部数据
out.write(headerBytes);
open = true;
}
写入数据
@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;
//循环进行数据加密
while (remaining > ptBuffer.remaining()) {
int sliceSize = ptBuffer.remaining();
ByteBuffer slice = ByteBuffer.wrap(pt, startPosition, sliceSize);
startPosition += sliceSize;
remaining -= sliceSize;
try {
ptBuffer.flip();
ctBuffer.clear();
//对本次处理的数据转为密文
encrypter.encryptSegment(ptBuffer, slice, false, ctBuffer);
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}
ctBuffer.flip();
//io写入转换的密文片段
out.write(ctBuffer.array(), ctBuffer.position(), ctBuffer.remaining());
ptBuffer.clear();
ptBuffer.limit(plaintextSegmentSize);
}
//更新待转换明文的起点指针,同时更新剩余的待转换数据长度
ptBuffer.put(pt, startPosition, remaining);
}
加密算法的执行
@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++;
//执行向密文的转换
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);
}
针对Buffer加密过程位于CypherSpi类代码如下,内部核心业务在engineUpdate和engineDoFinal方法中,由于暂未找到源码,暂时无法进一步阅读。
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");
}
}
对明文的加密过程在CipherSpi的子类中予以实现,由于自用工具限制暂时还未找到相关类,在此放置一张加密过程的原理框图
加密存储和解密用法
本节简要阐述如何使用Security进行加密和解密,将以一个存储手机号为例展示其基本用法 1.完成效果 (2)添加依赖
//安全相关依赖
implementation "androidx.security:security-crypto:1.0.0"
(3)封装加密工具 通过对EncryptedFile的编码写入和解码读取进行封装,简化外部的调用。其中每一个存储的敏感信息都需要指定一个String类型的key,作为加密文件保存所用的名称。选择的加密方式为AES256位长度密钥的对称性加密算法。
/**
* 执行加密存储和解密读取的工具类, 需要外部为存储的加密字符串设置key值,key值加上".txt"后缀将作为加密文件名<P/>
* 加密后的文件放在同一文件夹中
*/
public class SecurityFile {
private KeyGenParameterSpec mKeyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
//加密文件存放位置
private final String DIR_ENCRYPTED_FILE = "/data/data/"+ SecurityApp.getInstance().getPackageName() + "/encrypted";
/**
* 加密存储字符串
* @param inputString 待存储的字符串
*/
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();
}
//创建加密文件
EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(DIR_ENCRYPTED_FILE +"/" +key+".txt"),
SecurityApp.getInstance(), mainKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build();
//将待存储字符串写入文件
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();
}
}
/**
* 解密存储的字符串
* @param key 字符串key值
* @return 返回解密后的字符串
*/
public String decode(String key){
try{
String mainKeyAlias = MasterKeys.getOrCreate(mKeyGenParameterSpec);
File dir = new File(DIR_ENCRYPTED_FILE);
if(!dir.exists()){
return null;
}
//创建加密文件
EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(DIR_ENCRYPTED_FILE, key+".txt"),
SecurityApp.getInstance(), mainKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build();
//读取加密文件内容
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();
//转为字符串
return new String(bytes);
}catch (GeneralSecurityException e){
e.printStackTrace();
return null;
}catch (IOException e){
e.printStackTrace();
return null;
}
}
}
(4)在AndroidManifest添加读写权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
(5)调用示例 写了一个很简单的布局展示存储和读取效果,在此就不展示layout定义了,调用的代码示例如下
public class MainActivity extends AppCompatActivity {
//执行加密存储和读取的对象
private SecurityFile mSecurityFile;
private Button mButtonOriginText, mButtonReadText;
private TextView mTextViewOriginText, mTextViewReadText;
//待加密存储的手机号
private final static String MOBILE_PHONE_NO = "13632211233";
//手机号存储时所用的key
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);
//点击时加密写入明文
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, "手机号加密存储成功",Toast.LENGTH_SHORT).show();
}
});
//点击时解密读取密文
mButtonReadText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String decodeString = mSecurityFile.decode(MOBILE_KEY);
mTextViewReadText.setText(decodeString);
Toast.makeText(MainActivity.this, "手机号信息解密读取成功",Toast.LENGTH_SHORT).show();
}
});
}
}
学习心得
以上即是全部使用过程,了解后可以掌握敏感信息的加密存储。缺失的内容是对明文的加密过程是在CipherSpi的子类中予以实现,由于自用工具限制暂时还未找到相关类,后续会补充相关的阅读心得
|