Skip to content

Commit aefcdaa

Browse files
authored
Replace weak crypto implementation (#158)
Replace crypto implementation with stronger key generation method.
1 parent 96bf8d6 commit aefcdaa

File tree

10 files changed

+264
-26
lines changed

10 files changed

+264
-26
lines changed

line-sdk/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ apply plugin: 'maven-publish'
77
apply plugin: 'signing'
88

99
group = "com.linecorp.linesdk"
10-
version = "5.9.1"
10+
version = "5.10.0"
1111

1212
android {
1313
compileSdk 33
1414

1515
defaultConfig {
1616
minSdkVersion 24
1717
targetSdkVersion 33
18-
versionCode 5_09_00
18+
versionCode 5_10_00
1919
versionName version
2020

2121
consumerProguardFiles 'consumer-proguard-rules.pro'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.linecorp.android.security.encryption
2+
3+
import android.util.Base64
4+
5+
class CipherData(
6+
val encryptedData: ByteArray,
7+
val initialVector: ByteArray,
8+
val hmacValue: ByteArray,
9+
) {
10+
fun encodeToBase64String(): String =
11+
listOf(encryptedData, initialVector, hmacValue)
12+
.joinToString(SEPARATOR) { it.encodeBase64() }
13+
14+
companion object {
15+
private const val SEPARATOR = ";"
16+
private const val SIZE_DATA_TYPES = 3
17+
18+
fun decodeFromBase64String(cipherDataBase64String: String): CipherData {
19+
val parts = cipherDataBase64String.split(SEPARATOR)
20+
require(parts.size == SIZE_DATA_TYPES) {
21+
"Failed to split encrypted text `$cipherDataBase64String`"
22+
}
23+
24+
return CipherData(
25+
encryptedData = parts[0].decodeBase64(),
26+
initialVector = parts[1].decodeBase64(),
27+
hmacValue = parts[2].decodeBase64()
28+
)
29+
}
30+
}
31+
}
32+
33+
private fun ByteArray.encodeBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP)
34+
35+
private fun String.decodeBase64(): ByteArray = Base64.decode(this, Base64.NO_WRAP)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.linecorp.android.security.encryption
2+
3+
import android.content.Context
4+
import android.security.keystore.KeyGenParameterSpec
5+
import android.security.keystore.KeyProperties
6+
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
7+
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
8+
import android.security.keystore.KeyProperties.PURPOSE_SIGN
9+
import android.security.keystore.KeyProperties.PURPOSE_VERIFY
10+
import java.security.KeyStore
11+
import java.security.MessageDigest
12+
import javax.crypto.Cipher
13+
import javax.crypto.KeyGenerator
14+
import javax.crypto.Mac
15+
import javax.crypto.SecretKey
16+
import javax.crypto.spec.IvParameterSpec
17+
18+
/**
19+
* AES cipher by AndroidKeyStore
20+
*/
21+
class StringAesCipher : StringCipher {
22+
private val keyStore: KeyStore by lazy {
23+
KeyStore.getInstance(ANDROID_KEY_STORE).also {
24+
it.load(null)
25+
}
26+
}
27+
28+
private lateinit var hmac: Mac
29+
30+
override fun initialize(context: Context) {
31+
if (::hmac.isInitialized) {
32+
return
33+
}
34+
35+
synchronized(this) {
36+
getAesSecretKey()
37+
val integrityKey = getIntegrityKey()
38+
39+
hmac = Mac.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256).apply {
40+
init(integrityKey)
41+
}
42+
}
43+
}
44+
45+
override fun encrypt(context: Context, plainText: String): String {
46+
synchronized(this) {
47+
initialize(context)
48+
49+
try {
50+
val secretKey = getAesSecretKey()
51+
52+
val cipher = Cipher.getInstance(TRANSFORMATION_FORMAT).apply {
53+
init(Cipher.ENCRYPT_MODE, secretKey)
54+
}
55+
val encryptedData: ByteArray = cipher.doFinal(plainText.toByteArray())
56+
57+
return CipherData(
58+
encryptedData = encryptedData,
59+
initialVector = cipher.iv,
60+
hmacValue = hmac.calculateHmacValue(encryptedData, cipher.iv)
61+
).encodeToBase64String()
62+
} catch (e: Exception) {
63+
throw EncryptionException("Failed to encrypt", e)
64+
}
65+
}
66+
}
67+
68+
override fun decrypt(context: Context, cipherText: String): String {
69+
synchronized(this) {
70+
try {
71+
val secretKey = getAesSecretKey()
72+
73+
val cipherData = CipherData.decodeFromBase64String(cipherText)
74+
75+
cipherData.verifyHmacValue(hmac)
76+
77+
val ivSpec = IvParameterSpec(cipherData.initialVector)
78+
79+
return Cipher.getInstance(TRANSFORMATION_FORMAT)
80+
.apply { init(Cipher.DECRYPT_MODE, secretKey, ivSpec) }
81+
.run { doFinal(cipherData.encryptedData) }
82+
.let {
83+
String(it)
84+
}
85+
} catch (e: Exception) {
86+
throw EncryptionException("Failed to decrypt", e)
87+
}
88+
}
89+
}
90+
91+
private fun getAesSecretKey(): SecretKey {
92+
return if (keyStore.containsAlias(AES_KEY_ALIAS)) {
93+
val secretKeyEntry =
94+
keyStore.getEntry(AES_KEY_ALIAS, null) as KeyStore.SecretKeyEntry
95+
96+
secretKeyEntry.secretKey
97+
} else {
98+
createAesKey()
99+
}
100+
}
101+
102+
private fun getIntegrityKey(): SecretKey {
103+
return if (keyStore.containsAlias(INTEGRITY_KEY_ALIAS)) {
104+
val secretKeyEntry =
105+
keyStore.getEntry(INTEGRITY_KEY_ALIAS, null) as KeyStore.SecretKeyEntry
106+
107+
secretKeyEntry.secretKey
108+
} else {
109+
createIntegrityKey()
110+
}
111+
}
112+
113+
/**
114+
* Create a new AES key in the Android KeyStore. This key will be used for
115+
* encrypting and decrypting data. The key is generated with a size of 256 bits,
116+
* using the CBC block mode and PKCS7 padding.
117+
*/
118+
private fun createAesKey(): SecretKey {
119+
val keyGenerator = KeyGenerator
120+
.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
121+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
122+
AES_KEY_ALIAS,
123+
PURPOSE_ENCRYPT or PURPOSE_DECRYPT
124+
)
125+
.setKeySize(KEY_SIZE_IN_BIT)
126+
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
127+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
128+
.build()
129+
130+
keyGenerator.run {
131+
init(keyGenParameterSpec)
132+
return generateKey()
133+
}
134+
}
135+
136+
private fun createIntegrityKey(): SecretKey {
137+
val keyGenerator = KeyGenerator
138+
.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256, ANDROID_KEY_STORE)
139+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
140+
INTEGRITY_KEY_ALIAS,
141+
PURPOSE_SIGN or PURPOSE_VERIFY
142+
)
143+
.build()
144+
145+
keyGenerator.run {
146+
init(keyGenParameterSpec)
147+
return generateKey()
148+
}
149+
}
150+
151+
private fun Mac.calculateHmacValue(
152+
encryptedData: ByteArray,
153+
initialVector: ByteArray
154+
): ByteArray = doFinal(encryptedData + initialVector)
155+
156+
/**
157+
* Validate the HMAC value
158+
*
159+
* @throws SecurityException if the HMAC value doesn't match with [encryptedData]
160+
*/
161+
private fun CipherData.verifyHmacValue(mac: Mac) {
162+
val expectedHmacValue: ByteArray = mac.calculateHmacValue(
163+
encryptedData = encryptedData,
164+
initialVector = initialVector
165+
)
166+
167+
if (!MessageDigest.isEqual(expectedHmacValue, hmacValue)) {
168+
throw SecurityException("Cipher text has been tampered with.")
169+
}
170+
}
171+
172+
companion object {
173+
private const val AES_KEY_ALIAS =
174+
"com.linecorp.android.security.encryption.StringAesCipher"
175+
176+
private const val INTEGRITY_KEY_ALIAS =
177+
"com.linecorp.android.security.encryption.StringAesCipher.INTEGRITY_KEY"
178+
179+
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
180+
private const val KEY_SIZE_IN_BIT = 256
181+
182+
private const val TRANSFORMATION_FORMAT =
183+
KeyProperties.KEY_ALGORITHM_AES +
184+
"/${KeyProperties.BLOCK_MODE_CBC}" +
185+
"/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"
186+
}
187+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.linecorp.android.security.encryption
2+
3+
import android.content.Context
4+
5+
interface StringCipher {
6+
7+
fun initialize(context: Context)
8+
9+
fun encrypt(context: Context, plainText: String): String
10+
11+
fun decrypt(context: Context, cipherText: String): String
12+
}

