├── .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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/sample_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
34 |
35 |
43 |
44 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | android-crypto-samples
3 | Nothing to encrypt. Please provide text for encryption first
4 | Nothing to decrypt. Encrypt first
5 | Encrypted: %1$s
6 | Decrypted: %1$s
7 | Signature is valid
8 | Signature is invalid
9 | Sign
10 | Verify
11 | Signed
12 | Paste the message to sign
13 | No text provided for signing
14 | No text provided for verifying
15 | Message is valid
16 | Message is invalid
17 | Paste the message to create an HMAC
18 | HMAC
19 | No text provided to make HMAC
20 | Paste the message to create digest
21 | Digest
22 | No text provided to calculate the digest
23 | Digest: %1$s
24 | Biometric is not available (or not enabled) on this device
25 | Confirm operation
26 | Paste password to derive key from
27 | No password for key derivation
28 | Decryption failed, have you changed the typed password?
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '8.5.0' apply false
4 | id 'com.android.library' version '8.5.0' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/securevale/android-cryptography-samples/acf8c0af8d81b1931218305b70925a092084d710/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 28 15:09:26 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "android-crypto-samples"
16 | include ':app'
17 |
--------------------------------------------------------------------------------