├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── securevale │ │ └── androidcryptosamples │ │ ├── assymectric │ │ └── rsa │ │ │ └── RsaTest.kt │ │ ├── derivation │ │ └── KeyDerivationTest.kt │ │ ├── hash │ │ └── MessageDigestTest.kt │ │ ├── mac │ │ └── HmacTest.kt │ │ ├── signature │ │ └── SignatureTest.kt │ │ ├── symmetric │ │ └── aes │ │ │ ├── cbc │ │ │ └── AesCbcTest.kt │ │ │ └── gcm │ │ │ └── AesGcmTest.kt │ │ └── testhelpers │ │ └── TestHelpers.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── securevale │ │ └── androidcryptosamples │ │ ├── advanced │ │ └── biometric │ │ │ └── Biometric.kt │ │ ├── derivation │ │ └── KeyDerivation.kt │ │ ├── encryption │ │ ├── assymetric │ │ │ └── rsa │ │ │ │ └── Rsa.kt │ │ └── symmetric │ │ │ └── aes │ │ │ ├── cbc │ │ │ └── AesCbc.kt │ │ │ ├── ecb │ │ │ └── AesEcb.kt │ │ │ └── gcm │ │ │ └── AesGcm.kt │ │ ├── hash │ │ └── MessageDigest.kt │ │ ├── helpers │ │ └── Security.kt │ │ ├── mac │ │ └── Hmac.kt │ │ ├── signature │ │ └── Signature.kt │ │ └── ui │ │ ├── AesCbcSampleFragment.kt │ │ ├── AesGcmSampleFragment.kt │ │ ├── BiometricSampleFragment.kt │ │ ├── HmacSampleFragment.kt │ │ ├── KeyDerivationSampleFragment.kt │ │ ├── MainActivity.kt │ │ ├── MessageDigestSampleFragment.kt │ │ ├── RsaSampleFragment.kt │ │ ├── SignatureSampleFragment.kt │ │ ├── adapter │ │ └── SamplesAdapter.kt │ │ ├── dto │ │ └── OperationResult.kt │ │ └── lifecycle │ │ └── ClearOnDestroyObserver.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── sample_button.xml │ └── sample_fragment.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Secure Vale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Cryptography Samples 2 | 3 | This repo is a collection of code samples that supplement 4 | the [official documentation](https://developer.android.com/guide/topics/security/cryptography), 5 | explaining how to correctly implement common cryptographic operations on Android. 6 | 7 | ### Symmetric encryption 8 | 9 | - [AES-CBC](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/cbc/AesCbc.kt) 10 | - [AES-ECB](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/ecb/AesEcb.kt) 11 | - [AES-GCM](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/gcm/AesGcm.kt) 12 | 13 | ### Asymmetric encryption 14 | 15 | - [RSA](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/encryption/assymetric/rsa/Rsa.kt) 16 | 17 | ### Message digest 18 | 19 | - [Message Digest](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/hash/MessageDigest.kt) 20 | 21 | ### Signature 22 | 23 | - [Signing/Verifying](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/signature/Signature.kt) 24 | 25 | ### MAC 26 | 27 | - [HMAC](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/mac/Hmac.kt) 28 | 29 | ### Advanced use cases 30 | 31 | - [Key-derivation](https://github.com/securevale/android-cryptography-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/derivation/KeyDerivation.kt) 32 | - [Biometric-bound encryption](https://github.com/securevale/android-crypto-samples/blob/master/app/src/main/java/com/securevale/androidcryptosamples/advanced/biometric/Biometric.kt) 33 | 34 | > [!NOTE] 35 | > This is still WIP. The collection of code samples will be continuously updated with other 36 | > cryptographic algorithms and more advanced use cases. 37 | 38 | ## FAQ 39 | 40 | - [What key size should I use for AES?](https://crypto.stackexchange.com/questions/5118/is-aes-256-weaker-than-192-and-128-bit-versions?rq=1) 41 | - [What key size should I use for RSA?](https://stackoverflow.com/questions/589834/what-rsa-key-length-should-i-use-for-my-ssl-certificates/589850#589850) 42 | - [What is AAD in AES-GCM and what is it used for?](https://crypto.stackexchange.com/questions/89303/what-is-auth-data-in-aes-gcm/89306#89306) 43 | - [What padding should be used for AES-CBC?](https://crypto.stackexchange.com/a/48631/107088) 44 | - [Why should I use Base64 for encoding?](https://stackoverflow.com/questions/3538021/why-do-we-use-base64) 45 | - [What are the reasons to consider utilizing biometrics paired with a CryptoObject?](https://medium.com/androiddevelopers/using-biometricprompt-with-cryptoobject-how-and-why-aace500ccdb7) 46 | - [Why should I avoid using 'pure' cryptographic hash functions for password hashing?](https://security.stackexchange.com/questions/195563/why-is-sha-256-not-good-for-passwords) 47 | - [What is the main difference between Keystore and Keychain?](https://developer.android.com/privacy-and-security/keystore#WhichShouldIUse) 48 | - [What is the purpose of salt in Key Derivation?](https://crypto.stackexchange.com/questions/62807/why-do-some-key-derivation-functions-like-pbkdf2-use-a-salt) 49 | - [Is HMAC-SHA1 still considered secure?](https://crypto.stackexchange.com/questions/26510/why-is-hmac-sha1-still-considered-secure) 50 | 51 | ## Glossary 52 | 53 | - [AES-CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)) 54 | - [AES-ECB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB)) 55 | - [AES-GCM](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)) 56 | - [Android Keystore](https://developer.android.com/training/articles/keystore) 57 | - [Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) 58 | - [Biometric Security Measure Methodology](https://source.android.com/docs/security/features/biometric/measure) 59 | - [Block Cipher Mode Of Operation](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation) 60 | - [Crypto Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) 61 | - [Cryptographic Hash Function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) 62 | - [Digital Signature](https://en.wikipedia.org/wiki/Digital_signature) 63 | - [HMAC](https://en.wikipedia.org/wiki/HMAC) 64 | - [Initialization Vector](https://en.wikipedia.org/wiki/Initialization_vector) 65 | - [Key derivation](https://en.wikipedia.org/wiki/Key_derivation_function) 66 | - [Message Digest](https://csrc.nist.gov/glossary/term/message_digest) 67 | - [Message Authentication Code](https://en.wikipedia.org/wiki/Message_authentication_code) 68 | - [Padding](https://en.wikipedia.org/wiki/Padding_(cryptography)) 69 | - [Public-Key Cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) 70 | - [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) 71 | - [PRNG](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) 72 | - [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) 73 | - [Salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) 74 | - [Semantic Security](https://en.wikipedia.org/wiki/Semantic_security) 75 | - [Symmetric Cryptography](https://en.wikipedia.org/wiki/Symmetric-key_algorithm) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.securevale.androidcryptosamples' 8 | compileSdk 34 9 | 10 | defaultConfig { 11 | applicationId "com.securevale.androidcryptosamples" 12 | minSdk 23 13 | targetSdk 34 14 | versionCode 2 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_17 31 | targetCompatibility JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = '17' 35 | } 36 | buildFeatures { 37 | viewBinding true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.3.2' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.13.1' 52 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22') 53 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' 54 | implementation 'androidx.activity:activity-ktx:1.9.1' 55 | implementation 'androidx.fragment:fragment-ktx:1.8.2' 56 | implementation 'androidx.security:security-crypto:1.0.0' 57 | implementation 'androidx.appcompat:appcompat:1.7.0' 58 | implementation "androidx.biometric:biometric:1.1.0" 59 | implementation 'com.google.android.material:material:1.12.0' 60 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 61 | implementation 'androidx.core:core-ktx:1.13.1' 62 | 63 | androidTestImplementation 'org.mockito:mockito-android:5.10.0' 64 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 65 | androidTestImplementation 'androidx.test:runner:1.6.1' 66 | } 67 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/assymectric/rsa/RsaTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.assymectric.rsa 2 | 3 | import android.security.keystore.KeyPermanentlyInvalidatedException 4 | import com.securevale.androidcryptosamples.encryption.assymetric.rsa.Rsa 5 | import com.securevale.androidcryptosamples.testhelpers.clearTheKeystore 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Assert.assertThrows 8 | import org.junit.Test 9 | 10 | class RsaTest { 11 | 12 | @Test 13 | fun rsaEncryptsAndDecryptsCorrectly() { 14 | val message = "mymessage" 15 | 16 | val encrypted = Rsa.encrypt(message) 17 | 18 | val decrypted = Rsa.decrypt(encrypted) 19 | 20 | assertEquals(message, decrypted) 21 | } 22 | 23 | @Test 24 | fun whenKeyIsInvalidatedDecryptsThrowsAnError() { 25 | val message = "mymessage" 26 | 27 | val encrypted = Rsa.encrypt(message) 28 | 29 | clearTheKeystore() 30 | 31 | assertThrows(KeyPermanentlyInvalidatedException::class.java) { 32 | Rsa.decrypt(encrypted) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/derivation/KeyDerivationTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.derivation 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertThrows 5 | import org.junit.Test 6 | import java.nio.charset.Charset 7 | import javax.crypto.BadPaddingException 8 | import javax.crypto.Cipher 9 | import javax.crypto.spec.IvParameterSpec 10 | 11 | class KeyDerivationTest { 12 | 13 | private val testMessage = "message" 14 | 15 | @Test 16 | fun whenKeyDerivedFromTheSamePasswordDecryptsCorrectly() { 17 | val derivedKey = KeyDerivation.deriveKey("password", "some_salt".toByteArray()) 18 | 19 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 20 | init(Cipher.ENCRYPT_MODE, derivedKey) 21 | } 22 | 23 | val encrypted = cipher.doFinal(testMessage.toByteArray(Charset.defaultCharset())) 24 | 25 | val iv = cipher.iv 26 | 27 | val secondDerivedKey = KeyDerivation.deriveKey("password", "some_salt".toByteArray()) 28 | 29 | val ivParameterSpec = IvParameterSpec(iv) 30 | 31 | val decryptionCipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 32 | init(Cipher.DECRYPT_MODE, secondDerivedKey, ivParameterSpec) 33 | } 34 | 35 | val decrypted = decryptionCipher.doFinal(encrypted) 36 | assertEquals(testMessage, String(decrypted)) 37 | } 38 | 39 | @Test 40 | fun whenKeyDerivedFromDifferentPasswordsDecryptionFails() { 41 | val derivedKey = KeyDerivation.deriveKey("password", "some_salt".toByteArray()) 42 | 43 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 44 | init(Cipher.ENCRYPT_MODE, derivedKey) 45 | } 46 | 47 | val encrypted = cipher.doFinal(testMessage.toByteArray(Charset.defaultCharset())) 48 | 49 | val iv = cipher.iv 50 | 51 | val secondDerivedKey = 52 | KeyDerivation.deriveKey("different_password", "some_salt".toByteArray()) 53 | 54 | val ivParameterSpec = IvParameterSpec(iv) 55 | 56 | val decryptionCipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 57 | init(Cipher.DECRYPT_MODE, secondDerivedKey, ivParameterSpec) 58 | } 59 | 60 | assertThrows(BadPaddingException::class.java) { 61 | decryptionCipher.doFinal(encrypted) 62 | } 63 | } 64 | 65 | @Test 66 | fun whenKeyDerivedFromTheSamePasswordButDifferentSaltDecryptionFails() { 67 | val derivedKey = KeyDerivation.deriveKey("password", "some_salt".toByteArray()) 68 | 69 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 70 | init(Cipher.ENCRYPT_MODE, derivedKey) 71 | } 72 | 73 | val encrypted = cipher.doFinal(testMessage.toByteArray(Charset.defaultCharset())) 74 | 75 | val iv = cipher.iv 76 | 77 | val secondDerivedKey = 78 | KeyDerivation.deriveKey("password", "different_salt".toByteArray()) 79 | 80 | val ivParameterSpec = IvParameterSpec(iv) 81 | 82 | val decryptionCipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 83 | init(Cipher.DECRYPT_MODE, secondDerivedKey, ivParameterSpec) 84 | } 85 | 86 | assertThrows(BadPaddingException::class.java) { 87 | decryptionCipher.doFinal(encrypted) 88 | } 89 | } 90 | 91 | 92 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/hash/MessageDigestTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.hash 2 | 3 | import org.junit.Assert.assertArrayEquals 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Assert.assertFalse 6 | import org.junit.Assert.assertNotEquals 7 | import org.junit.Test 8 | 9 | class MessageDigestTest { 10 | 11 | @Test 12 | fun digestWhenCalculatedTwiceFromTheSameMessageIsTheSame() { 13 | val message = "message" 14 | 15 | val digest1 = MessageDigest.calculateDigest(message) 16 | val digest2 = MessageDigest.calculateDigest(message) 17 | 18 | assertArrayEquals(digest1, digest2) 19 | assertEquals(String(digest1), String(digest2)) 20 | } 21 | 22 | @Test 23 | fun digestWhenCalculatedTwiceFromDifferentMessagesIsNotTheSame() { 24 | val message = "message" 25 | val anotherMessage = "message1" 26 | 27 | val digest1 = MessageDigest.calculateDigest(message) 28 | val digest2 = MessageDigest.calculateDigest(anotherMessage) 29 | 30 | assertFalse(digest1.contentEquals(digest2)) 31 | assertNotEquals(String(digest1), String(digest2)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/mac/HmacTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.mac 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertNotEquals 5 | import org.junit.Test 6 | 7 | class HmacTest { 8 | 9 | @Test 10 | fun hmacWhenCalculatedTwiceFromTheSameMessageIsTheSame() { 11 | val message = "message" 12 | 13 | val digest1 = Hmac.computeHmac(message) 14 | val digest2 = Hmac.computeHmac(message) 15 | 16 | assertEquals(digest1, digest2) 17 | } 18 | 19 | @Test 20 | fun hmacWhenCalculatedTwiceFromDifferentMessagesIsNotTheSame() { 21 | val message = "message" 22 | val anotherMessage = "different message" 23 | 24 | val digest1 = Hmac.computeHmac(message) 25 | val digest2 = Hmac.computeHmac(anotherMessage) 26 | 27 | assertNotEquals(digest1, digest2) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/signature/SignatureTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.signature 2 | 3 | import junit.framework.TestCase.assertFalse 4 | import junit.framework.TestCase.assertTrue 5 | import org.junit.Test 6 | 7 | class SignatureTest { 8 | 9 | @Test 10 | fun signatureProperlyGeneratedAndVerified() { 11 | val message = "message" 12 | 13 | val signature = Signature.sign(message.toByteArray()) 14 | 15 | val verificationResult = Signature.verify(message.toByteArray(), signature) 16 | 17 | assertTrue(verificationResult) 18 | } 19 | 20 | @Test 21 | fun signatureVerificationFailsWithDifferentMessage() { 22 | val message = "message" 23 | 24 | val signature = Signature.sign(message.toByteArray()) 25 | 26 | val verificationResult = Signature.verify("new message".toByteArray(), signature) 27 | 28 | assertFalse(verificationResult) 29 | } 30 | 31 | @Test 32 | fun signatureVerificationFailsWithDifferentSignature() { 33 | val message = "message" 34 | 35 | val signature1 = Signature.sign("new message".toByteArray()) 36 | 37 | val verificationResult = Signature.verify(message.toByteArray(), signature1) 38 | 39 | assertFalse(verificationResult) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/symmetric/aes/cbc/AesCbcTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.symmetric.aes.cbc 2 | 3 | import com.securevale.androidcryptosamples.encryption.symmetric.aes.cbc.AesCbc 4 | import com.securevale.androidcryptosamples.testhelpers.clearTheKeystore 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Assert.assertThrows 7 | import org.junit.Test 8 | import javax.crypto.BadPaddingException 9 | 10 | class AesCbcTest { 11 | 12 | @Test 13 | fun aesCbcEncryptsAndDecryptsCorrectly() { 14 | val message = "mymessage" 15 | 16 | val encrypted = AesCbc.encrypt(message) 17 | 18 | val decrypted = AesCbc.decrypt(encrypted.data, encrypted.iv) 19 | 20 | assertEquals(message, decrypted) 21 | } 22 | 23 | @Test 24 | fun whenKeyIsInvalidatedDecryptsThrowsAnError() { 25 | val message = "mymessage" 26 | 27 | val encrypted = AesCbc.encrypt(message) 28 | 29 | clearTheKeystore() 30 | 31 | assertThrows(BadPaddingException::class.java) { 32 | AesCbc.decrypt(encrypted.data, encrypted.iv) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/symmetric/aes/gcm/AesGcmTest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.symmetric.aes.gcm 2 | 3 | import com.securevale.androidcryptosamples.encryption.symmetric.aes.gcm.AesGcm 4 | import com.securevale.androidcryptosamples.testhelpers.clearTheKeystore 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Assert.assertThrows 7 | import org.junit.Test 8 | import javax.crypto.BadPaddingException 9 | 10 | class AesGcmTest { 11 | 12 | @Test 13 | fun aesGcmEncryptsAndDecryptsCorrectly() { 14 | val message = "mymessage" 15 | 16 | val encrypted = AesGcm.encrypt(message) 17 | 18 | val decrypted = AesGcm.decrypt(encrypted.data, encrypted.iv) 19 | 20 | assertEquals(message, decrypted) 21 | } 22 | 23 | @Test 24 | fun whenKeyIsInvalidatedDecryptsThrowsAnError() { 25 | val message = "mymessage" 26 | 27 | val encrypted = AesGcm.encrypt(message) 28 | 29 | clearTheKeystore() 30 | 31 | assertThrows(BadPaddingException::class.java) { 32 | AesGcm.decrypt(encrypted.data, encrypted.iv) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/securevale/androidcryptosamples/testhelpers/TestHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.testhelpers 2 | 3 | import java.security.KeyStore 4 | 5 | fun clearTheKeystore() { 6 | val keystore = KeyStore.getInstance("AndroidKeyStore").apply { 7 | load(null) 8 | } 9 | 10 | val aliases = keystore.aliases() 11 | 12 | for (alias in aliases) { 13 | keystore.deleteEntry(alias) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/advanced/biometric/Biometric.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.advanced.biometric 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.security.keystore.KeyGenParameterSpec 6 | import android.security.keystore.KeyProperties 7 | import android.util.Base64 8 | import androidx.biometric.BiometricManager 9 | import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG 10 | import androidx.biometric.BiometricPrompt 11 | import androidx.core.content.ContextCompat 12 | import androidx.fragment.app.Fragment 13 | import com.securevale.androidcryptosamples.R 14 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 15 | import java.security.Key 16 | import java.security.KeyStore 17 | import javax.crypto.Cipher 18 | import javax.crypto.KeyGenerator 19 | import javax.crypto.SecretKey 20 | import javax.crypto.spec.GCMParameterSpec 21 | 22 | /** 23 | * Sample biometric-bound encryption on Android. 24 | * 25 | * While this instance employs the AES-GCM cipher, please note that BiometricPrompt.CryptoObject supports other Ciphers, Mac, and Signature objects. 26 | */ 27 | 28 | /** 29 | * Just hardcoded alias for sample purposes. 30 | */ 31 | private const val KEY_ALIAS = "biometricSample" 32 | 33 | private val store: KeyStore by lazy { 34 | // Initialise Android Keystore. 35 | KeyStore.getInstance("AndroidKeyStore").apply { 36 | load(null) 37 | } 38 | } 39 | 40 | /** 41 | * Create BiometricPrompt. 42 | */ 43 | fun createPrompt( 44 | fragment: Fragment, 45 | authenticationCallback: BiometricPrompt.AuthenticationCallback 46 | ): BiometricPrompt { 47 | val context = fragment.requireContext() 48 | 49 | val executor = ContextCompat.getMainExecutor(context) 50 | 51 | return BiometricPrompt(fragment, executor, authenticationCallback) 52 | } 53 | 54 | /** 55 | * Check whether this device supports biometric and there is any biometric's (fingerprint, face or iris) enrolled. 56 | * 57 | * For more detailed explanation why [BiometricManager.Authenticators.BIOMETRIC_STRONG] is the right choice, check the README. 58 | */ 59 | fun biometricAvailable(context: Context) = BiometricManager.from(context) 60 | .canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS 61 | 62 | // Encrypt using biometric-bound key. 63 | fun encryptWithBiometrics( 64 | fragment: Fragment, 65 | data: String, 66 | callback: BiometricCallback 67 | ) { 68 | doCryptoOperationWithBiometric(fragment, Purpose.ENCRYPTION, data, null, callback) 69 | } 70 | 71 | // Decrypt using biometric-bound key. 72 | fun decryptWithBiometrics( 73 | fragment: Fragment, 74 | encryptedData: OperationResult, 75 | callback: BiometricCallback 76 | ) { 77 | doCryptoOperationWithBiometric( 78 | fragment, 79 | Purpose.DECRYPTION, 80 | encryptedData.data, 81 | encryptedData.iv, 82 | callback 83 | ) 84 | } 85 | 86 | // Get generated key from the Keystore or generate one if not present. 87 | private val key: Key 88 | get() = (store.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey 89 | ?: generateSecretKey() 90 | 91 | // Generate key. 92 | private fun generateSecretKey(): SecretKey { 93 | /** 94 | * Set all parameters of the key that is to be generated. 95 | * This configuration mostly follows the AesGcm sample, with notable distinctions outlined below. 96 | * For further details, please refer to the AesGcm file 97 | * @see com.securevale.androidcryptosamples.encryption.symmetric.aes.gcm.AesGcm . 98 | * 99 | * keystoreAlias - the alias used to identify the key within the Keystore. 100 | * purposes - how the key will be used, for encryption and decryption in our case. 101 | */ 102 | val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( 103 | KEY_ALIAS, 104 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT 105 | ) 106 | .setKeySize(256) 107 | .setBlockModes(KeyProperties.BLOCK_MODE_GCM) 108 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) 109 | /** 110 | * Set whether the key can be used only if the user has been authenticated. 111 | * 112 | * The key generation occurs exclusively when a secure lock screen is established. 113 | * When coupled with [KeyGenParameterSpec.Builder.setUserAuthenticationParameters] or [KeyGenParameterSpec.Builder.setUserAuthenticationValidityDurationSeconds], 114 | * at least one biometric must be registered. 115 | * 116 | * Furthermore, it is important to note that if the secure lock screen is deactivated post 117 | * key generation, the generated key will be irreversibly invalidated. 118 | * Any cryptographic operations attempted with such a key will result in KeyPermanentlyInvalidatedException. 119 | * These limitations applies only to secret and private key operations, public key ones are not restricted. 120 | */ 121 | .setUserAuthenticationRequired(true) 122 | .apply { 123 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 124 | /** 125 | * Set whether this key should be invalidated on biometric enrollment 126 | * 127 | * This applies only to keys that requires authentication, established through [KeyGenParameterSpec.Builder.setUserAuthenticationRequired]. 128 | * Enabling this option results in irreversibly invalidation of the key if new biometric data is enrolled or if all existing biometric data is deleted. 129 | */ 130 | setInvalidatedByBiometricEnrollment(true) 131 | } 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 133 | /** 134 | * Set the duration (in seconds) and authorization type for which the key is authorized after user is successfully authenticated. 135 | * 136 | * There are two types available: [KeyProperties.AUTH_BIOMETRIC_STRONG] and [KeyProperties.AUTH_DEVICE_CREDENTIAL] for more detailed 137 | * explanation on which type you should use check the README. 138 | * If set to 0, user authentication must take place for every use of the key. 139 | */ 140 | setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG) 141 | } else { 142 | /** 143 | * Set the duration (in seconds), which the key is authorized to be used after user is successfully authenticated. 144 | * 145 | * If set to -1, user authentication must take place for every use of the key. 146 | */ 147 | setUserAuthenticationValidityDurationSeconds(-1) 148 | } 149 | } 150 | .build() 151 | 152 | return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").run { 153 | // No need to create SecureRandom and pass it manually as the KeyGenerator creates its own instance under the hood. 154 | init(spec) 155 | // Generate key and return it. 156 | generateKey() 157 | } 158 | } 159 | 160 | /** 161 | * Instantiate a Cipher object, initializing it with the selected mode, supplying the key and GCMParameterSpec (if applicable). 162 | * Then, encapsulate it within a BiometricPrompt.CryptoObject. 163 | */ 164 | private fun getCryptoObject(mode: Purpose, iv: ByteArray?) = when (mode) { 165 | Purpose.ENCRYPTION -> BiometricPrompt.CryptoObject( 166 | Cipher.getInstance("AES/GCM/NoPadding").apply { 167 | init(Cipher.ENCRYPT_MODE, key) 168 | }) 169 | 170 | Purpose.DECRYPTION -> { 171 | val gcmParameterSpec = GCMParameterSpec(128, iv) 172 | BiometricPrompt.CryptoObject(Cipher.getInstance("AES/GCM/NoPadding").apply { 173 | init(Cipher.DECRYPT_MODE, key, gcmParameterSpec) 174 | }) 175 | } 176 | } 177 | 178 | // Perform crypto operation. 179 | private fun doCryptoOperationWithBiometric( 180 | fragment: Fragment, 181 | mode: Purpose, 182 | data: String, 183 | iv: ByteArray?, 184 | callback: BiometricCallback 185 | ) { 186 | // Prompt info configuration. 187 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 188 | .setTitle(fragment.requireContext().getString(R.string.biometrics_prompt_title)) 189 | /** 190 | * For more detailed explanation why [BiometricManager.Authenticators.BIOMETRIC_STRONG] is the right choice, check the README. 191 | */ 192 | .setAllowedAuthenticators(BIOMETRIC_STRONG) 193 | .setConfirmationRequired(true) 194 | .setNegativeButtonText("Cancel") 195 | .build() 196 | 197 | // Callback for BiometricPrompt. 198 | val innerCallback = object : BiometricPrompt.AuthenticationCallback() { 199 | 200 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 201 | super.onAuthenticationError(errorCode, errString) 202 | callback.onError(errorCode, errString) 203 | } 204 | 205 | override fun onAuthenticationFailed() { 206 | super.onAuthenticationFailed() 207 | callback.onFailed() 208 | } 209 | 210 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { 211 | super.onAuthenticationSucceeded(result) 212 | /** 213 | * Obtain biometric-bound cryptoObject (Cipher instance in our case). 214 | */ 215 | val cipher = result.cryptoObject!!.cipher!! 216 | // Encrypt or decrypt based on the selected mode. 217 | val operationResult = when (mode) { 218 | Purpose.ENCRYPTION -> OperationResult( 219 | Base64.encodeToString( 220 | cipher.doFinal(data.toByteArray()), 221 | Base64.DEFAULT 222 | ), 223 | cipher.iv 224 | ) 225 | 226 | Purpose.DECRYPTION -> OperationResult( 227 | String( 228 | cipher.doFinal( 229 | Base64.decode( 230 | data, 231 | Base64.DEFAULT 232 | ) 233 | ) 234 | ) 235 | ) 236 | } 237 | callback.onSuccess(operationResult) 238 | } 239 | } 240 | 241 | // Create BiometricPrompt supplying the cryptoObject and show it. 242 | createPrompt(fragment, innerCallback).apply { 243 | authenticate(promptInfo, getCryptoObject(mode, iv)) 244 | } 245 | } 246 | 247 | // A convenient interface for delivering results to the UI in a more organized manner. 248 | interface BiometricCallback { 249 | fun onError(errorCode: Int, errorString: CharSequence) 250 | fun onFailed() 251 | fun onSuccess(result: OperationResult) 252 | } 253 | 254 | // Whether we want to encrypt or decrypt. 255 | enum class Purpose { 256 | ENCRYPTION, DECRYPTION 257 | } 258 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/derivation/KeyDerivation.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.derivation 2 | 3 | import android.os.Build 4 | import javax.crypto.SecretKey 5 | import javax.crypto.SecretKeyFactory 6 | import javax.crypto.spec.PBEKeySpec 7 | 8 | /** 9 | * Sample Key-derivation implementation on Android. 10 | */ 11 | object KeyDerivation { 12 | 13 | /** 14 | * For salt generation please check the [com.securevale.androidcryptosamples.ui.KeyDerivationSampleFragment] 15 | */ 16 | fun deriveKey(password: String, salt: ByteArray): SecretKey { 17 | 18 | /** 19 | * DISCLAIMER: 20 | * The iterationsCount value provided below is for sample purposes only and should not be considered secure. 21 | * 22 | * While OWASP recommends 210.000 iterations for PBKDF2withHmacSHA512 and 1.3 million for 23 | * PBKDF2-HMAC-SHA (see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) 24 | * these values are likely to be too high for an Android device due to performance constraints. 25 | * 26 | * You need to choose a number of iterations that balances security and performance for your specific use case 27 | * (for guidance, see https://security.stackexchange.com/a/3993/284386). 28 | */ 29 | val iterationCount = 12_000 30 | 31 | /** 32 | * In this example, we are assume that we are generating a key for AES-CBS, which requires 256 bits. 33 | * However, please note that your case may differ, so the derived key length might need to be of different size. 34 | */ 35 | val keyLength = 256 36 | 37 | val spec = PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength) 38 | 39 | val factory = if (Build.VERSION.SDK_INT >= 26) { 40 | /** 41 | * A brief explanation of why SHA512 was used over SHA256: 42 | * https://stackoverflow.com/questions/11624372/best-practice-for-hashing-passwords-sha256-or-sha512 43 | * https://stackoverflow.com/questions/18080445/difference-between-hmacsha256-and-hmacsha512 44 | */ 45 | SecretKeyFactory.getInstance("PBKDF2withHmacSHA512") 46 | } else { 47 | /** 48 | * Using PBKDF2withHmacSHA1 is not ideal, but it is unavoidable in this scenario, 49 | * given my assumption in this repository to rely on platform APIs rather than external dependencies. 50 | * Note that support for PBKDF2withHmacSHA512 is only available starting from API level 26: 51 | * https://developer.android.com/reference/javax/crypto/SecretKeyFactory. 52 | */ 53 | SecretKeyFactory.getInstance("PBKDF2withHmacSHA1") 54 | } 55 | 56 | return factory.generateSecret(spec) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/encryption/assymetric/rsa/Rsa.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.encryption.assymetric.rsa 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import android.util.Base64 6 | import java.security.KeyPair 7 | import java.security.KeyPairGenerator 8 | import java.security.KeyStore 9 | import java.security.PrivateKey 10 | import java.util.UUID 11 | import javax.crypto.Cipher 12 | 13 | /** 14 | * Sample RSA implementation on Android. 15 | */ 16 | object Rsa { 17 | 18 | private val store: KeyStore by lazy { 19 | // Initialise Android Keystore. 20 | KeyStore.getInstance("AndroidKeyStore").apply { 21 | load(null) 22 | } 23 | } 24 | 25 | /** 26 | * Just random alias per session of the app for sample purposes. 27 | * In real use cases, you should use something permanent as an alias. 28 | */ 29 | private val alias: String = UUID.randomUUID().toString() 30 | 31 | /** 32 | * Set all parameters of the key that is to be generated. 33 | * 34 | * keystoreAlias - the alias used to identify the key within the Keystore. 35 | * purposes - how the key will be used, for signing and verification in our case. 36 | */ 37 | private val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( 38 | alias, 39 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT 40 | ) 41 | /** 42 | * Set the key size. 43 | * 44 | * The current recommended RSA key size is 2048 bits, but if you want to be more future proof consider 3072 or 4096 bits. 45 | * It is worth noting that RSA can only encrypt data of which size does not exceed the selected key size (2048 bits = 256 bytes in our case) minus 46 | * any padding and header data. 47 | * For more detailed explanation check the "FAQ" section in README. 48 | */ 49 | .setKeySize(2048) 50 | /** 51 | * Set mode of operation. 52 | * 53 | * It is not implementing the ECB mode (like in AES) as it encrypts/decrypts only single block of data. 54 | * The BLOCK_MODE_ECB is used just to mimic the cipher string for block ciphers. 55 | */ 56 | .setBlockModes(KeyProperties.BLOCK_MODE_ECB) 57 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) 58 | .build() 59 | 60 | // Get generated keypair from the Keystore or generate one if not present. 61 | private val keyPair: KeyPair 62 | get() { 63 | val privateKey = store.getKey(alias, null) as PrivateKey? 64 | val publicKey = store.getCertificate(alias)?.publicKey 65 | 66 | return if (privateKey != null && publicKey != null) { 67 | KeyPair(publicKey, privateKey) 68 | } else { 69 | generateKeyPair() 70 | } 71 | } 72 | 73 | // Get the public key. 74 | private val publicKey = keyPair.public 75 | 76 | // Get the private key. 77 | private val privateKey = keyPair.private 78 | 79 | // Generate key pair. 80 | private fun generateKeyPair(): KeyPair = 81 | KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore").run { 82 | // Initialise with spec. 83 | initialize(spec) 84 | // Generate key pair and return it. 85 | generateKeyPair() 86 | } 87 | 88 | fun encrypt(data: String): String { 89 | /** 90 | * Create Cipher instance and init it in encryption mode supplying the public key. 91 | * 92 | * DISCLAIMER: 93 | * The OAEPPadding should be chosen over the PKCS1Padding (see https://crypto.stackexchange.com/questions/47436/how-much-safer-is-rsa-oaep-compared-to-rsa-with-pkcs1-v1-5-padding), 94 | * but its use involves strange workarounds due to a bug in Android (see https://issuetracker.google.com/issues/36708951#comment15). This problem will be 95 | * addressed in the future as a separate sample, let's stick to the PKCS1Padding for now. 96 | * 97 | * Might be also RSA/NONE/PKCS1Padding as the "ECB" part is only used as a placeholder for block cipher and there is no block mode used under the hood, 98 | * thus "NONE" also indicates that there is no mode of operation used here. 99 | */ 100 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding").apply { 101 | init(Cipher.ENCRYPT_MODE, publicKey) 102 | } 103 | 104 | // Encrypt. 105 | val encrypted = cipher.doFinal(data.toByteArray()) 106 | 107 | /** 108 | * Return the Base64-encoded ciphertext. 109 | * Encoding is optional. You could just as well return byteArray if it better fits your needs. 110 | */ 111 | return Base64.encodeToString(encrypted, Base64.DEFAULT) 112 | } 113 | 114 | 115 | fun decrypt(ciphertext: String): String { 116 | /** 117 | * Create Cipher instance and init it in decryption mode supplying the private key. 118 | * 119 | * Might be also RSA/NONE/PKCS1Padding as the "ECB" part is only used as a placeholder for block cipher and there is no block mode used under the hood, 120 | * thus "NONE" also indicates that there is no mode of operation used here. 121 | */ 122 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding").apply { 123 | init(Cipher.DECRYPT_MODE, privateKey) 124 | } 125 | 126 | // Decode Base64-encoded ciphertext to raw bytes. 127 | val decoded = Base64.decode(ciphertext, Base64.DEFAULT) 128 | 129 | // Decrypt. 130 | val decrypted = cipher.doFinal(decoded) 131 | 132 | // Return the decrypted plaintext as a String. 133 | return String(decrypted) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/cbc/AesCbc.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.encryption.symmetric.aes.cbc 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import android.util.Base64 6 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 7 | import java.nio.charset.Charset 8 | import java.security.Key 9 | import java.security.KeyStore 10 | import java.util.UUID 11 | import javax.crypto.Cipher 12 | import javax.crypto.KeyGenerator 13 | import javax.crypto.SecretKey 14 | import javax.crypto.spec.IvParameterSpec 15 | 16 | /** 17 | * Sample AES-CBC implementation on Android. 18 | */ 19 | object AesCbc { 20 | 21 | private val store: KeyStore by lazy { 22 | // Initialise Android Keystore. 23 | KeyStore.getInstance("AndroidKeyStore").apply { 24 | load(null) 25 | } 26 | } 27 | 28 | /** 29 | * Just random alias per session of the app for sample purposes. 30 | * In real use cases, you should use something permanent as an alias. 31 | */ 32 | private val alias: String = UUID.randomUUID().toString() 33 | 34 | /** 35 | * Set all parameters of the key that is to be generated. 36 | * 37 | * keystoreAlias - the alias used to identify the key within the Keystore. 38 | * purposes - how the key will be used, for encryption and decryption in our case. 39 | */ 40 | private val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( 41 | alias, 42 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT 43 | ) 44 | /** 45 | * Set the key size (for AES-GCM it can be 128, 192 and 256). 46 | * 47 | * We will use 256 bits for sample purposes, which is little slower than 128 but on Android we usually don't encrypt large data sets so it's not an issue. 48 | * For more detailed explanation which size to use check the "FAQ" section in README. 49 | */ 50 | .setKeySize(256) 51 | // The block mode (CBC in our case). 52 | .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 53 | // Padding for CBC. 54 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 55 | .build() 56 | 57 | // Get generated key from the Keystore or generate one if not present. 58 | private val key: Key 59 | get() = (store.getEntry(alias, null) as? KeyStore.SecretKeyEntry)?.secretKey 60 | ?: generateSecretKey() 61 | 62 | // Generate key. 63 | private fun generateSecretKey(): SecretKey = 64 | KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").run { 65 | // No need to create SecureRandom and pass it manually as the KeyGenerator creates its own instance under the hood. 66 | init(spec) 67 | // Generate the key and return it. 68 | generateKey() 69 | } 70 | 71 | fun encrypt(data: String): OperationResult { 72 | /** 73 | * Create Cipher instance and init it in encryption mode supplying the key. 74 | * 75 | * Needs to be PKCS7Padding even if the https://developer.android.com/reference/javax/crypto/Cipher is not listing it as a supported algorithm. 76 | * It's not the first time when crypto's documentation on Android is not up-to-date. 77 | */ 78 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 79 | init(Cipher.ENCRYPT_MODE, key) 80 | } 81 | 82 | // Encrypt. 83 | val encrypted = cipher.doFinal(data.toByteArray(Charset.defaultCharset())) 84 | 85 | /** 86 | * *Optional* Encode using Base64. 87 | * 88 | * Base64 encoding is used as a standard for encoding data that is sent between different parties 89 | * in order to preserve both data format and information, and allow it to be easily decoded and not corrupted during the transfer. 90 | */ 91 | val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT) 92 | 93 | /** 94 | * Return the Base64-encoded ciphertext alongside with the Initialization Vector (IV). 95 | * IV needs to be saved alongside with the ciphertext as it will be needed for decryption. 96 | * The most common way to do it is just to append/prepend IV to the ciphertext itself. 97 | */ 98 | return OperationResult( 99 | encoded, 100 | cipher.iv 101 | ) 102 | } 103 | 104 | fun decrypt(ciphertext: String, iv: ByteArray): String { 105 | // Instantiate IvParameterSpec with the supplied IV. 106 | val ivParameterSpec = IvParameterSpec(iv) 107 | 108 | /** 109 | * Create Cipher instance and init it in decryption mode supplying the key and IvParameterSpec. 110 | * 111 | * Needs to be PKCS7Padding even if the https://developer.android.com/reference/javax/crypto/Cipher is not listing it as a supported algorithm. 112 | * It's not the first time when crypto's documentation on Android is not up-to-date. 113 | */ 114 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 115 | init(Cipher.DECRYPT_MODE, key, ivParameterSpec) 116 | } 117 | 118 | // Decode Base64-encoded ciphertext to raw bytes. 119 | val decoded = Base64.decode(ciphertext, Base64.DEFAULT) 120 | 121 | // Decrypt. 122 | val decrypted = cipher.doFinal(decoded) 123 | 124 | // Return the decrypted plaintext as a String. 125 | return String(decrypted) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/ecb/AesEcb.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.encryption.symmetric.aes.ecb 2 | 3 | object AesEcb { 4 | /** 5 | * There is no example of using AES-ECB as you SHOULD NOT USE IT. It is not semantically secure which means 6 | * that merely observing the ciphertext can leak information about the plaintext. 7 | * For more detailed explanation, check "Semantic Security" from the Glossary in the README. 8 | */ 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/encryption/symmetric/aes/gcm/AesGcm.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.encryption.symmetric.aes.gcm 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import android.util.Base64 6 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 7 | import java.nio.charset.Charset 8 | import java.security.Key 9 | import java.security.KeyStore 10 | import java.util.UUID 11 | import javax.crypto.Cipher 12 | import javax.crypto.KeyGenerator 13 | import javax.crypto.SecretKey 14 | import javax.crypto.spec.GCMParameterSpec 15 | 16 | /** 17 | * Sample AES-GCM implementation on Android. 18 | */ 19 | object AesGcm { 20 | 21 | private val store: KeyStore by lazy { 22 | // Initialise Android Keystore. 23 | KeyStore.getInstance("AndroidKeyStore").apply { 24 | load(null) 25 | } 26 | } 27 | 28 | private const val AAD: String = "additional tag" 29 | 30 | /** 31 | * Just random alias per session of the app for sample purposes. 32 | * In real use cases, you should use something permanent as an alias. 33 | */ 34 | private val alias: String = UUID.randomUUID().toString() 35 | 36 | /** 37 | * Set all parameters of the key that is to be generated. 38 | * 39 | * keystoreAlias - the alias used to identify the key within the Keystore. 40 | * purposes - how the key will be used, for encryption and decryption in our case. 41 | */ 42 | private val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( 43 | alias, 44 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT 45 | ) 46 | /** 47 | * Set the key size (for AES-GCM it can be 128, 192 and 256). 48 | * 49 | * We will use 256 bits for sample purposes, it's little slower than 128 but on Android we usually don't encrypt large data sets so it's not an issue. 50 | * For more detailed explanation which key size to use check the "FAQ" section in README. 51 | */ 52 | .setKeySize(256) 53 | // The block mode (GCM in our case). 54 | .setBlockModes(KeyProperties.BLOCK_MODE_GCM) 55 | /** 56 | * Padding configuration for GCM. 57 | * 58 | * Internally, GCM is an implementation of CTR mode alongside with a polynomial hashing function applied to the ciphertext, so it doesn't need padding. 59 | * For more detailed explanation, check the GCM "mathematical basis" section on Wikipedia. 60 | */ 61 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) 62 | .build() 63 | 64 | // Get generated key from the Keystore or generate one if not present. 65 | private val key: Key 66 | get() = (store.getEntry(alias, null) as? KeyStore.SecretKeyEntry)?.secretKey 67 | ?: generateSecretKey() 68 | 69 | // Generate key. 70 | private fun generateSecretKey(): SecretKey = 71 | KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").run { 72 | // No need to create SecureRandom and pass it manually as the KeyGenerator creates its own instance under the hood. 73 | init(spec) 74 | // Generate key and return it. 75 | generateKey() 76 | } 77 | 78 | fun encrypt(data: String): OperationResult { 79 | // Create Cipher instance and init it in encryption mode supplying the key. 80 | val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { 81 | init(Cipher.ENCRYPT_MODE, key) 82 | } 83 | 84 | /** 85 | * *Optional* Use Additional Authenticated Data (AAD). 86 | * 87 | * For more detailed explanation, check the README. 88 | */ 89 | cipher.updateAAD(AAD.toByteArray()) 90 | 91 | // Encrypt. 92 | val encrypted = cipher.doFinal(data.toByteArray(Charset.defaultCharset())) 93 | 94 | /** 95 | * *Optional* Encode using Base64. 96 | * 97 | * Base64 encoding is used as a standard for encoding data that is sent between different parties 98 | * in order to preserve both data's format and information and allow it to be easily decoded and not corrupted during the transfer. 99 | */ 100 | val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT) 101 | 102 | /** 103 | * Return the Base64-encoded ciphertext alongside with the Initialization Vector (IV). 104 | * IV needs to be saved alongside with the ciphertext as it will be needed for decryption. 105 | * The most common way to do it is just to append/prepend IV to the ciphertext itself. 106 | */ 107 | return OperationResult( 108 | encoded, 109 | cipher.iv 110 | ) 111 | } 112 | 113 | fun decrypt(ciphertext: String, iv: ByteArray): String { 114 | /** 115 | * GCMParameterSpec with authentication tag length (in bits so 16 * 8 = 128) and the IV. 116 | * The generated tag is always 16 bytes long. 117 | */ 118 | val gcmParameterSpec = GCMParameterSpec(128, iv) 119 | 120 | // Create Cipher instance and init it in decryption mode supplying key and GCMParameterSpec. 121 | val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { 122 | init(Cipher.DECRYPT_MODE, key, gcmParameterSpec) 123 | } 124 | 125 | // Decode Base64-encoded ciphertext to raw bytes. 126 | val decoded = Base64.decode(ciphertext, Base64.DEFAULT) 127 | 128 | // *Optional* Update AAD. 129 | cipher.updateAAD(AAD.toByteArray()) 130 | 131 | // Decrypt. 132 | val decrypted = cipher.doFinal(decoded) 133 | 134 | // Return the decrypted plaintext as a String. 135 | return String(decrypted) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/hash/MessageDigest.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.hash 2 | 3 | import java.security.MessageDigest 4 | 5 | /** 6 | * Sample MessageDigest implementation on Android. 7 | */ 8 | object MessageDigest { 9 | 10 | fun calculateDigest(data: String): ByteArray { 11 | /** 12 | * Create MessageDigest instance using provided algorithm, calculate digest and return it. 13 | * 14 | * SHA-256 is used as one of NIST-approved hash functions. 15 | * Any algorithm from the NIST-approved SHA-2 family can be used here, as Android currently does not support SHA-3 16 | * (list of the NIST-approved hash functions can be found here https://csrc.nist.gov/projects/hash-functions). 17 | */ 18 | val messageDigest = MessageDigest.getInstance("SHA-256") 19 | messageDigest.update(data.toByteArray()) 20 | return messageDigest.digest() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/helpers/Security.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.helpers 2 | 3 | import android.util.Log 4 | import java.security.Security 5 | 6 | // List all providers available. 7 | fun listProviders() { 8 | for (provider in Security.getProviders()) { 9 | Log.d("Provider ", provider.name) 10 | } 11 | } 12 | 13 | // List supported algorithms applying filter. 14 | fun listSupportedAlgorithms(filter: String = "") { 15 | for (provider in Security.getProviders()) { 16 | val name = provider.name 17 | 18 | for (service in provider.services) { 19 | if (filter.isBlank()) { 20 | Log.d("$name algorithm: ", service.algorithm) 21 | } else { 22 | val algorithm = service.algorithm 23 | if (algorithm.contains(filter)) { 24 | Log.d("$name algorithm: ", service.algorithm) 25 | 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | // List supported MessageDigest algorithms. 33 | fun listMessageDigestAlgorithms() = 34 | Log.d("securevale: MessageDigest ", Security.getAlgorithms("MessageDigest").toString()) 35 | 36 | // List supported Cipher algorithms. 37 | fun listCipherAlgorithms() = 38 | Log.d("securevale: Cipher ", Security.getAlgorithms("Cipher").toString()) 39 | 40 | // List supported Signature algorithms. 41 | fun listSignatureAlgorithms() = 42 | Log.d("securevale: Signature ", Security.getAlgorithms("Signature").toString()) 43 | 44 | // List supported Mac algorithms. 45 | fun listMacAlgorithms() = 46 | Log.d("securevale: Mac ", Security.getAlgorithms("Mac").toString()) 47 | 48 | // List available strong SecureRandom(s). 49 | fun listStrongRandom() = 50 | Log.d("securevale: StrongRandom ", Security.getProperty("securerandom.strongAlgorithms")) -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/mac/Hmac.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.mac 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import android.util.Base64 6 | import java.nio.charset.Charset 7 | import java.security.Key 8 | import java.security.KeyStore 9 | import java.util.UUID 10 | import javax.crypto.KeyGenerator 11 | import javax.crypto.Mac 12 | import javax.crypto.SecretKey 13 | 14 | /** 15 | * Sample HMAC implementation on Android. 16 | */ 17 | object Hmac { 18 | 19 | private val store: KeyStore by lazy { 20 | // Initialise Android Keystore. 21 | KeyStore.getInstance("AndroidKeyStore").apply { 22 | load(null) 23 | } 24 | } 25 | 26 | /** 27 | * Just random alias per session of the app for sample purposes. 28 | * In real use cases, you should use something permanent as an alias. 29 | */ 30 | private val alias: String = UUID.randomUUID().toString() 31 | 32 | /** 33 | * Set all parameters of the key that is to be generated. 34 | * 35 | * keystoreAlias - the alias used to identify the key within the Keystore. 36 | * purposes - how the key will be used, for encryption and decryption in our case. 37 | */ 38 | private val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( 39 | alias, 40 | KeyProperties.PURPOSE_SIGN 41 | ).build() 42 | 43 | // Get generated key from the Keystore or generate one if not present. 44 | private val key: Key 45 | get() = (store.getEntry(alias, null) as? KeyStore.SecretKeyEntry)?.secretKey 46 | ?: generateSecretKey() 47 | 48 | // Generate key. 49 | private fun generateSecretKey(): SecretKey = 50 | KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore").run { 51 | // No need to create SecureRandom and pass it manually as the KeyGenerator creates its own instance under the hood. 52 | init(spec) 53 | // Generate key and return it. 54 | generateKey() 55 | } 56 | 57 | /** 58 | * In order to verify integrity and authenticity of a message after creating the original hmac 59 | * you should generate it again with message you received and compare to the original one (whether they match). 60 | * You can check such a sample check in [com.securevale.androidcryptosamples.ui.HmacSampleFragment]. 61 | */ 62 | fun computeHmac(data: String): String { 63 | // Create Mac instance and initialise it with chosen mode. 64 | val hmac = Mac.getInstance("HmacSHA256") 65 | 66 | // Init it providing secret key. 67 | hmac.init(key) 68 | 69 | // Calculate HMAC. 70 | val result = hmac.doFinal(data.toByteArray(Charset.defaultCharset())) 71 | 72 | /** 73 | * *Optional* Encode using Base64. 74 | * 75 | * Base64 encoding is used as a standard for encoding data that is sent between different parties 76 | * in order to preserve both data's format and information and allow it to be easily decoded and not corrupted during the transfer. 77 | */ 78 | return Base64.encodeToString(result, Base64.DEFAULT) 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/signature/Signature.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.signature 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import java.math.BigInteger 6 | import java.security.KeyPair 7 | import java.security.KeyPairGenerator 8 | import java.security.KeyStore 9 | import java.security.PrivateKey 10 | import java.security.Signature 11 | import java.util.Calendar 12 | import java.util.Date 13 | import java.util.UUID 14 | import javax.security.auth.x500.X500Principal 15 | 16 | /** 17 | * Sample Signing/Verifying with RSA implementation on Android. 18 | */ 19 | object Signature { 20 | 21 | private val store: KeyStore by lazy { 22 | // Initialise Android Keystore. 23 | KeyStore.getInstance("AndroidKeyStore").apply { 24 | load(null) 25 | } 26 | } 27 | 28 | /** 29 | * Just random alias per session of the app for sample purposes. 30 | * In real use cases, you should use something permanent as an alias. 31 | */ 32 | private val alias: String = UUID.randomUUID().toString() 33 | 34 | /** 35 | * Set all parameters of the key that is to be generated. 36 | * 37 | * keystoreAlias - the alias used to identify the key within the Keystore. 38 | * purposes - how the key will be used, for encryption and decryption in our case. 39 | */ 40 | private fun spec(notBefore: Date, notAfter: Date): KeyGenParameterSpec = 41 | KeyGenParameterSpec.Builder( 42 | alias, 43 | KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY 44 | ) 45 | /** 46 | * Set the key size. 47 | * 48 | * The current recommended RSA key size is 2048 bits, but if you want to be more future proof consider 3072 or 4096 bits. 49 | * It is worth noting that RSA can only encrypt data of which size does not exceed the selected key size (2048 bits = 256 bytes in our case) minus 50 | * any padding and header data. 51 | * For more detailed explanation check the "FAQ" section in README. 52 | */ 53 | .setKeySize(2048) 54 | /** 55 | * Set digest with which the key can be used. 56 | */ 57 | .setDigests(KeyProperties.DIGEST_SHA256) 58 | /** 59 | * Key's validity start date. It's just a sample, in real scenarios you should use date that best suits your use-cases. 60 | */ 61 | .setKeyValidityStart(notBefore) 62 | /** 63 | * Key's validity end date. It's just a sample, in real scenarios you should use date that best suits your use-cases. 64 | */ 65 | .setKeyValidityEnd(notAfter) 66 | /** 67 | * CertificateSubject. It's just a sample, in real scenarios you should use subject that best suits your use-cases. 68 | */ 69 | .setCertificateSubject(X500Principal("CN=example")) 70 | /** 71 | * *Optional* It defaults to one. 72 | */ 73 | .setCertificateSerialNumber(BigInteger.ONE) 74 | .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) 75 | .build() 76 | 77 | 78 | fun sign(data: ByteArray): ByteArray { 79 | // Create Signature instance and init it with chosen mode. 80 | val signature = Signature.getInstance("SHA256withRSA") 81 | 82 | // Init in signing mode supplying the private key. 83 | signature.initSign(privateKey) 84 | 85 | // Update the Signature instance with data to sign. 86 | signature.update(data) 87 | 88 | // Sign. 89 | return signature.sign() 90 | } 91 | 92 | fun verify(data: ByteArray, sign: ByteArray): Boolean { 93 | // Create Signature instance and init it with chosen mode. 94 | val signature = Signature.getInstance("SHA256withRSA") 95 | 96 | // Init in verification mode supplying the public key. 97 | signature.initVerify(publicKey) 98 | 99 | // Update the Signature instance with data to verify. 100 | signature.update(data) 101 | 102 | // Verify 103 | return signature.verify(sign) 104 | } 105 | 106 | // Get generated keypair from the Keystore or generate one if not present. 107 | private val keyPair: KeyPair 108 | get() { 109 | val privateKey = store.getKey(alias, null) as PrivateKey? 110 | val publicKey = store.getCertificate(alias)?.publicKey 111 | 112 | return if (privateKey != null && publicKey != null) { 113 | KeyPair(publicKey, privateKey) 114 | } else { 115 | generateKeyPair() 116 | } 117 | } 118 | 119 | // Get the public key. 120 | private val publicKey = keyPair.public 121 | 122 | // Get the private key. 123 | private val privateKey = keyPair.private 124 | 125 | // Generate key pair. 126 | private fun generateKeyPair(): KeyPair = 127 | KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore").run { 128 | // Initialise with spec. 129 | initialize( 130 | spec( 131 | Calendar.getInstance().apply { add(Calendar.YEAR, -1) }.time, 132 | Calendar.getInstance().apply { add(Calendar.YEAR, 5) }.time 133 | ) 134 | ) 135 | // Generate key pair and return it. 136 | generateKeyPair() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/AesCbcSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.securevale.androidcryptosamples.R 9 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 10 | import com.securevale.androidcryptosamples.encryption.symmetric.aes.cbc.AesCbc 11 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 12 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 13 | 14 | class AesCbcSampleFragment : Fragment() { 15 | 16 | private var encryptionResult: OperationResult = OperationResult() 17 | private var binding: SampleFragmentBinding by bindWithLifecycle() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 24 | binding = this 25 | }.root 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | initFields() 30 | } 31 | 32 | private fun initFields() = with(binding) { 33 | 34 | encryptionBtn.setOnClickListener { 35 | val text = input.text.toString() 36 | 37 | if (text.isBlank()) { 38 | result.text = getString(R.string.nothing_to_encrypt) 39 | } else { 40 | encryptionResult = AesCbc.encrypt(input.text.toString()) 41 | result.text = getString(R.string.encrypted, encryptionResult.data) 42 | } 43 | } 44 | 45 | decryptionBtn.setOnClickListener { 46 | if (encryptionResult.hasNoData()) { 47 | result.text = getString(R.string.nothing_to_decrypt) 48 | } else { 49 | val decrypted = AesCbc.decrypt(encryptionResult.data, encryptionResult.iv) 50 | result.text = getString(R.string.decrypted, decrypted) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/AesGcmSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.securevale.androidcryptosamples.R 9 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 10 | import com.securevale.androidcryptosamples.encryption.symmetric.aes.gcm.AesGcm 11 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 12 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 13 | 14 | class AesGcmSampleFragment : Fragment() { 15 | 16 | private var encryptionResult: OperationResult = OperationResult() 17 | private var binding: SampleFragmentBinding by bindWithLifecycle() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 24 | binding = this 25 | }.root 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | initFields() 30 | } 31 | 32 | private fun initFields() = with(binding) { 33 | encryptionBtn.setOnClickListener { 34 | val text = input.text.toString() 35 | 36 | if (text.isEmpty()) { 37 | result.text = getString(R.string.nothing_to_encrypt) 38 | } else { 39 | encryptionResult = AesGcm.encrypt(input.text.toString()) 40 | result.text = getString(R.string.encrypted, encryptionResult.data) 41 | } 42 | } 43 | 44 | decryptionBtn.setOnClickListener { 45 | if (encryptionResult.hasNoData()) { 46 | result.text = getString(R.string.nothing_to_decrypt) 47 | } else { 48 | val decrypted = AesGcm.decrypt(encryptionResult.data, encryptionResult.iv) 49 | result.text = getString(R.string.decrypted, decrypted) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/BiometricSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | import androidx.fragment.app.Fragment 9 | import com.securevale.androidcryptosamples.R 10 | import com.securevale.androidcryptosamples.advanced.biometric.BiometricCallback 11 | import com.securevale.androidcryptosamples.advanced.biometric.Purpose 12 | import com.securevale.androidcryptosamples.advanced.biometric.biometricAvailable 13 | import com.securevale.androidcryptosamples.advanced.biometric.decryptWithBiometrics 14 | import com.securevale.androidcryptosamples.advanced.biometric.encryptWithBiometrics 15 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 16 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 17 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 18 | 19 | class BiometricSampleFragment : Fragment() { 20 | 21 | private var operationResult: OperationResult = OperationResult() 22 | 23 | private var mode = Purpose.ENCRYPTION 24 | 25 | private var binding: SampleFragmentBinding by bindWithLifecycle() 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, 29 | container: ViewGroup?, 30 | savedInstanceState: Bundle? 31 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 32 | binding = this 33 | }.root 34 | 35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 36 | super.onViewCreated(view, savedInstanceState) 37 | initFields() 38 | } 39 | 40 | private fun initFields() = with(binding) { 41 | encryptionBtn.setOnClickListener { 42 | encrypt(input.text.toString()) 43 | mode = Purpose.ENCRYPTION 44 | } 45 | 46 | decryptionBtn.setOnClickListener { 47 | decrypt(operationResult) 48 | mode = Purpose.DECRYPTION 49 | } 50 | } 51 | 52 | private fun updateViews(mode: Purpose) = with(binding) { 53 | when (mode) { 54 | Purpose.ENCRYPTION -> { 55 | result.text = getString(R.string.encrypted, operationResult.data) 56 | } 57 | 58 | Purpose.DECRYPTION -> { 59 | result.text = getString(R.string.decrypted, operationResult.data) 60 | } 61 | } 62 | } 63 | 64 | private fun encrypt(data: String) { 65 | if (biometricAvailable(requireContext())) { 66 | if (data.isBlank()) { 67 | binding.result.text = getString(R.string.nothing_to_encrypt) 68 | } else { 69 | encryptWithBiometrics(this, data, callback) 70 | } 71 | } else { 72 | Toast.makeText( 73 | requireContext(), 74 | getString(R.string.biometric_not_available), 75 | Toast.LENGTH_LONG 76 | ).show() 77 | } 78 | } 79 | 80 | private fun decrypt(encryptedData: OperationResult) { 81 | if (biometricAvailable(requireContext())) { 82 | decryptWithBiometrics(this, encryptedData, callback) 83 | } else { 84 | Toast.makeText( 85 | requireContext(), 86 | getString(R.string.biometric_not_available), 87 | Toast.LENGTH_LONG 88 | ).show() 89 | } 90 | } 91 | 92 | private val callback = object : BiometricCallback { 93 | override fun onError(errorCode: Int, errorString: CharSequence) { 94 | Toast.makeText(requireContext(), errorString, Toast.LENGTH_LONG).show() 95 | // Something went wrong, you need to handle accordingly. 96 | } 97 | 98 | override fun onFailed() { 99 | // Something went wrong, you need to handle accordingly. 100 | } 101 | 102 | override fun onSuccess(result: OperationResult) { 103 | operationResult = result 104 | updateViews(mode) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/HmacSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.securevale.androidcryptosamples.R 9 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 10 | import com.securevale.androidcryptosamples.mac.Hmac 11 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 12 | 13 | class HmacSampleFragment : Fragment() { 14 | 15 | private var hmac: String? = null 16 | 17 | private var binding: SampleFragmentBinding by bindWithLifecycle() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 24 | binding = this 25 | }.root 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | initFields() 30 | } 31 | 32 | private fun initFields() = with(binding) { 33 | input.hint = getString(R.string.hmac_hint) 34 | 35 | encryptionBtn.let { 36 | it.text = getString(R.string.hmac) 37 | it.setOnClickListener { 38 | val data = input.text.toString() 39 | if (data.isBlank()) { 40 | result.text = getString(R.string.nothing_to_hmac) 41 | } else { 42 | hmac = Hmac.computeHmac(data) 43 | 44 | result.text = hmac 45 | } 46 | } 47 | } 48 | 49 | decryptionBtn.apply { 50 | text = getString(R.string.verify) 51 | setOnClickListener { 52 | val data = input.text.toString() 53 | if (data.isBlank()) { 54 | result.text = getString(R.string.nothing_to_verify) 55 | } else { 56 | val computedHmacResult = Hmac.computeHmac(data) 57 | 58 | result.text = getString( 59 | if (computedHmacResult == hmac) R.string.valid_message else 60 | R.string.invalid_message 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/KeyDerivationSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.util.Base64 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.securevale.androidcryptosamples.R 10 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 11 | import com.securevale.androidcryptosamples.derivation.KeyDerivation 12 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 13 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 14 | import java.nio.charset.Charset 15 | import java.security.SecureRandom 16 | import javax.crypto.BadPaddingException 17 | import javax.crypto.Cipher 18 | import javax.crypto.spec.IvParameterSpec 19 | 20 | class KeyDerivationSampleFragment : Fragment() { 21 | 22 | private var encryptionResult: OperationResult = OperationResult() 23 | private var binding: SampleFragmentBinding by bindWithLifecycle() 24 | 25 | private val sampleMessage = "This is your decrypted message!" 26 | 27 | private val salt: ByteArray by lazy { 28 | /** 29 | * Create salt using random generator, as it should be unique for each encryption key created. 30 | * Store the salt used for each generated key, and ensure you use the same salt during decryption. 31 | */ 32 | val secureRandom = SecureRandom() 33 | 34 | /** 35 | * Public key cryptography standard recommends salt length of at least 64 bits https://datatracker.ietf.org/doc/html/rfc8018#section-4. 36 | */ 37 | val salt = ByteArray(512) 38 | 39 | secureRandom.nextBytes(salt) 40 | 41 | salt 42 | } 43 | 44 | override fun onCreateView( 45 | inflater: LayoutInflater, 46 | container: ViewGroup?, 47 | savedInstanceState: Bundle? 48 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 49 | binding = this 50 | }.root 51 | 52 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 53 | super.onViewCreated(view, savedInstanceState) 54 | initFields() 55 | } 56 | 57 | private fun initFields() = with(binding) { 58 | 59 | input.hint = getString(R.string.key_derivation_hint) 60 | 61 | encryptionBtn.setOnClickListener { 62 | val password = input.text.toString() 63 | 64 | if (password.isBlank()) { 65 | result.text = getString(R.string.no_password_to_derive_key_from) 66 | } else { 67 | encryptionResult = encrypt(password) 68 | result.text = getString(R.string.encrypted, encryptionResult.data) 69 | } 70 | } 71 | 72 | decryptionBtn.setOnClickListener { 73 | if (encryptionResult.hasNoData()) { 74 | result.text = getString(R.string.nothing_to_decrypt) 75 | } else { 76 | val password = input.text.toString() 77 | 78 | if (password.isBlank()) { 79 | result.text = getString(R.string.no_password_to_derive_key_from) 80 | } else { 81 | val decrypted = decrypt(password) 82 | 83 | if (decrypted.isBlank()) { 84 | result.text = getString(R.string.decryption_with_derived_key_failed) 85 | } else { 86 | result.text = getString( 87 | R.string.decrypted, 88 | decrypted 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | // Separated methods for encryption/decryption to keep the sample clear and simple. 97 | private fun encrypt(password: String): OperationResult { 98 | val derivedKey = KeyDerivation.deriveKey(password, salt) 99 | 100 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 101 | init(Cipher.ENCRYPT_MODE, derivedKey) 102 | } 103 | 104 | val encoded = Base64.encodeToString( 105 | cipher.doFinal(sampleMessage.toByteArray(Charset.defaultCharset())), 106 | Base64.DEFAULT 107 | ) 108 | 109 | return OperationResult(encoded, cipher.iv) 110 | } 111 | 112 | private fun decrypt(password: String): String { 113 | val derivedKey = KeyDerivation.deriveKey(password, salt) 114 | 115 | val ivParameterSpec = IvParameterSpec(encryptionResult.iv) 116 | 117 | // I used AES-CBC as an example, but you can use the derived key with other ciphers, adjusting the parameters as needed for your specific use case. 118 | val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { 119 | init(Cipher.DECRYPT_MODE, derivedKey, ivParameterSpec) 120 | } 121 | 122 | val decoded = Base64.decode(encryptionResult.data, Base64.DEFAULT) 123 | 124 | 125 | return try { 126 | String(cipher.doFinal(decoded)) 127 | } catch (e: BadPaddingException) { 128 | "" 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Button 8 | import android.widget.FrameLayout 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.core.view.isVisible 11 | import androidx.fragment.app.Fragment 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.securevale.androidcryptosamples.R 15 | import com.securevale.androidcryptosamples.databinding.ActivityMainBinding 16 | import com.securevale.androidcryptosamples.helpers.listCipherAlgorithms 17 | import com.securevale.androidcryptosamples.helpers.listMacAlgorithms 18 | import com.securevale.androidcryptosamples.helpers.listMessageDigestAlgorithms 19 | import com.securevale.androidcryptosamples.helpers.listSignatureAlgorithms 20 | import com.securevale.androidcryptosamples.helpers.listStrongRandom 21 | import com.securevale.androidcryptosamples.ui.adapter.SamplesAdapter 22 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 23 | 24 | class MainActivity : AppCompatActivity(), SamplesAdapter.SampleClickListener { 25 | 26 | private var binding: ActivityMainBinding by bindWithLifecycle() 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | 31 | binding = ActivityMainBinding.inflate(layoutInflater).apply { 32 | setContentView(root) 33 | recycler.apply { 34 | layoutManager = LinearLayoutManager(this@MainActivity) 35 | adapter = SamplesAdapter(this@MainActivity) 36 | } 37 | } 38 | 39 | // List just for educational purposes. TODO create dedicated screen for presenting these 40 | listMessageDigestAlgorithms() 41 | listCipherAlgorithms() 42 | listSignatureAlgorithms() 43 | listMacAlgorithms() 44 | listStrongRandom() 45 | } 46 | 47 | private fun replace(fragment: Fragment) { 48 | with(binding) { 49 | container.apply { 50 | changeComponentsVisibility(true) 51 | supportFragmentManager 52 | .beginTransaction() 53 | .replace(R.id.container, fragment) 54 | .addToBackStack(null) 55 | .commit() 56 | } 57 | } 58 | } 59 | 60 | @Suppress("OVERRIDE_DEPRECATION") 61 | override fun onBackPressed() { 62 | // Ugly but hey it's just a sample app for cryptography 63 | if (supportFragmentManager.backStackEntryCount > 0) { 64 | supportFragmentManager.popBackStack() 65 | changeComponentsVisibility(false) 66 | } else { 67 | super.onBackPressed() 68 | } 69 | } 70 | 71 | override fun onSampleClick(sampleFragment: Class) = 72 | replace(sampleFragment.newInstance()) 73 | 74 | private fun changeComponentsVisibility(containerVisible: Boolean) = with(binding) { 75 | container.isVisible = containerVisible 76 | recycler.isVisible = !containerVisible 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/MessageDigestSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.util.Base64 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.view.isVisible 9 | import androidx.fragment.app.Fragment 10 | import com.securevale.androidcryptosamples.R 11 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 12 | import com.securevale.androidcryptosamples.hash.MessageDigest 13 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 14 | 15 | class MessageDigestSampleFragment : Fragment() { 16 | 17 | private var binding: SampleFragmentBinding by bindWithLifecycle() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 24 | binding = this 25 | }.root 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | initFields() 30 | } 31 | 32 | private fun initFields() = with(binding) { 33 | input.hint = getString(R.string.digest_hint) 34 | encryptionBtn.apply { 35 | text = getString(R.string.digest) 36 | setOnClickListener { 37 | val data = input.text.toString() 38 | if (data.isBlank()) { 39 | result.text = getString(R.string.nothing_to_digest) 40 | } else { 41 | val digest = MessageDigest.calculateDigest(data) 42 | // Make digest Base64-encoded for showing in UI. 43 | result.text = getString( 44 | R.string.digest_result, 45 | Base64.encodeToString(digest, Base64.DEFAULT) 46 | ) 47 | } 48 | } 49 | } 50 | decryptionBtn.isVisible = false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/RsaSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.securevale.androidcryptosamples.R 9 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 10 | import com.securevale.androidcryptosamples.encryption.assymetric.rsa.Rsa 11 | import com.securevale.androidcryptosamples.ui.dto.OperationResult 12 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 13 | 14 | class RsaSampleFragment : Fragment() { 15 | 16 | private var encryptionResult: OperationResult = OperationResult() 17 | private var binding: SampleFragmentBinding by bindWithLifecycle() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 24 | binding = this 25 | }.root 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | initFields() 30 | } 31 | 32 | private fun initFields() = with(binding) { 33 | encryptionBtn.setOnClickListener { 34 | val text = input.text.toString() 35 | if (text.isEmpty()) { 36 | result.text = getString(R.string.nothing_to_encrypt) 37 | } else { 38 | encryptionResult = OperationResult(Rsa.encrypt(input.text.toString())) 39 | result.text = getString(R.string.encrypted, encryptionResult.data) 40 | } 41 | } 42 | 43 | decryptionBtn.setOnClickListener { 44 | if (encryptionResult.hasNoData()) { 45 | result.text = getString(R.string.nothing_to_decrypt) 46 | } else { 47 | val decrypted = OperationResult(Rsa.decrypt(encryptionResult.data)) 48 | result.text = getString(R.string.decrypted, decrypted.data) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/SignatureSampleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.securevale.androidcryptosamples.R 9 | import com.securevale.androidcryptosamples.databinding.SampleFragmentBinding 10 | import com.securevale.androidcryptosamples.signature.Signature 11 | import com.securevale.androidcryptosamples.ui.lifecycle.bindWithLifecycle 12 | 13 | class SignatureSampleFragment : Fragment() { 14 | 15 | private var signature: ByteArray? = null 16 | private var binding: SampleFragmentBinding by bindWithLifecycle() 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View = SampleFragmentBinding.inflate(inflater, container, false).apply { 23 | binding = this 24 | }.root 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | initFields() 29 | } 30 | 31 | private fun initFields() = with(binding) { 32 | input.hint = getString(R.string.signing_hint) 33 | 34 | encryptionBtn.apply { 35 | text = getString(R.string.sign) 36 | setOnClickListener { 37 | val data = input.text.toString() 38 | if (data.isBlank()) { 39 | result.text = getString(R.string.nothing_to_sign) 40 | } else { 41 | signature = Signature.sign(data.toByteArray()) 42 | result.text = getString(R.string.signed) 43 | } 44 | } 45 | } 46 | 47 | decryptionBtn.apply { 48 | text = getString(R.string.verify) 49 | setOnClickListener { 50 | val data = input.text.toString() 51 | if (data.isBlank()) { 52 | result.text = getString(R.string.nothing_to_verify) 53 | } else { 54 | val verificationResult = Signature.verify(data.toByteArray(), signature!!) 55 | 56 | val verdict = 57 | getString(if (verificationResult) R.string.valid_signature else R.string.invalid_signature) 58 | 59 | result.text = verdict 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/adapter/SamplesAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.fragment.app.Fragment 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.securevale.androidcryptosamples.databinding.SampleButtonBinding 8 | import com.securevale.androidcryptosamples.ui.AesCbcSampleFragment 9 | import com.securevale.androidcryptosamples.ui.AesGcmSampleFragment 10 | import com.securevale.androidcryptosamples.ui.BiometricSampleFragment 11 | import com.securevale.androidcryptosamples.ui.HmacSampleFragment 12 | import com.securevale.androidcryptosamples.ui.KeyDerivationSampleFragment 13 | import com.securevale.androidcryptosamples.ui.MessageDigestSampleFragment 14 | import com.securevale.androidcryptosamples.ui.RsaSampleFragment 15 | import com.securevale.androidcryptosamples.ui.SignatureSampleFragment 16 | 17 | class SamplesAdapter(private val clickListener: SampleClickListener) : 18 | RecyclerView.Adapter() { 19 | 20 | private val fragments = listOf( 21 | "AES-CBC" to AesCbcSampleFragment::class.java, 22 | "AES-GCM" to AesGcmSampleFragment::class.java, 23 | "BIOMETRIC" to BiometricSampleFragment::class.java, 24 | "HMAC" to HmacSampleFragment::class.java, 25 | "MESSAGE DIGEST" to MessageDigestSampleFragment::class.java, 26 | "RSA" to RsaSampleFragment::class.java, 27 | "SIGNATURE" to SignatureSampleFragment::class.java, 28 | "KEY DERIVATION" to KeyDerivationSampleFragment::class.java 29 | ) 30 | 31 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 32 | ViewHolder( 33 | SampleButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false) 34 | ) 35 | 36 | override fun getItemCount(): Int = fragments.size 37 | 38 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 39 | holder.bind(fragments[position]) 40 | } 41 | 42 | inner class ViewHolder(private val binding: SampleButtonBinding) : 43 | RecyclerView.ViewHolder(binding.root) { 44 | 45 | fun bind(fragment: Pair>) = with(binding) { 46 | button.apply { 47 | text = fragment.first 48 | setOnClickListener { clickListener.onSampleClick(fragment.second) } 49 | } 50 | } 51 | } 52 | 53 | interface SampleClickListener { 54 | fun onSampleClick(sampleFragment: Class) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/dto/OperationResult.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui.dto 2 | 3 | @Suppress("ArrayInDataClass") 4 | data class OperationResult( 5 | val data: String = "", 6 | val iv: ByteArray = byteArrayOf() 7 | ) { 8 | 9 | fun hasNoData() = data.isBlank() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/securevale/androidcryptosamples/ui/lifecycle/ClearOnDestroyObserver.kt: -------------------------------------------------------------------------------- 1 | package com.securevale.androidcryptosamples.ui.lifecycle 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.DefaultLifecycleObserver 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleOwner 8 | import kotlin.reflect.KProperty 9 | 10 | class ClearOnDestroyObserver(private val lifecycle: () -> Lifecycle) : DefaultLifecycleObserver { 11 | 12 | private var value: T? = null 13 | 14 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = 15 | checkNotNull(value) { "Value not initialised or being outside of lifecycle" } 16 | 17 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 18 | lifecycle().removeObserver(this) 19 | this.value = value 20 | lifecycle().addObserver(this) 21 | } 22 | 23 | override fun onDestroy(owner: LifecycleOwner) { 24 | super.onDestroy(owner) 25 | value = null 26 | } 27 | } 28 | 29 | fun AppCompatActivity.bindWithLifecycle() = ClearOnDestroyObserver { lifecycle } 30 | fun Fragment.bindWithLifecycle() = ClearOnDestroyObserver { viewLifecycleOwner.lifecycle } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sample_button.xml: -------------------------------------------------------------------------------- 1 | 2 |