Skip to content

Commit

Permalink
Merge pull request #7 from evervault/jake/etr-1068-update-android-sdk
Browse files Browse the repository at this point in the history
Add decrypt function
  • Loading branch information
jakekgrog authored Aug 29, 2023
2 parents 8c7c41d + b6fa9a7 commit 1661080
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
local.properties

### IntelliJ IDEA ###
.idea/**/*
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ val encryptedPassword = Evervault.shared.encrypt("Super Secret Password")

The `encrypt` method returns an `Any` type, so you will need to safely cast the result based on the data type you provided. For Boolean, Numerics, and Strings, the encrypted data is returned as a String. For Arrays, Lists and Maps, the encrypted data maintains the same structure but is encrypted (except that Arrays become Lists). For ByteArray, the encrypted data is returned as encrypted ByteArray, which can be useful for encrypting files.

### Decrypting Data

You can use the `decrypt` method to decrypt data previously encrypted through Evervault. To perform decryptions you will be required to provide a Client Side Token. The token is a time bound token for decrypting data. The token can be generated using our backend SDKs for use in our client-side SDKs. The payload provided to the `decrypt` method must be the same as the payload used to generate the token.


Here's an example of decrypting data.

```kotlin
val encrypted = Evervault.shared.encrypt("John Doe")

val decrypted = Evervault.shared.decrypt("<CLIENT_SIDE_TOKEN>", mapOf("name" to encrypted)) as Map<String, Any>
println(decrypted["name"]) // Prints "John Doe"
```

The `decrypt` function will return `Any`, however this can be cast to `Map<String, Any>`. The data argument must be a map.

## Sample App

The Evervault Kotlin Multiplatform SDK Package includes a sample app, located in the `examples` directory. The sample app consist of a `shared` module, which contains the Evervault Kotlin Multiplatform SDK, and an `android` module, which contains the sample app.
Expand Down
3 changes: 3 additions & 0 deletions evervault-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ kotlin {

// JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")

implementation("com.google.code.gson:gson:2.8.7")
}
}

val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
implementation("com.google.code.gson:gson:2.8.7")
}
}

Expand Down
11 changes: 8 additions & 3 deletions evervault-core/src/commonMain/kotlin/com/evervault/sdk/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.evervault.sdk
import com.evervault.sdk.core.keys.CageKey

private const val KEYS_URL = "https://keys.evervault.com"
private const val API_URL = "https://api.evervault.com"

private val DEBUG_KEY = CageKey(
ecdhP256Key = "Al1/Mo85D7t/XvC3I+YYpJvP+OsSyxIbSrhtDhg1SClQ",
Expand All @@ -23,7 +24,7 @@ internal data class Config(
teamId = teamId,
appId = appId,
encryption = EncryptionConfig(publicKey),
httpConfig = HttpConfig(keysUrl = configUrls.keysUrl)
httpConfig = HttpConfig(keysUrl = configUrls.keysUrl, apiUrl = configUrls.apiUrl)
)
}

Expand All @@ -36,7 +37,11 @@ data class ConfigUrls(
/**
* The URL for the custom keys endpoint. Default is the Evervault keys URL.
*/
var keysUrl: String = KEYS_URL
var keysUrl: String = KEYS_URL,
/**
* The URL for the API.
*/
var apiUrl: String = API_URL,
)