line-sdk/src/main/java/com/linecorp/android/security/encryption/StringCipher.java renamed to line-sdk/src/main/java/com/linecorp/android/security/encryption/StringCipherDeprecated.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import android.content.SharedPreferences;
66
import android.os.Build;
77
import android.provider.Settings;
8+
import android.text.TextUtils;
9+
import android.util.Base64;
10+
811
import androidx.annotation.NonNull;
912
import androidx.annotation.Nullable;
1013
import androidx.annotation.WorkerThread;
11-
import android.text.TextUtils;
12-
import android.util.Base64;
1314

1415
import java.io.UnsupportedEncodingException;
1516
import java.security.InvalidAlgorithmParameterException;
@@ -45,9 +46,12 @@
4546
* Either first access of {@link #encrypt(Context, String)}, {@link #decrypt(Context, String)} or
4647
* {@link #initialize(Context)} is very slow because there is secret key generation with PBKDF2.
4748
* We recommend that you initialize an instance of this class beforehand and cache it.
49+
*
50+
* @deprecated This class is obsolete. Use {@link StringAesCipher} as its replacement.
4851
*/
52+
@Deprecated
4953
@WorkerThread
50-
public class StringCipher {
54+
public class StringCipherDeprecated implements StringCipher {
5155
// for PBKDF
5256
private static final int DEFAULT_ITERATIONS = 10000;
5357

@@ -79,7 +83,7 @@ public class StringCipher {
7983
@Nullable
8084
private SecretKeys secretKeys;
8185

82-
public StringCipher(@NonNull String sharedPreferenceName) {
86+
public StringCipherDeprecated(@NonNull String sharedPreferenceName) {
8387
this(sharedPreferenceName, DEFAULT_ITERATIONS, false);
8488
}
8589

@@ -92,7 +96,7 @@ public StringCipher(@NonNull String sharedPreferenceName) {
9296
Note : This field should always be false as it is deprecated and
9397
returns UNKNOWN in some cases from Android SDK >= 27
9498
*/
95-
public StringCipher(
99+
public StringCipherDeprecated(
96100
@NonNull String sharedPreferenceName,
97101
int pbkdf2IterationCount,
98102
boolean isSerialIncludedInDevicePackageSpecificId) {
@@ -111,6 +115,7 @@ public StringCipher(
111115
}
112116
}
113117

118+
@Override
114119
public void initialize(@NonNull Context context) {
115120
synchronized (syncObject) {
116121
if (secretKeys == null) {
@@ -119,6 +124,7 @@ public void initialize(@NonNull Context context) {
119124
}
120125
}
121126

127+
@Override
122128
@NonNull
123129
public String encrypt(@NonNull Context context, @NonNull String plainText) {
124130
synchronized (syncObject) {
@@ -160,12 +166,13 @@ public String encrypt(@NonNull Context context, @NonNull String plainText) {
160166
}
161167
}
162168

169+
@Override
163170
@NonNull
164-
public String decrypt(@NonNull Context context, @NonNull String b64CipherText) {
171+
public String decrypt(@NonNull Context context, @NonNull String cipherText) {
165172
synchronized (syncObject) {
166173
initialize(context);
167174
try {
168-
byte[] cipherTextAndMac = Base64.decode(b64CipherText, Base64.DEFAULT);
175+
byte[] cipherTextAndMac = Base64.decode(cipherText, Base64.DEFAULT);
169176
// get mac, last 32 bytes
170177
int idx = cipherTextAndMac.length - HMAC_SIZE_IN_BYTE;
171178
byte[] mac = Arrays.copyOfRange(cipherTextAndMac, idx, cipherTextAndMac.length);
@@ -219,7 +226,7 @@ private SecretKeys getSecretKeys(@NonNull Context context) {
219226
SecretKey encryptionKey = new SecretKeySpec(
220227
Arrays.copyOfRange(keyBytes, 0, AES_KEY_SIZE_IN_BIT / 8), "AES");
221228
SecretKey integrityKey = new SecretKeySpec(
222-
Arrays.copyOfRange(keyBytes, HMAC_KEY_SIZE_IN_BIT / 8, keyBytes.length), "HmacSHA256");
229+
Arrays.copyOfRange(keyBytes, AES_KEY_SIZE_IN_BIT / 8, keyBytes.length), "HmacSHA256");
223230
return new SecretKeys(encryptionKey, integrityKey);
224231
}
225232

line-sdk/src/main/java/com/linecorp/linesdk/api/internal/LineApiClientImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import android.text.TextUtils;
44

5+
import androidx.annotation.NonNull;
6+
import androidx.annotation.Nullable;
7+
58
import com.linecorp.linesdk.FriendSortField;
69
import com.linecorp.linesdk.GetFriendsResponse;
710
import com.linecorp.linesdk.GetGroupsResponse;
@@ -29,9 +32,6 @@
2932

3033
import java.util.List;
3134

32-
import androidx.annotation.NonNull;
33-
import androidx.annotation.Nullable;
34-
3535
/**
3636
* Implementation of {@link LineApiClient}.
3737
*/

line-sdk/src/main/java/com/linecorp/linesdk/internal/AccessTokenCache.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import android.content.SharedPreferences;
55
import android.text.TextUtils;
66

7-
import com.linecorp.android.security.encryption.EncryptionException;
8-
import com.linecorp.android.security.encryption.StringCipher;
9-
import com.linecorp.linesdk.utils.ObjectUtils;
10-
117
import androidx.annotation.NonNull;
128
import androidx.annotation.Nullable;
139
import androidx.annotation.VisibleForTesting;
1410

11+
import com.linecorp.android.security.encryption.EncryptionException;
12+
import com.linecorp.android.security.encryption.StringCipher;
13+
import com.linecorp.linesdk.utils.ObjectUtils;
14+
1515
/**
1616
* Class to cache {@link InternalAccessToken}.
1717
*/

0 commit comments

Comments
 (0)