Last active
February 28, 2024 15:42
-
-
Save Hayk985/d5e6c3dcaa6ae37a7c6eeba3c4f9758c to your computer and use it in GitHub Desktop.
Key Wrapping example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.security.keystore.KeyProperties | |
import android.util.Base64 | |
import java.security.Key | |
import java.security.SecureRandom | |
import javax.crypto.Cipher | |
import javax.crypto.spec.IvParameterSpec | |
import javax.crypto.spec.SecretKeySpec | |
import kotlin.jvm.Throws | |
// Source - https://medium.com/@hayk.mkrtchyan8998/shedding-light-on-android-encryption-android-crypto-api-part-2-cipher-147ff4411e1d | |
interface KeyWrapper { | |
/** | |
* Get the key that is saved in a storage or create a new one if it doesn't exist. | |
* Note: Wrapping logic is already handled by this function. | |
* @return - The key | |
*/ | |
@Throws(Exception::class) | |
fun getOrCreateKey(): Key | |
} | |
class KeyWrapperImpl : KeyWrapper { | |
/** | |
* The key length should be 16, 24 or 32 characters long | |
* - 16 -> AES-128 | |
* - 24 -> AES-192 | |
* - 32 -> AES-256 | |
* @see <a href="https://en.wikipedia.org/wiki/Advanced_Encryption_Standard">AES Algorithm</a> | |
*/ | |
private val baseKeyMaterial = "keep_this_key_sc".toByteArray(Charsets.UTF_8) | |
/** | |
* Out initial key, using which we will wrap and unwrap other key. | |
* wrapKey and unwrapKey functions accept initialKey as an argument. | |
* It's better to keep it outside of this class to make this class more generic and reusable. | |
* Note: You can also use KeyGenerator to generate a key. It's out of this lesson scope. | |
* I will tell about it in the next part. | |
*/ | |
private val initialKey = SecretKeySpec(baseKeyMaterial, AES_ALGORITHM) | |
/** | |
* Function that wraps the key with another key | |
* @param initialKey - The key that will be used to wrap the other key | |
* @param keyToBeWrapped - The key that will be wrapped | |
* @return - The encrypted key data | |
*/ | |
@Throws(Exception::class) | |
private fun wrapKey(initialKey: Key, keyToBeWrapped: Key): ByteArray { | |
val cipher = Cipher.getInstance(TRANSFORMATION) | |
cipher.init(Cipher.WRAP_MODE, initialKey) | |
val encryptedKey = cipher.wrap(keyToBeWrapped) | |
val iv = cipher.iv | |
val encryptedDataWithIV = ByteArray(iv.size + encryptedKey.size) | |
System.arraycopy(iv, 0, encryptedDataWithIV, 0, iv.size) | |
System.arraycopy(encryptedKey, 0, encryptedDataWithIV, iv.size, encryptedKey.size) | |
return encryptedDataWithIV | |
} | |
/** | |
* Function that unwraps the key. Use the same initial key that was used to wrap the key. | |
* @param initialKey - The key that will be used to wrap the other key | |
* @param wrappedKey - The encrypted key data | |
* @return - The unwrapped key | |
*/ | |
@Throws(Exception::class) | |
private fun unwrapKey(initialKey: Key, wrappedKey: ByteArray): Key { | |
val cipher = Cipher.getInstance(TRANSFORMATION) | |
val iv = wrappedKey.copyOfRange(0, cipher.blockSize) | |
val encryptedData = wrappedKey.copyOfRange(cipher.blockSize, wrappedKey.size) | |
cipher.init(Cipher.UNWRAP_MODE, initialKey, IvParameterSpec(iv)) | |
return cipher.unwrap(encryptedData, AES_ALGORITHM, Cipher.SECRET_KEY) | |
} | |
/** | |
* Creates a new key. You can also use KeyGenerator to generate a key. | |
* @param keyMaterial - The material for the key | |
* @return - The newly created key | |
*/ | |
@Throws(IllegalArgumentException::class) | |
private fun createKey(keyMaterial: ByteArray): Key { | |
return SecretKeySpec(keyMaterial, AES_ALGORITHM) | |
} | |
/** | |
* Generates a random key material. This adds an extra layer of security as the material is random. | |
*/ | |
private fun generateRandomKeyMaterial(): ByteArray { | |
val random = SecureRandom() | |
val key = ByteArray(16) | |
random.nextBytes(key) | |
return key | |
} | |
@Throws(Exception::class) | |
override fun getOrCreateKey(): Key { | |
//Retrieve the key from where you have persisted it. Currently setting to null for demonstration purposes | |
val existingKeyFromStorage: String? = null | |
return existingKeyFromStorage?.let { | |
// If the key is found in the a storage, we unwrap and return it | |
unwrapKey( | |
initialKey = initialKey, | |
wrappedKey = Base64.decode(it, Base64.DEFAULT), | |
) | |
} ?: run { | |
/** | |
* Key does not exist in local storage. Maybe it's our first run or the user cleared the app data | |
* Create a new key, wrap it, save in local storage and return it | |
* NOTE - If you have some encrypted data with your generated key, but you have lost it, | |
* you won't be able to decrypt it. So make sure to save it somewhere. | |
*/ | |
val key = createKey(generateRandomKeyMaterial()) | |
val encryptedKey = wrapKey( | |
initialKey = initialKey, | |
keyToBeWrapped = key, | |
) | |
// Persist this somewhere | |
Base64.encodeToString(encryptedKey, Base64.DEFAULT) | |
key | |
} | |
} | |
companion object { | |
private const val AES_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES | |
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC | |
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 | |
private const val TRANSFORMATION = "$AES_ALGORITHM/$BLOCK_MODE/$PADDING" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment