有沒有想過如何使用資料加密來保護您的私人使用者資料免遭黑客攻擊?不要再看了,在本教程中你會做到這一點!
由於最近的所有資料洩露和新的隱私法律(例如GDPR),您的應用程式的可信度取決於您管理使用者資料的方式。有強大的Android API專注於資料加密,在開始專案時有時會被忽略。您可以充分利用它們並從頭開始考慮安全性。
在本教程中,您將獲得儲存醫療資訊的獸醫診所的應用程式。在此過程中,您將學習如何:
收緊應用許可權
加密您的資料
使用KeyStore
入門
如要下載入門專案請看文末。花點時間熟悉專案的結構。構建並執行應用程式以檢視您正在使用的內容。
你會看到一個簡單的註冊螢幕。輸入密碼並選擇
如果在Android 7+上,你會遇到錯誤
保護基礎
要開始加密應用程式並保護重要資料,首先必須防止資料洩漏到
使用許可權
當您第一次開始構建應用程式時,重要的是要考慮您實際需要保留多少使用者資料。如今,最好的做法是避免儲存私人資料(如果您不需要) - 特別是對於我們可愛的小Lightning,他擔心自己的隱私。
從Android 6.0開始,檔案和SharedPreferences
儲存都是使用MODE_PRIVATE
常量設定的。這意味著只有您的應用才能訪問資料。Android 7不允許任何其他選項。首先,首先要確保安全地設定專案。
開啟
MODE_WORLD_READABLE
和MODE_WORLD_WRITABLE
。這些允許在早期Android版本上公開訪問您的檔案。找到設定MODE_WORLD_WRITABLE
並用以下內容替換它的行:val preferences = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE)
複製程式碼
然後,找到設定的行MODE_WORD_READABLE
並將其替換為:
val editor = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE).edit()
複製程式碼
太棒了,你剛剛讓你的喜好更安全一點!此外,如果您現在構建並執行該應用程式,則由於安全性違反Android 7+版本,您不應該遇到之前遇到的崩潰。您現在應該為應用安裝目錄強制實施安全位置。
限制安裝目錄
Android在過去幾年中面臨的一個重大問題是沒有足夠的記憶體來安裝大量應用程式。這主要是由於裝置的儲存容量較低,但由於技術已經發展,手機變得更便宜,現在大多數裝置都為大量應用程式提供了大量儲存空間。但是,為了減少儲存空間不足,Android允許您將應用程式安裝到
為此,請開啟
android:installLocation="auto"
並將其替換為以下內容:android:installLocation = “internalOnly”
複製程式碼
現在,安裝位置僅限於裝置,但您仍然可以備份應用及其資料。這意味著使用者可以使用
android:allowBackup="true"
並替換值的行"false"
。遵循這些最佳實踐,您已經在某種程度上強化了您的應用程式。但是,您可以在有根裝置上繞過這些許可權措施。解決方案是使用潛在攻擊者無法找到的一條資訊對資料進行加密。
使用密碼保護使用者資料
建立金鑰
如上所述,AES使用金鑰進行加密。該金鑰也用於解密資料。這稱為
。金鑰可以是不同的長度,但256位是標準的。直接使用使用者的密碼進行加密是很危險的。它可能不會隨機或足夠大。因此,使用者密碼與加密金鑰不同。一個名為
由於每個金鑰都是唯一的,如果攻擊者竊取並線上釋出金鑰,則不會公開使用相同密碼的所有使用者。
首先生成鹽。開啟
encrypt
方法,其中包含//TODO: Add code here
:val random = SecureRandom()
val salt = ByteArray(256)
random.nextBytes(鹽)
複製程式碼
在這裡,您使用SecureRandom
類,這確保輸出難以預測。這被稱為
現在,您將使用使用者的密碼和salt生成金鑰。在剛剛新增的程式碼下新增以下內容:
VAL pbKeySpec = PBEKeySpec(密碼,鹽,1324,256)// 1
VAL secretKeyFactory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1” )// 2
VAL keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded // 3
VAL keySpec = SecretKeySpec(keyBytes ,“AES”)// 4
複製程式碼
這是程式碼內部的內容。您:
- 將salt和密碼放入
PBEKeySpec
基於密碼的加密物件中。建構函式採用迭代計數(1324)。數字越大,在暴力攻擊期間操作一組鍵所需的時間越長。 - 通過
PBEKeySpec
進入SecretKeyFactory
。 - 生成金鑰為
ByteArray
。 - 將原始包裝
ByteArray
成一個SecretKeySpec
物體。
CharArray
而不是String
物件。這是因為物件String
是不可變的。A CharArray
可以被覆蓋,允許您在完成後從記憶體中刪除敏感資訊。新增初始化向量
您幾乎已準備好加密資料,但還有一件事要做。
AES工作在不同的模式。標準模式稱為
如果您對與另一條訊息相同的訊息進行加密,則第一個加密塊將是相同的!這為攻擊者提供了線索。要解決此問題,您將使用
對於與第一個塊進行異或的隨機資料塊,IV是一個奇特的術語。請記住,每個塊都依賴於此前處理的所有塊。這意味著使用相同金鑰加密的相同資料集將不會產生相同的輸出。
現在通過在剛剛新增的程式碼之後新增以下程式碼來建立IV:
val ivRandom = SecureRandom()//不快取以前的種子SecureRandom例項
// 1
val iv = ByteArray(16)
ivRandom.nextBytes(ⅳ)
val ivSpec = IvParameterSpec(iv)// 2
複製程式碼
在這裡,您:
- 建立了16個位元組的隨機資料。
- 將其打包到IvParameterSpec物件中。
加密資料
現在您已擁有所有必需的部分,請新增以下程式碼以執行加密:
val cipher = Cipher.getInstance(“AES / CBC / PKCS7Padding”)// 1
密碼。init(Cipher.ENCRYPT_MODE,keySpec,ivSpec)
val encrypted = cipher.doFinal(dataToEncrypt)// 2
複製程式碼
這裡:
- 您傳入了規範字串“AES / CBC / PKCS7Padding”。它使用密碼塊連結模式選擇AES。
PKCS7Padding
是一個眾所周知的填充標準。由於您正在使用塊,並非所有資料都完全適合塊大小,因此您需要填充剩餘空間。順便說一句,塊長128位,AES在加密前新增填充。 doFinal
實際加密。
接下來,新增以下內容:
map [ “salt” ] =鹽
map [ “iv” ] = iv
map [ “encrypted” ] =加密
複製程式碼
您將加密資料打包成一個HashMap
。您還將salt和初始化向量新增到地圖中。那是因為所有這些部分都是解密資料所必需的。
如果您正確地執行了這些步驟,則不應該有任何錯誤,並且該encrypt
函式已準備好保護某些資料!儲存鹽和IV是可以的,但重複使用或順序遞增它們會削弱安全性。但你永遠不應該儲存鑰匙!就在現在,你構建了加密資料的方法,但是為了稍後閱讀它,你仍然需要解密它。讓我們看看如何做到這一點。
解密資料
現在,您已經獲得了一些加密資料。為了解密,你必須改變的模式Cipher
在init
從方法ENCRYPT_MODE
到DECRYPT_MODE
。將以下內容新增到
decrypt
方法,該行右側的行如下所示://TODO: Add code here
// 1
val salt = map [ “salt” ]
val iv = map [ “iv” ]
val encrypted = map [ “encrypted” ]
// 2
//再生從密碼金鑰
VAL pbKeySpec = PBEKeySpec(密碼,鹽,1324,256)
VAL secretKeyFactory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1” )
VAL keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
VAL keySpec = SecretKeySpec(keyBytes ,“AES”)
// 3
// Decrypt
val cipher = Cipher.getInstance(“AES / CBC / PKCS7Padding”)
val ivSpec = IvParameterSpec(iv)
密碼。init(Cipher.DECRYPT_MODE,keySpec,ivSpec)
decrypted = cipher.doFinal(加密)
複製程式碼
在此程式碼中,您執行了以下操作:
- 使用包含解密所需的加密資料,salt和IV 的HashMap。
- 根據資訊加上使用者密碼重新生成金鑰。
- 解密資料並將其作為ByteArray返回。
請注意您是如何使用相同的配置進行解密的,但已追溯到您的步驟。這是因為您使用的是對稱加密演算法。您現在可以加密資料並解密它!
哦,我提到永遠不會儲存金鑰嗎?:]
儲存加密資料
現在加密過程已完成,您需要儲存該資料。該應用程式已在讀取和寫入儲存資料。您將更新這些方法以使其與加密資料一起使用。
在
createDataSource
方法替換方法內的所有內容:val inputStream = applicationContext.assets。open(filename)
val bytes = inputStream.readBytes()
inputStream.close()
val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)
val map = Encryption()。encrypt(bytes,password)
ObjectOutputStream(FileOutputStream(outFile))。use {
它 - > it.writeObject(map)
}
複製程式碼
在更新的程式碼中,您將資料檔案作為輸入流開啟,並將資料提供給加密方法。您HashMap
使用ObjectOutputStream
該類序列化,然後將其儲存到儲存中。
構建並執行應用程式。請注意,列表中現在缺少寵物:
那是因為儲存的資料是加密的。您需要更新程式碼才能讀取加密內容。在
loadPets
方法中,刪除和註釋標記。然後,將以下程式碼新增到其讀取的位置:/*``*/``//TODO: Add decrypt call here
decrypted = Encryption()。decrypt(
hashMapOf(“iv”到iv,“salt”到salt,“加密”到加密),密碼)
複製程式碼
您decrypt
使用加密資料IV和salt 呼叫了該方法。現在輸入流來自一個ByteArray
而不是File
,替換val inputStream = file.inputStream()
用這個讀取的行:
val inputStream = ByteArrayInputStream(解密)
複製程式碼
如果你現在構建並執行應用程式,你應該看到幾個友好的面孔!
資料現在是安全的,但是使用者資料儲存在Android上的另一個常見位置是
保護SharedPreferences
該應用程式還會跟蹤上次訪問時間SharedPreferences
,因此它是應用程式中的另一個要保護的位置。在敏感資訊中儲存SharedPreferences
可能是不安全的,因為即使使用Context.MODE_PRIVATE
標記,您仍然可以從應用程式中洩漏資訊。你會稍微解決這個問題。
開啟
saveLastLoggedInTime
用以下程式碼替換該方法://獲取密碼
val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)
// Base64資料
val currentDateTimeString = DateFormat.getDateTimeInstance()。format(Date())
// 1
val map =
加密()。encrypt(currentDateTimeString.toByteArray(Charsets.UTF_8),密碼)
// 2
val valueBase64String = Base64.encodeToString(map [ “encrypted” ],Base64.NO_WRAP)
val saltBase64String = Base64.encodeToString(map [ “salt” ],Base64.NO_WRAP)
val ivBase64String = Base64.encodeToString(map [ “iv “ ],Base64.NO_WRAP)
//儲存到共享的首選項
val editor = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE).edit()
// 3
editor.putString(“l”,valueBase64String)
editor.putString(“lsalt”,saltBase64String)
editor.putString(“liv”,ivBase64String)
editor.apply()
複製程式碼
在這裡,您:
- 使用UTF-8編碼轉換
String
為a 並加密它。在前面的程式碼中,您將檔案開啟為二進位制檔案,但在使用字串時,您需要考慮字元編碼。ByteArray
- 將原始資料轉換為
String
表示形式。SharedPreferences
不能ByteArray
直接儲存,但它可以使用String
。Base64是將原始資料轉換為字串表示的標準。 - 將字串儲存到
SharedPreferences
。您可以選擇加密首選項鍵和值。這樣,攻擊者無法通過檢視金鑰來弄清楚值是什麼,並且使用“密碼”之類的金鑰不適用於暴力破解,因為它也會被加密。
現在,替換該lastLoggedIn
方法以獲取加密的位元組:
//獲取密碼
val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)
//檢索共享首選項資料
// 1個
VAL偏好= getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE)
VAL base64Encrypted = preferences.getString(“L” , “” )
VAL base64Salt = preferences.getString(“lsalt” , “” )
val base64Iv = preferences.getString(“liv”,“”)
// Base64 decode
// 2
val encrypted = Base64.decode(base64Encrypted,Base64.NO_WRAP)
val iv = Base64.decode(base64Iv,Base64.NO_WRAP)
val salt = Base64.decode(base64Salt,Base64.NO_WRAP)
// Decrypt
// 3
val decrypted = Encryption()。decrypt(
hashMapOf(“iv”到iv,“salt”到salt,“加密”到加密),密碼)
var lastLoggedIn:String?= null
解密?.let {
lastLoggedIn = String(它,Charsets.UTF_8)
}
返回 lastLoggedIn
複製程式碼
你做了以下事情:
- 檢索加密資料,IV和salt的字串表示。
- 對字串應用Base64解碼,將它們轉換回原始位元組。
- 將該資料傳遞
HashMap
給decrypt
方法。
現在你已經儲存設定安全,開始通過導航到新的
構建並執行應用程式。如果一切正常,登入後您應該再次看到寵物回到螢幕上。Esther很高興她的私人資料已加密。:]
使用伺服器的金鑰
您剛剛完成了一個很棒的真實示例,但很多應用都需要良好的入職體驗。除登入螢幕外,顯示密碼螢幕可能不是一個好的使用者體驗。對於這樣的要求,您有一些選擇。
第一種是利用登入密碼來獲得金鑰。您也可以讓伺服器生成該金鑰。金鑰將是唯一的,並在使用者使用其憑據進行身份驗證後安全地傳輸。
如果您要使用伺服器路由,重要的是要知道由於伺服器生成金鑰,因此它具有解密儲存在裝置上的資料的能力。有人可能洩漏金鑰。
如果這些解決方案都不適合您,您可以利用裝置安全性來保護應用程式。
使用KeyStore
Android M引入了使用
生成新的隨機金鑰
在
keystoreTest
方法以生成隨機金鑰。這一次,KeyStore保護金鑰:val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,“AndroidKeyStore”)// 1
val keyGenParameterSpec = KeyGenParameterSpec.Builder(“MyKeyAlias”,
KeyProperties.PURPOSE_ENCRYPT或KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
//.setUserAuthenticationRequired(true)// 2需要鎖定螢幕,如果鎖定螢幕被禁用
則無效//.setUserAuthenticationValidityDurationSeconds(120)// 3僅在密碼驗證時可用x秒。-1需要指紋 - 每次
.setRandomizedEncryptionRequired(true)//每次呼叫時相同明文的4個不同密文
。建立()
的KeyGenerator。init(keyGenParameterSpec)
keyGenerator.generateKey()
複製程式碼
這是程式碼內部發生的事情:
- 您建立了一個
KeyGenerator
例項並將其設定為“AndroidKeyStore”提供程式。 - 或者,您新增了
.setUserAuthenticationRequired(true)
需要設定鎖定螢幕的功能。 - 您可以選擇新增,
.setUserAuthenticationValidityDurationSeconds(120)
以便在裝置身份驗證後120秒可以使用該金鑰。 - 你打過電話
.setRandomizedEncryptionRequired(true)
。這告訴KeyStore每次都使用新的IV。如前所述,這意味著如果您再次加密相同的資料,加密的輸出將不相同。它可以防止攻擊者根據相同的輸入資訊獲取有關加密資料的線索。
您應該瞭解的KeyStore選項還有一些其他內容:
- 對於
.setUserAuthenticationValidityDurationSeconds()
,您可以在每次要訪問金鑰時傳遞-1以要求指紋身份驗證。 - 一旦使用者移除或更改鎖定螢幕引腳或密碼,啟用螢幕鎖定要求將撤消金鑰。
- 將金鑰儲存在與加密資料相同的位置就像在門墊下放一把鑰匙。KeyStore嘗試使用嚴格的許可權和核心級程式碼來保護金鑰。在某些裝置上,金鑰是硬體支援的。
- 你可以用
.setUserAuthenticationValidWhileOnBody(boolean remainsValid)
。這使得一旦裝置檢測到金鑰不再在該人身上,該金鑰就不可用。
加密資料
現在,您將使用儲存在KeyStore中的金鑰。在
keystoreEncrypt
方法中//TODO: Add code here
:// 1
//獲取金鑰
val keyStore = KeyStore.getInstance(“AndroidKeyStore”)
keyStore.load(null)
val secretKeyEntry =
keyStore.getEntry(“MyKeyAlias”,null)as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey
// 2
//加密資料
val cipher = Cipher.getInstance(“AES / GCM / NoPadding”)
密碼。init(Cipher.ENCRYPT_MODE,secretKey)
val ivBytes = cipher.iv
val encryptedBytes = cipher.doFinal(dataToEncrypt)
// 3
map [ “iv” ] = ivBytes
map [ “encrypted” ] = encryptedBytes
複製程式碼
這裡:
- 這次,您從KeyStore中檢索金鑰。
- 您使用該
Cipher
物件加密了資料SecretKey
。 - 像以前一樣,您返回一個
HashMap
包含解密資料所需的加密資料和IV。
解密為位元組陣列
將以下內容新增到keystoreDecrypt
方法中,右下方//TODO: Add code here
:
// 1
//獲取金鑰
val keyStore = KeyStore.getInstance(“AndroidKeyStore”)
keyStore.load(null)
val secretKeyEntry =
keyStore.getEntry(“MyKeyAlias”,null)as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey
// 2
//從地圖中提取資訊
val encryptedBytes = map [ “encrypted” ]
val ivBytes = map [ “iv” ]
// 3
//解密資料
val cipher = Cipher.getInstance(“AES / GCM / NoPadding”)
val spec = GCMParameterSpec(128,ivBytes)
密碼。init(Cipher.DECRYPT_MODE,secretKey,spec)
decrypted = cipher.doFinal(encryptedBytes)
複製程式碼
在此程式碼中,您:
- 從KeyStore再次獲得金鑰。
- 從中提取必要的資訊
map
。 Cipher
使用DECRYPT_MODE
常量初始化物件並將資料解密為aByteArray
。
測試示例
現在您已經建立了使用KeyStore API加密和解密資料的方法,現在是時候測試它們了。將以下內容新增到keystoreTest
方法的末尾:
// 1
val map = keystoreEncrypt(“我非常敏感的字串!”。 toByteArray(Charsets.UTF_8))// 2
val decryptedBytes
= keystoreDecrypt(map)
decryptedBytes?.let {
val decryptedString = String(it,Charsets.UTF_8)
Log.e(“MyApp”,“解密的字串是:$ decryptedString ”)
}
複製程式碼
在更新的程式碼中,您:
- 建立了一個測試
string
並對其進行了加密。 - 呼叫
decrypt
加密輸出上的方法來測試一切是否正常。
在
onCreate
方法中,取消註釋讀取的行。構建並執行應用程式以檢查它是否有效。您應該看到解密的字串://Encryption().keystoreTest()
然後去哪兒?
恭喜,您已經學會了在Android上加密和解密資料的方法!
您還學習了使用Keystore處理金鑰的其他方法。您可以使用本教程頂部或底部的“
很高興知道如何正確實現安全性。有了這些知識,您將能夠確認第三方安全庫是否符合最佳實踐。然而,自己實施它,特別是如果匆忙,可能會導致錯誤。如果您在該船上,請考慮使用經過行業認可或經過時間考驗的第三方。
是第三方加密庫的絕佳選擇。它可以讓您啟動並執行,而無需擔心底層細節。一個缺點 - 當黑客暴露流行的圖書館中的漏洞時。這會影響同時依賴該第三方的所有應用程式。具有自定義實現的應用程式通常不受廣泛的指令碼攻擊的影響。是Android作業系統的一部分,並擁有相應的API。它是使用者帳戶憑據的集中管理器,因此您的應用程式不必直接儲存或使用密碼和登入。最著名的例子是請求* OAuth2令牌*。在Android 4.0(API Level 14)中引入,處理金鑰管理。它專門用於PrivateKey
和X509Certificate
物件,並提供比使用應用程式的資料儲存更安全的容器。您可以使用它來安裝證照並直接使用私鑰物件。只要有人不篡改您的應用程式,您的安全程式碼就可以很好地保護您的應用程式。檢視
教程,瞭解如何幫助防止逆向工程或篡改與安全相關的程式碼。現在您已經保護了資料,為什麼不瞭解如何保護傳輸中的資料?對於那些尋找高階加密技術的人,請檢視AES的GCM模式。
順便說一句,樣本資料字元Esther,Cornelius,Lightning和Birgit都是真實的!:]