I've got an application which will be installed on many devices manually and we want to add a specific AES key for each devices.
My first thought was to send an intent with ADB and decode the key to add it to the application Keystore. But this solution was deem not secure enough by my company because anyone with an adb access can replicate the intent and override the key.
The choosen solution was to create a second application only installed when a setup or a maintenance is done on the device. This app should receive the intent and share it with the main application securely. This second app can be sign with the same KeyStore as the first one.
I once heard the AndroidKeystore is shared between the same signed app and if its true I could simply add the key to the second application Keystore and the first one should be able to use it. Is it true ? If true, how could I use it ?
I've got no success with this method but here's how I store and load AES Keys to keystore in my samples :
/** Create AES key from raw bytes */
fun createKey(raw: ByteArray): Result<SecretKey> = runCatching {
if (raw.size !in AES_SIZES) throw IllegalArgumentException("The key is not an AES key")
SecretKeySpec(raw, ALGORITHM)
}
/**
* Store specified [key] into keystore as [alias]
* #see loadKey to retrieve previously added keys
*/
fun storeKey(alias: String, key: SecretKey): Result<Unit> = runCatching {
val protection = KeyProtection.Builder(PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE_CBC)
.setEncryptionPaddings(ENCRYPTION_PADDING_PKCS7)
.build()
val keystore = getKeystore().getOrThrow()
val secretKeyEntry = KeyStore.SecretKeyEntry(key)
keystore.setEntry(alias, secretKeyEntry, protection)
}
/**
* Search for specified [alias] inside keystore and return the key if found.
* #see storeKey to add a key
*/
fun loadKey(alias: String): Result<SecretKey> = runCatching {
val keyStore = getKeystore().getOrThrow()
val aliases = keyStore.aliases().toList()
if (alias !in aliases) throw IllegalArgumentException("Alias $alias not found")
val entry = keyStore.getEntry(alias, null)
(entry as KeyStore.SecretKeyEntry).secretKey
}
What's the best way to transfer the key otherwise ? Should I use a ContentProvider, a Service or a BroadcastReceiver with a specific permission with protectionLevel="signature".
Related
I know that my question is a duplicate of an existing one, but that question still doesn't have an answer. At the moment I have the following code to generate a key pair.
keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, ANDROID_KEYSTORE)
private fun createKeyPair() {
keyPairGenerator.initialize(
KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
.setKeySize(4096)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.build()
)
keyPairGenerator.generateKeyPair()
}
I chose the RSA algorithm because I need asymmetric encryption (encryption with public key
and decryption using biometrics). However, the key generation freezes ui for a few seconds. I have a progress bar and it freezes when the key pair is generated.
I've tried putting the generation code on a background thread, but that doesn't work and still freeze. I don't understand what I'm doing wrong, because I see similar logic in many apps, but I didn't notice freezes there.
Perhaps I need to use a different encryption algorithm? But the documentation does not say that there are any problems with the RSA algorithm and I don't understand how to rewrite this code and encryption code using ECC algorithm as advised in mentioned answer
here is my encryption code:
private const val ALGORITHM = "RSA"
private var cipher = Cipher.getInstance(TRANSFORMATION)
private fun initCipherForEncrypt() {
if (!keyStoreInstance.containsAlias(KEY_ALIAS)) {
keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, ANDROID_KEYSTORE)
createKeyPair()
}
val key: PublicKey = keyStoreInstance.getCertificate(KEY_ALIAS).publicKey
val unrestricted: PublicKey = KeyFactory.getInstance(key.algorithm).generatePublic(X509EncodedKeySpec(key.encoded))
val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec("SHA-1"), PSource.PSpecified.DEFAULT)
cipher.init(Cipher.ENCRYPT_MODE, unrestricted, spec)
}
fun encrypt(token: String): ByteArray {
initCipherForEncrypt()
return cipher.doFinal(token.toByteArray())
}
I have a library that has default ways to encrypt stuff that it uses, which means user just need to provide a string as a key.
lib.encryption("key")
lib.doEncryptedStuff() // use default encryption
I want to allow users use custom encryption, including those that implement custom java crypto providers. My question is, what should be the correct way to ask user for such encryption methods? For now I came with idea to request two ciphers - one for encryption and one for decryption.
// create and init() ciphers here
lib.encryption(cipherEncrypt, cipherDecrypt)
lib.doEncryptedStuff() // use custom ciphers
But I'm unsure whether this is the correct way. Are Ciphers enough? Or should I provide interface for encryption for user to implement? I remember there being issues with reusing IV, which means cipher re-initialization?
Another issue that I'm facing is that it's really hard for me to generalize interface. Like AES with ECB will work with encrypt()/decrypt() methods, but with CBC it requires IV, which could also be stored inside encrypted data. What a mess.
Seems like I've nailed it. I used Stream-like approach, but not tied to streams. It allows to use any encryption user needs by passing interface as parameter.
My interface:
interface Encryption
{
fun encrypt(
read: (buffer: ByteArray, count: Int) -> Int,
write: (buffer: ByteArray, count: Int) -> Unit
)
fun decrypt(
read: (buffer: ByteArray, count: Int) -> Int,
write: (buffer: ByteArray, count: Int) -> Unit
)
}
Example implementation:
// init cipher and buffer
val cipher = Cipher.getInstance(MODE)
val buffer = ByteArray(BLOCK_SIZE)
cipher.init(Cipher.ENCRYPT_MODE, keystore.getKey(KEY_ALIAS, null))
// write iv (when decrypting read IV from beginning)
val iv = cipher.iv
write(iv, iv.size)
// write data
var count = 0
while (true)
{
count = read(buffer, BLOCK_SIZE)
if (count < BLOCK_SIZE)
break
val encrypted = cipher.update(buffer, 0, count)
write(encrypted, encrypted.size)
}
// flush remains
if (count > 0)
{
val final = cipher.doFinal(buffer, 0, count)
write(final, final.size)
}
Example usage (input and output are streams):
encryption.encrypt(
read = { buffer, count ->
input.read(buffer, 0, count)
},
write = { buffer, count ->
output.write(buffer, 0, count)
}
)
Im investigating the use of com.google.crypto.tink:tink-android:1.6.1 in my current Android project.
The data I am encrypting includes the OAuth2 Access Token/Refresh Token I employ for my remote API calls, e.g. Access Token is my Bearer token for the Authorisation HTTP header.
Im concerned I have made an error in my encryption/decryption logic as I am experiencing an intermittent problem where I cannot Refresh the Token. The error from the server
{"error_description":"unknown, invalid, or expired refresh token","error":"invalid_grant"}
The refresh token cannot be expired as it lasts 24 hours.
My code that initialises Tink resembles this:-
private fun manageTink() {
try {
AeadConfig.register()
authenticatedEncryption = generateNewKeysetHandle().getPrimitive(Aead::class.java)
} catch (e: GeneralSecurityException) {
throw RuntimeException(e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
#Throws(IOException::class, GeneralSecurityException::class)
private fun generateNewKeysetHandle(): KeysetHandle =
AndroidKeysetManager
.Builder()
.withSharedPref(this, TINK_KEYSET_NAME, PREF_FILE_NAME)
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withMasterKeyUri(MASTER_KEY_URI)
.build()
.keysetHandle
Here is my code for encryption/decryption
import android.util.Base64
import com.google.crypto.tink.Aead
import javax.inject.Inject
const val BASE64_ENCODE_SETTINGS = Base64.NO_WRAP or Base64.NO_PADDING
data class Security #Inject constructor(private val authenticatedEncryption: Aead) {
fun conceal(plainText: String, associatedData: String): String {
val plain64 = Base64.encode(plainText.encodeToByteArray(), BASE64_ENCODE_SETTINGS)
val associated64 = Base64.encode(associatedData.encodeToByteArray(), BASE64_ENCODE_SETTINGS)
val encrypted: ByteArray? = authenticatedEncryption.encrypt(plain64, associated64)
return Base64.encodeToString(encrypted, BASE64_ENCODE_SETTINGS)
}
fun reveal(encrypted64: String, associatedData: String): String {
val encrypted = Base64.decode(encrypted64.encodeToByteArray(), BASE64_ENCODE_SETTINGS)
val associated64 = Base64.encode(associatedData.encodeToByteArray(), BASE64_ENCODE_SETTINGS)
val decrypted: ByteArray? = authenticatedEncryption.decrypt(encrypted, associated64)
return String(Base64.decode(decrypted, BASE64_ENCODE_SETTINGS))
}
}
Could the use of Base64 encode/decode be the issue?
Where is my mistake?
If Tink can decrypt your token, the issue should be with your token/the encoding and not with Tink (It is using authenticated encryption, so you are guaranteed that the bytes that you encrypted are going to be the bytes that you decrypt, or you get an error).
To also address the concern in the comment: Tink will happily encrypt and decrypt any byte strings (with the associated data also being allowed to be any byte string), and not be concerned with whether they are printable or even valid Unicode. You will have to base64 encode the output of Tink if you want to use it as a string, though, as it will be uniformly random distributed bytes, i.e. contain invalid Unicode and non printable characters with high probability.
The same code works in runtime and doesnt work in test
There is such code
private fun generatePrivateKeyFromText(key: String): Key {
val kf = KeyFactory.getInstance("RSA")
val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.decodeBase64(key))
return kf.generatePrivate(keySpecPKCS8)
}
When I run or debug app it works ok, but this code fails on generatePrivate while testing
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
#Test
fun decrypt() {
val encrypt = "MoRxCpLJNqxfXGVeU73zZFi+X2j2TLUTyIn1XRqCoEfeN8rNBR/YrEtumAz+8/0AaEsvx0+qTilfbw+edZd8Wfum4McWQ8oWXifvWLgoXybhxWUmCdi2fwA9Gw0parY6CSNYUDA2UuLrLLaDGMz/Jj4s4XmXKp5zuec1zXVdrPM="
val prkey = "MIICXAIBAAKBgQCAaTCQOxAZPfTXfgel2MMPUCLAf32TxfsXu71/c3mVFGtDPB+7IpmxCmEvAv6nlq1ymX1zRR5gIzNt4DZoM0RhE58eoksUXcmFcRnMX5V4bnI8DitHLdB2EZzdvnPX0Umzs+tE7I1ouDIocNQRsEoQeDcNPfz5av2zMPsg0Xl/yQIDAQABAoGAV8UOX5cvQsGZZ+2J1q8ZbI8OodrCf83z+V3mgYXxVZe2VSd0XNmiiWMZ2CNI4k3YUhtdpvtYbsfAsFpvdbuNAXMW82Zwsd0oluPzzoLELGkFvaUJlh2YGmizrBnEwvF0usJYwjsjUbXw3o1xKX8ILk5FBfdr2+L65YIIZ0UhqoECQQD/B0P8iZhoOTx7myhwlFCuVeSadwaOMGy2CgXRLvTFAtstz8YVO+D+yPKsEpAvMlFgEnkTt7tl8DRxMpKnfmO5AkEAgOZudjUD72xWfSJpCEcX76WGSASWE+fLCZ8C/HumIZ+0trW5/bsmBrI/6SldEJcy4b2bHh2nOggC/6R5rEUkkQJAAg779IDj0wuLOnAxLl90G0QkOT72tZUce4evLlYTsbdpL4B619cI5OWYV906frcIQx9DDO6xu4vp0HQZDPMPOQJAOVbH8ntY2ctmmdmRwWXmpusJ1cV8gTROJGSArpHOcAycFd628sCqhLYMKgsFZBjuQG7YrsfgGLdxpgijO1eykQJBAOE8+BrQwFWyOcgnUShPHo8mDOBkeplGr9VZdnWktac2aBr1Biovy+pipUyjSCAaFgOsSU0FDcK0I5ulTOpgMRg="
val decrypt = CryptoService.decrypt(encrypt, prkey)
assertEquals("Pika-pika", decrypt)
}
fun decrypt(ciphertext: String, key: String): String {
var decodedBytes: ByteArray? = null
try {
val c = Cipher.getInstance("RSA")
c.init(Cipher.DECRYPT_MODE, generatePrivateKeyFromText(key))
decodedBytes = c.doFinal(Base64.decodeBase64(ciphertext))
} catch (e: Exception) {
Log.e("Crypto", "RSA decryption error: $e")
}
return String(decodedBytes ?: ByteArray(0))
}
Working function is in Fragment
private fun testCrypto() {
val encrypt = "MoRxCpLJNqxfXGVeU73zZFi+X2j2TLUTyIn1XRqCoEfeN8rNBR/YrEtumAz+8/0AaEsvx0+qTilfbw+edZd8Wfum4McWQ8oWXifvWLgoXybhxWUmCdi2fwA9Gw0parY6CSNYUDA2UuLrLLaDGMz/Jj4s4XmXKp5zuec1zXVdrPM="
val prkey = "MIICXAIBAAKBgQCAaTCQOxAZPfTXfgel2MMPUCLAf32TxfsXu71/c3mVFGtDPB+7IpmxCmEvAv6nlq1ymX1zRR5gIzNt4DZoM0RhE58eoksUXcmFcRnMX5V4bnI8DitHLdB2EZzdvnPX0Umzs+tE7I1ouDIocNQRsEoQeDcNPfz5av2zMPsg0Xl/yQIDAQABAoGAV8UOX5cvQsGZZ+2J1q8ZbI8OodrCf83z+V3mgYXxVZe2VSd0XNmiiWMZ2CNI4k3YUhtdpvtYbsfAsFpvdbuNAXMW82Zwsd0oluPzzoLELGkFvaUJlh2YGmizrBnEwvF0usJYwjsjUbXw3o1xKX8ILk5FBfdr2+L65YIIZ0UhqoECQQD/B0P8iZhoOTx7myhwlFCuVeSadwaOMGy2CgXRLvTFAtstz8YVO+D+yPKsEpAvMlFgEnkTt7tl8DRxMpKnfmO5AkEAgOZudjUD72xWfSJpCEcX76WGSASWE+fLCZ8C/HumIZ+0trW5/bsmBrI/6SldEJcy4b2bHh2nOggC/6R5rEUkkQJAAg779IDj0wuLOnAxLl90G0QkOT72tZUce4evLlYTsbdpL4B619cI5OWYV906frcIQx9DDO6xu4vp0HQZDPMPOQJAOVbH8ntY2ctmmdmRwWXmpusJ1cV8gTROJGSArpHOcAycFd628sCqhLYMKgsFZBjuQG7YrsfgGLdxpgijO1eykQJBAOE8+BrQwFWyOcgnUShPHo8mDOBkeplGr9VZdnWktac2aBr1Biovy+pipUyjSCAaFgOsSU0FDcK0I5ulTOpgMRg="
val decrypt = CryptoService.decrypt(encrypt, prkey)
println(decrypt) // "Pika-pika"
}
I call it on onViewCreated
Updated:
I added BC provider (thanks, #JamesKPolk)
private fun generatePrivateKeyFromText(key: String): Key {
Security.addProvider(BouncyCastleProvider())
val kf = KeyFactory.getInstance(algorithm)
val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.decodeBase64(key))
return kf.generatePrivate(keySpecPKCS8)
}
But it is still ok in runtime and not while testing
javax.crypto.BadPaddingException: Decryption error
So problem for different running code didnt go.
What the difference between runtime and test which crashes code?
The issue is that the private key is not a PKCS8EncodedKeySpec, but rather an RSAPrivateKey object from PKCS#1. The BC provider, however, will still decode this mistake without complaint. However, other providers will rightfully complain. My guess is that the runtime is using an older version of Android where the default provider is BC, but your test is using a newer version where that isn't the case.
The fix is to make your private key a proper PKCS8EncodedKeySpec. Alternatively, you can explicitly request the "BC" provider. To do so, you need to specify "BC" in the getInstance() call: val keyFactory = KeyFactory.getInstance("RSA", "BC")
However, note that it appears that BC provider support is on its way out.
To convert a private key in the PKCS#1 format, either wrap a 'BEGIN RSA PRIVATE KEY'-style header and footer around the base64 blob or decode the base64 blob and place that in a file, then run:
openssl pkcs8 -topk8 -in privkey.pem -outform der -nocrypt | openssl base64 -A
or
openssl pkcs8 -topk8 -in privkey.der -inform der -nocrypt | openssl base64 -A
A second issue comes from relying on defaults. Instead of doing
val c = Cipher.getInstance("RSA")
which gets you defaults for mode and padding and thus is non-portable, always specify the full "algorithm/mode/padding" transformation string to Cipher.getInstance(). In you case, it appears the data is not padded (an insecure mode) you'd need something like
val c = Cipher.getInstance("RSA/ECB/NoPadding")
However, you really should use proper randomized padding, and currently that is OAEP padding.
Summary
The runtime environment is Android, but I think the test environment is Oracle Java (or maybe openjdk). There are evidently two critical differences in those environments:
Android uses the BC provider for KeyFactory which will handle private keys encoded in PKCS#1 RSAPrivateKey format. Oracle Java only supports PKCS8 encoded keys.
In Android, Cipher.getInstance("RSA") defaults to Cipher.getInstance("RSA/ECB/NoPadding"), but Oracle Java defaults to Cipher.getInstance("RSA/ECB/PKCS1Padding")
I am trying to make use of the twitter api, and am setting up a handler to deal with twitter api requests.
To do this I am using an HTTPUrlConnection and following the Twitter api docs
I've managed to get authenticated using the 3-legged OAuth process, but am stuck when actually trying to make requests with the oauth token.
Here is an example of what my auth headers look like:
Accept=*/*,
Connection=close,
User-Agent=OAuth gem v0.4.4,
Content-Type=application/x-www-form-urlencoded,
Authorization=
OAuth oauth_consumer_key=****&
oauth_nonce=bbmthpoiuq&
oauth_signature=*****%3D&
oauth_signature_method=HMAC-SHA1&
oauth_timestamp=1570586135&
oauth_token=*****&
oauth_version=1.0,
Host=api.twitter.com
for each header in the auth header I add it to my HTTP GET call like this:
urlConnection.setRequestProperty(header.key, header.value)
Note that Authorization points to one string
OAuth oauth_consumer_key=****&oauth_nonce=bbmthpoiuq&oauth_signature=*****%3Doauth_signature_method=HMAC-SHA1oauth_timestamp=1570586135&oauth_token=*****&oauth_version=1.0,Host=api.twitter.com
The following params are collected as follows:
oauth_consumer_key is my application API key
oauth_signature is computed by the hmacSign function below
oauth_token is the "oauth_token" received in the response from /oauth/access_token
The hmacSign function:
private fun hmacSign(requestType: RequestType, url: String, params: Map<String, String>): String {
val type = "HmacSHA1"
val key = "$API_SECRET&$tokenSecret"
val value = makeURLSafe("${requestType.string}&$url${getURLString(params.toList().sortedBy { it.first }.toMap())}")
val mac = javax.crypto.Mac.getInstance(type)
val secret = javax.crypto.spec.SecretKeySpec(key.toByteArray(), type)
mac.init(secret)
val digest = mac.doFinal(value.toByteArray())
return makeURLSafe(Base64.encodeToString(digest, NO_WRAP))
}
private fun makeURLSafe(url: String) : String {
return url.replace("/", "%2F")
.replace(",", "%2C")
.replace("=", "%3D")
.replace(":", "%3A")
.replace(" ", "%2520")
}
protected fun getURLString(params: Map<String, Any>) : String {
if (params.isEmpty()) return ""
return params.toList().fold("?") { total, current ->
var updated = total
updated += if (total == "?")
"${current.first}=${current.second}"
else
"&${current.first}=${current.second}"
updated
}
}
In the GET call I'm referring to, tokenSecret would be the oauth secret received from /oauth/access_token
After i make the call I get a 400: Bad Request
Is there anything obvious I'm doing wrong?
Update: By putting the params at the end of the url like ?key=value&key2=value2... instead of a 400 I get a 401. So I'm not sure which is worse, or which is the right way to do it.
Okay, finally got it working
So using the suggestion in the comments, I downloaded postman and copied all my info into postman - when i made the request there, I got a 200!
So then i looked back and tried to figure out what was different and there were a few things:
First the hmac sign function was missing an &
New function (added another & after the url):
private fun hmacSign(requestType: RequestType, url: String, params: Map<String, String>): String {
val type = "HmacSHA1"
val key = "$API_SECRET&$tokenSecret"
val value = makeURLSafe("${requestType.string}&$url&${getURLString(params.toList().sortedBy { it.first }.toMap())}")
val mac = javax.crypto.Mac.getInstance(type)
val secret = javax.crypto.spec.SecretKeySpec(key.toByteArray(), type)
mac.init(secret)
val digest = mac.doFinal(value.toByteArray())
return makeURLSafe(Base64.encodeToString(digest, NO_WRAP))
}
Next I noticed my auth header had its params seperated with & but they all should've been replaced with , - i also needed to surround each of my values in ""
So it looked like:
OAuth oauth_consumer_key="****",oauth_nonce="bbmthpoiuq",oauth_signature="*****%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1570586135",oauth_token="*****",oauth_version="1.0",Host="api.twitter.com"
After these changes i started getting 200!
Hopefully this helps someone else (though im sure its unlikely considering how specific these issues were)