文章翻譯:趙潔
發(fā)表時間:2015 年 7 月 10 日
原文作者:OBARO OGBO
文章分類:云計算與安全
本文主要介紹如何使用安卓密鑰庫來輕松地管理應用程序密鑰。有了密鑰庫,你就會發(fā)現(xiàn),無論是存儲密鑰、還是刪除密鑰,以及加密和解密用戶所提供的文本,都不再是難題。所以不妨來學習一下吧。
幾個月前,Godfrey Nolan 寫了一篇超棒的文章,討論了安卓應用程序的開發(fā)者如何存儲用戶的密碼和敏感/私人信息。安卓密鑰庫提供一個安全系統(tǒng)等級證書存儲。有了密鑰庫,一個應用程序可以創(chuàng)建一個新的私人的/公共的密鑰對,并在保存到私人存儲文件夾之前就可以用于加密應用程序的秘密。在本文中,我們將要展示如何使用安卓密鑰庫來創(chuàng)建和刪除密鑰,以及如何使用這些密鑰來加密和解密用戶提供的文本。
在我們編碼之前,了解一點關于安卓密鑰庫的事情以及其性能是很有幫助的。密鑰庫不是直接用來存儲應用程序的秘密的,比如說密碼,而是提供一個安全容器,應用程序用其來存儲私鑰,在某種程度上對于惡意(未經授權)的用戶和應用程序來說,檢索是相當困難的。
正如其名,一個應用程序可以在密鑰庫里存儲多樣的密鑰,但是只能查看和問詢它自己的密鑰。理想上,有了密鑰庫,應用程序可以生成或獲取一個存儲在密鑰庫的私人或公共的密鑰對。公鑰可用于加密應用程序的秘密,在其被存儲在應用程序指定的文件夾之前。而私鑰是在需要的時候,解密相同的信息。
盡管安卓密鑰庫供應商在 API 等級 18(安卓 4.3)中引進,密鑰庫本身是在 API 1 的時候可被使用,由 VPN 和 WiFi 系統(tǒng)限制使用。
密鑰庫本身是使用用戶自身的鎖屏 pin 或者密碼來加密的,因此,當裝置屏幕鎖上的話,密鑰庫是不能使用的。謹記如果你有一個后臺服務,可能需要訪問你的應用程序的秘密。
我們實例化應用程序的主要布局是一個 ListView,由應用程序所創(chuàng)建的所有密鑰(實際上是密鑰別名或名稱)所組成的項目。保存為 layout/activity_main.xml。
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="com.sample.foo.simplekeystoreapp.MainActivity">
</ListView>
列表上的每一個項目包含一個 TextView,代表了密鑰的別名,一個刪除密鑰的按鈕,以及加密和解密文本的按鈕。這是我們項目中的 layout/list_item.xml。
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_list_item-300x88.jpg" alt="image" />
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cardBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="4dp"
android:layout_margin="5dp">
<TextView
android:id="@+id/keyAlias"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="30dp"/>
<Button
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/keyAlias"
android:layout_alignParentLeft="true"
android:layout_centerHorizontal="true"
android:text="@string/delete"
style="@style/Base.Widget.AppCompat.Button.Borderless" />
<Button
android:id="@+id/encryptButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/keyAlias"
android:layout_alignRight="@+id/keyAlias"
android:text="@string/encrypt"
style="@style/Base.Widget.AppCompat.Button.Borderless"/>
<Button
android:id="@+id/decryptButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/keyAlias"
android:layout_toLeftOf="@+id/encryptButton"
android:text="@string/decrypt"
style="@style/Base.Widget.AppCompat.Button.Borderless"/>
</RelativeLayout>
使用此方法將列表標題添加到 ListView。
View listHeader = View.inflate(this, R.layout.activity_main_header, null);
listView.addHeaderView(listHeader);
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_layout-300x533.jpg" alt="image" />
在上面的圖片中,ListView 當前是空的,所以只有列表標題是可以看到的。列表標題相當明確,在頂端是一個 EditText,當創(chuàng)建一個密鑰時需要一個字符串作為別名。生成新的密鑰的按鈕就在這個的下方。按鈕后是三個 EditTexts,一個是需要輸入的字符串被加密,另一個展示了加密的結果,然后第三個展示了解密字符串(一個成功的解密)。此文件保存在 layout/activity_main_header.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<EditText
android:id="@+id/aliasText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:hint="@string/key_alias"/>
<Button
android:id="@+id/generateKeyPair"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/aliasText"
android:layout_centerHorizontal="true"
android:layout_alignParentRight="true"
android:text="@string/generate"
android:onClick="createNewKeys" />
<EditText
android:id="@+id/startText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/generateKeyPair"
android:layout_centerHorizontal="true"
android:hint="@string/initial_text"/>
<EditText
android:id="@+id/encryptedText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/startText"
android:layout_centerHorizontal="true"
android:editable="false"
android:textIsSelectable="true"
android:hint="@string/final_text"/>
<EditText
android:id="@+id/decryptedText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/encryptedText"
android:layout_centerHorizontal="true"
android:editable="false"
android:textIsSelectable="true"
android:hint="@string/decrypt_result"/>
</RelativeLayout>
關于任何活動,我們開始都使用 onCreate() 方法。我們做的第一件事就是引用 AndroidKeyStore,然后對其初始化,使用:
Keystore.getInstance("AndroidKeyStore");
keystore.load(null)
然后我們調用 refreshKeys() 方法(接下來討論)來列出我們應用程序存儲在密鑰庫所有的密鑰。這個保證了在密鑰庫中的任何的密鑰在 ListView 初始化之后立即顯示。
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_keys-300x533.jpg" alt="image" />
要獲取可在密鑰庫應用程序中所有密鑰的列舉,只需調用 aliases() 方法。在我們下方的 refreshKeys() 方法中,我們獲取密鑰庫別名,然后把返回的字符串放置到一個 ArrayList(由我們 ListView 的適配器所使用)中。
private void refreshKeys() {
keyAliases = new ArrayList<>();
try {
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
keyAliases.add(aliases.nextElement());
}
}
catch(Exception e) {}
if(listAdapter != null)
listAdapter.notifyDataSetChanged();
}
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_new_key-300x533.jpg" alt="image" />
每一個由應用程序創(chuàng)建的密鑰必須有一個獨特的別名,可以是任何字符串。我們使用一個 KeyPairGeneratorSpec 對象來創(chuàng)建我們所需密鑰的規(guī)格。你可以設置密鑰(setStartDate() 和 setEndDate())的有效期,設置別名和其他的主體(自簽密鑰)。該主體必須是一個 X500Principal 對象,解析到一個字符串的格式“CN=通用名稱,O=組織,C=國家”。
要生成一個私人或公共的密鑰對,我們需要一個 KeyPairGenerator 對象。我們獲取 KeyPairGenerator 集合的實例來使用 “AndroidKeyStore” 的 RSA 算法。調用 generateKeyPair() 創(chuàng)建新的密鑰對(私鑰和相應的公鑰),并將其添加到密鑰庫中。
public void createNewKeys(View view) {
String alias = aliasText.getText().toString();
try {
// Create new key if needed
if (!keyStore.containsAlias(alias)) {
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 1);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
.setAlias(alias)
.setSubject(new X500Principal("CN=Sample Name, O=Android Authority"))
.setSerialNumber(BigInteger.ONE)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
generator.initialize(spec);
KeyPair keyPair = generator.generateKeyPair();
}
} catch (Exception e) {
Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
refreshKeys();
}
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_delete-300x533.jpg" alt="image" />
從密鑰庫中刪除一個密鑰相當簡單。有密鑰別名做準備,調用 keystore.deleteEntry(keyAlias)。沒有辦法重新存儲一個刪除了的密鑰,所以在刪除之前要確定。
public void deleteKey(final String alias) {
AlertDialog alertDialog =new AlertDialog.Builder(this)
.setTitle("Delete Key")
.setMessage("Do you want to delete the key \"" + alias + "\" from the keystore?")
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
keyStore.deleteEntry(alias);
refreshKeys();
} catch (KeyStoreException e) {
Toast.makeText(MainActivity.this,
"Exception " + e.getMessage() + " occured",
Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
dialog.dismiss();
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
alertDialog.show();
}
http://wiki.jikexueyuan.com/project/wiki-journal-201507-1/images/aa_keystore_encrypt-300x533.jpg" alt="image" />
加密一個文本塊由密鑰對的公鑰執(zhí)行。我們檢索公鑰,請求一個密碼,使用我們更喜歡的加密或解密轉換(“RSA/ECB/PKCS1Padding”),然后初始化密碼,使用檢索到的公鑰來執(zhí)行加密(Cipher.ENCRYPT_MODE)。密碼操作(和返回)一個字節(jié) []。我們將密碼包含在 CipherOutputStream 中,和 ByteArrayOutputStream 一起來處理加密復雜性。加密進程的結果就是轉化成一個顯示為 Base64 的字符串。
public void encryptString(String alias) {
try {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey();
// Encrypt the text
String initialText = startText.getText().toString();
if(initialText.isEmpty()) {
Toast.makeText(this, "Enter text in the 'Initial Text' widget", Toast.LENGTH_LONG).show();
return;
}
Cipher input = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
input.init(Cipher.ENCRYPT_MODE, publicKey);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(
outputStream, input);
cipherOutputStream.write(initialText.getBytes("UTF-8"));
cipherOutputStream.close();
byte [] vals = outputStream.toByteArray();
encryptedText.setText(Base64.encodeToString(vals, Base64.DEFAULT));
} catch (Exception e) {
Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}
解密基本上是加密的逆過程。解密是通過使用密鑰對的私鑰完成的。然后我們使用和加密相同的轉化算法來初始化一個密碼,但是設置 Cipher.DECRYPT_MODE。Base64 字符串解碼為一個字節(jié)[],然后放置在 ByteArrayInputStream 中。然后我們使用一個 CipherInputStream 來解密數據為一個字節(jié)[]。然后顯示為一個字符串。
public void decryptString(String alias) {
try {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey();
Cipher output = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
output.init(Cipher.DECRYPT_MODE, privateKey);
String cipherText = encryptedText.getText().toString();
CipherInputStream cipherInputStream = new CipherInputStream(
new ByteArrayInputStream(Base64.decode(cipherText, Base64.DEFAULT)), output);
ArrayList<Byte> values = new ArrayList<>();
int nextByte;
while ((nextByte = cipherInputStream.read()) != -1) {
values.add((byte)nextByte);
}
byte[] bytes = new byte[values.size()];
for(int i = 0; i < bytes.length; i++) {
bytes[i] = values.get(i).byteValue();
}
String finalText = new String(bytes, 0, bytes.length, "UTF-8");
decryptedText.setText(finalText);
} catch (Exception e) {
Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}
你想知道更多嗎?訂閱我們的安卓開發(fā)者簡報。僅在下方輸入你的電子郵件地址,就可以每周一次在你的收件箱里獲取所有頂級的開發(fā)者信息、提示和鏈接。
PS. 永遠不會有垃圾郵件。你的郵箱地址只用于安卓開發(fā)周報。
安卓密鑰庫使創(chuàng)建和管理應用程序密鑰變得輕而易舉,并為應用程序提供了一個相對安全的庫來存儲加密密鑰。當然公鑰也可以發(fā)送到你的服務器上,服務器的公鑰可以發(fā)送到你的應用程序上,來確保應用程序和服務器之前的安全通信。按往常來說,完整的源代碼在 github 上可供你使用。想要補充、改正或討論,請在下方留下評論,我們非常期待聽到你的聲音。
更多IT技術干貨: wiki.jikexueyuan.com
加入極客星球翻譯團隊: http://wiki.jikexueyuan.com/project/wiki-editors-guidelines/translators.html版權聲明:
本譯文僅用于學習和交流目的。非商業(yè)轉載請注明譯者、出處,并保留文章在極客學院的完整鏈接
商業(yè)合作請聯(lián)系 wiki@jikexueyuan.com
原文地址:http://www.androidauthority.com/use-android-keystore-store-passwords-sensitive-information-623779/