internal data class EncryptionConfig(
Expand All @@ -53,4 +58,4 @@ internal data class EncryptionConfig(
data class Header(val iss: String, val version: Int)
}

internal data class HttpConfig(var keysUrl: String)
internal data class HttpConfig(var keysUrl: String, var apiUrl: String)
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,34 @@ class Evervault private constructor() {
return client.encrypt(data)
}

/**
* Decrypts data previously encrypted with the `encrypt()` function or through Relay.
*
* @param token The token used to decrypt the data.
* @param data The encrypted data that's to be decrypted. Must be in the form of a map e.g { "data": encryptedData }
* @return The decrypted data. The data is a `Map<String, Any>` and will need to be cast. See below for an example
* @throws EvervaultException.InitializationError If the encryption process fails.
*
* ## Declaration
* ```kotlin
* suspend fun decrypt(token: String, data: Any): Any
* ```
*
* ## Example
* ```kotlin
* val decrypted = Evervault.shared.decrypt("token1234567890", encryptedData) as Map<String, Any>
* ```
*
* The `decrypt()` function allows you to decrypt previously encrypted data using a token and attempt to deserialize it to the parameterized type. The token is a single use, time bound token for decrypting data.
*
* Tokens will only last for 5 minutes and must be used with the same payload that was used to create the token.
*
* The function returns the decrypted data as `Map<String, Any>`, and the caller is responsible for safely casting the result based on the original data type.
*/
suspend fun decrypt(token: String, data: Any): Any{
val client = client ?: throw EvervaultException.InitializationError
return client.decryptWithToken(token, data)
}

companion object {
/**
Expand Down Expand Up @@ -146,6 +174,10 @@ internal class Client(private val config: Config, private val http: Http, privat

return handlers.encrypt(data)
}

suspend fun decryptWithToken(token: String, data: Any): Any {
return http.decryptWithToken(token, data)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import com.evervault.sdk.HttpConfig
import com.evervault.sdk.core.keys.CageKey

internal class Http(
private val keysLoader: HttpKeysLoader
private val keysLoader: HttpKeysLoader,
private val httpRequest: HttpRequest
) {

constructor(config: HttpConfig, teamId: String, appId: String, context: String): this(
HttpKeysLoader("${config.keysUrl}/$teamId/apps/$appId?context=$context")
HttpKeysLoader("${config.keysUrl}/$teamId/apps/$appId?context=$context"),
HttpRequest(config)
)

suspend fun loadKeys(): CageKey {
return keysLoader.loadKeys()
}

suspend fun decryptWithToken(token: String, data: Any): Any {
return httpRequest.decryptWithToken(token, data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.evervault.sdk.core

import com.evervault.sdk.HttpConfig
import io.ktor.client.HttpClient
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.post
import io.ktor.client.request.header
import io.ktor.client.request.headers
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.Json
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

internal class HttpRequest(private var config: HttpConfig) {
private var activeTask: kotlinx.coroutines.Deferred<Any>? = null

private var httpClient = HttpClient {
defaultRequest {
header(HttpHeaders.ContentType, "application/json")
header(HttpHeaders.UserAgent, "Evervault/Kotlin")
}
}

private val json = Json { ignoreUnknownKeys = true }

suspend fun decryptWithToken(token: String, data: Any): Any {
activeTask?.let {
val res = it.await()
return res
}

val task = coroutineScope{
async {
try {
val result = executeDecryptWithToken(token, data)
activeTask = null
return@async result
} catch (error: Error) {
activeTask = null
throw error
}
}
}

activeTask = task

return task.await()
}

private suspend fun executeDecryptWithToken(token: String, payload: Any): Any {
val data = Gson().toJson(payload)
val response: HttpResponse = httpClient.post("${config.apiUrl}/decrypt") {
setBody(data)
headers {
append("Authorization", "Token ${token}")
}
}

if (response.status != HttpStatusCode.OK) {
throw Error("Failed to decrypt data. Status code: ${response.status}")
}

val responseBody = response.bodyAsText()
val type: Type = object : TypeToken<Map<String, Any>>() {}.type
val map: Map<String, Any> = Gson().fromJson(responseBody, type)
return map
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ class HttpKeysLoaderTest {
@Test
fun testLoadKeys() = runBlocking {
val http = Http(
config = HttpConfig(keysUrl = ConfigUrls().keysUrl),
config = HttpConfig(
keysUrl = ConfigUrls().keysUrl,
apiUrl = ConfigUrls().apiUrl
),
teamId = getenv("VITE_EV_TEAM_UUID"),
appId = getenv("VITE_EV_APP_UUID"),
context = "default"
)
val cageKey = http.loadKeys()

assertEquals(
cageKey,
CageKey(publicKey = cageKey.ecdhP256KeyUncompressed, isDebugMode = cageKey.isDebugMode)
Expand Down
Loading

0 comments on commit 1661080

Please sign in to comment.