diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d94dd50..dfb5c98 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip diff --git a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Unlock.kt b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Unlock.kt index 654bc0c..980c851 100644 --- a/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Unlock.kt +++ b/tanker-bindings/src/androidTest/kotlin/io/tanker/api/Unlock.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import io.tanker.api.admin.TankerAppUpdateOptions import io.tanker.api.errors.InvalidArgument +import io.tanker.api.errors.InvalidVerification +import io.tanker.api.errors.PreconditionFailed import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request @@ -21,11 +23,22 @@ class UnlockTests : TankerSpec() { @Before fun beforeTest() { identity = tc.createIdentity() - tanker1 = Tanker(options.setPersistentPath(createTmpDir().toString()).setCachePath(createTmpDir().toString())) - tanker2 = Tanker(options.setPersistentPath(createTmpDir().toString()).setCachePath(createTmpDir().toString())) + tanker1 = Tanker( + options.setPersistentPath(createTmpDir().toString()) + .setCachePath(createTmpDir().toString()) + ) + tanker2 = Tanker( + options.setPersistentPath(createTmpDir().toString()) + .setCachePath(createTmpDir().toString()) + ) } - private fun checkSessionToken(publicIdentity: String, token: String, allowedMethod: String, value: String? = null): String { + private fun checkSessionToken( + publicIdentity: String, + token: String, + allowedMethod: String, + value: String? = null + ): String { val jsonMapper = ObjectMapper() val jsonAllowedMethod = jsonMapper.createObjectNode() jsonAllowedMethod.put("type", allowedMethod) @@ -48,12 +61,12 @@ class UnlockTests : TankerSpec() { val url = tc.trustchaindUrl() val request = Request.Builder() - .url("$url/verification/session-token") - .post(jsonBody.toRequestBody(HttpClient.JSON)) - .build() + .url("$url/verification/session-token") + .post(jsonBody.toRequestBody(HttpClient.JSON)) + .build() val response = OkHttpClient().newCall(request).execute() if (!response.isSuccessful) - throw RuntimeException("Check session token request failed: "+response.body?.string()) + throw RuntimeException("Check session token request failed: " + response.body?.string()) val jsonResponse = jsonMapper.readTree(response.body?.string()) return jsonResponse.get("verification_method").asText() } @@ -147,7 +160,13 @@ class UnlockTests : TankerSpec() { val verificationCode = tc.getSMSVerificationCode(phoneNumber) tanker1.registerIdentity(PhoneNumberVerification(phoneNumber, verificationCode)).get() val methods = tanker1.getVerificationMethods().get() - assertThat(methods).containsExactlyInAnyOrderElementsOf(listOf(PhoneNumberVerificationMethod(phoneNumber))) + assertThat(methods).containsExactlyInAnyOrderElementsOf( + listOf( + PhoneNumberVerificationMethod( + phoneNumber + ) + ) + ) tanker1.stop().get() } @@ -210,8 +229,8 @@ class UnlockTests : TankerSpec() { val martineIdentity = tc.createIdentity(martineConfig.email) val appOptions = TankerAppUpdateOptions() - .setOidcClientId(oidcConfig.clientId) - .setOidcClientProvider(oidcConfig.provider) + .setOidcClientId(oidcConfig.clientId) + .setOidcClientProvider(oidcConfig.provider) tc.admin.appUpdate(tc.id(), appOptions) // Get a fresh OIDC ID token from GOOG @@ -224,9 +243,9 @@ class UnlockTests : TankerSpec() { val jsonBody = jsonMapper.writeValueAsString(jsonObj) val request = Request.Builder() - .url("https://www.googleapis.com/oauth2/v4/token") - .post(jsonBody.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()!!)) - .build() + .url("https://www.googleapis.com/oauth2/v4/token") + .post(jsonBody.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()!!)) + .build() val response = OkHttpClient().newCall(request).execute() if (!response.isSuccessful) throw java.lang.RuntimeException("Google OAuth test request failed!") @@ -320,7 +339,8 @@ class UnlockTests : TankerSpec() { assertThat(notToken).isNull() var verificationCode = tc.getEmailVerificationCode(email) - val token = tanker1.setVerificationMethod(EmailVerification(email, verificationCode), options).get() + val token = + tanker1.setVerificationMethod(EmailVerification(email, verificationCode), options).get() assertThat(token).isNotBlank val publicIdentity = Identity.getPublicIdentity(identity) @@ -342,7 +362,10 @@ class UnlockTests : TankerSpec() { assertThat(notToken).isNull() var verificationCode = tc.getSMSVerificationCode(phoneNumber) - val token = tanker1.setVerificationMethod(PhoneNumberVerification(phoneNumber, verificationCode), options).get() + val token = tanker1.setVerificationMethod( + PhoneNumberVerification(phoneNumber, verificationCode), + options + ).get() assertThat(token).isNotBlank val publicIdentity = Identity.getPublicIdentity(identity) @@ -432,12 +455,12 @@ class UnlockTests : TankerSpec() { tanker1.start(identity).get() tanker1.registerIdentity(PassphraseVerification(pass)).get() assertThat(tanker1.getVerificationMethods().get()).containsExactly( - PassphraseVerificationMethod + PassphraseVerificationMethod ) tanker1.setVerificationMethod(PreverifiedEmailVerification(email)).get() assertThat(tanker1.getVerificationMethods().get()).containsExactlyInAnyOrder( - PreverifiedEmailVerificationMethod(email), PassphraseVerificationMethod + PreverifiedEmailVerificationMethod(email), PassphraseVerificationMethod ) tanker2.start(identity).get() @@ -446,7 +469,7 @@ class UnlockTests : TankerSpec() { assertThat(tanker2.getStatus()).isEqualTo(Status.READY) assertThat(tanker1.getVerificationMethods().get()).containsExactlyInAnyOrder( - EmailVerificationMethod(email), PassphraseVerificationMethod + EmailVerificationMethod(email), PassphraseVerificationMethod ) tanker1.stop().get() @@ -464,12 +487,12 @@ class UnlockTests : TankerSpec() { tanker1.start(identity).get() tanker1.registerIdentity(PassphraseVerification(pass)).get() assertThat(tanker1.getVerificationMethods().get()).containsExactly( - PassphraseVerificationMethod + PassphraseVerificationMethod ) tanker1.setVerificationMethod(PreverifiedPhoneNumberVerification(phoneNumber)).get() assertThat(tanker1.getVerificationMethods().get()).containsExactlyInAnyOrder( - PreverifiedPhoneNumberVerificationMethod(phoneNumber), PassphraseVerificationMethod + PreverifiedPhoneNumberVerificationMethod(phoneNumber), PassphraseVerificationMethod ) tanker2.start(identity).get() @@ -478,10 +501,104 @@ class UnlockTests : TankerSpec() { assertThat(tanker2.getStatus()).isEqualTo(Status.READY) assertThat(tanker1.getVerificationMethods().get()).containsExactlyInAnyOrder( - PhoneNumberVerificationMethod(phoneNumber), PassphraseVerificationMethod + PhoneNumberVerificationMethod(phoneNumber), PassphraseVerificationMethod ) tanker1.stop().get() tanker2.stop().get() } + + @Test + fun test_register_e2e_passphrase() { + val passphrase = "mangerbouger.fr" + tanker1.start(identity).get() + tanker1.registerIdentity(E2ePassphraseVerification(passphrase)).get() + + tanker2.start(identity).get() + tanker2.verifyIdentity(E2ePassphraseVerification(passphrase)).get() + assertThat(tanker2.getStatus()).isEqualTo(Status.READY) + + tanker1.stop().get() + tanker2.stop().get() + } + + @Test + fun test_update_e2e_passphrase() { + val oldPassphrase = "alkalosis" + val newPassphrase = "acidosis" + tanker1.start(identity).get() + tanker1.registerIdentity(E2ePassphraseVerification(oldPassphrase)).get() + tanker1.setVerificationMethod(E2ePassphraseVerification(newPassphrase)).get() + tanker1.stop().get() + + tanker2.start(identity).get() + val e = shouldThrow { + tanker2.verifyIdentity(E2ePassphraseVerification(oldPassphrase)).get() + } + assertThat(e.cause).hasCauseInstanceOf(InvalidVerification::class.java) + + tanker2.verifyIdentity(E2ePassphraseVerification(newPassphrase)).get() + assertThat(tanker2.getStatus()).isEqualTo(Status.READY) + tanker2.stop().get() + } + + @Test + fun test_switch_to_e2e_passphrase() { + val oldPassphrase = "alkalosis" + val newPassphrase = "acidosis" + tanker1.start(identity).get() + tanker1.registerIdentity(PassphraseVerification(oldPassphrase)).get() + tanker1.setVerificationMethod( + E2ePassphraseVerification(newPassphrase), + VerificationOptions().allowE2eMethodSwitch(true) + ).get() + tanker1.stop().get() + + tanker2.start(identity).get() + val e = shouldThrow { + tanker2.verifyIdentity(PassphraseVerification(oldPassphrase)).get() + } + assertThat(e.cause).hasCauseInstanceOf(PreconditionFailed::class.java) + + tanker2.verifyIdentity(E2ePassphraseVerification(newPassphrase)).get() + assertThat(tanker2.getStatus()).isEqualTo(Status.READY) + tanker2.stop().get() + } + + fun test_switch_from_e2e_passphrase() { + val oldPassphrase = "alkalosis" + val newPassphrase = "acidosis" + tanker1.start(identity).get() + tanker1.registerIdentity(E2ePassphraseVerification(oldPassphrase)).get() + tanker1.setVerificationMethod( + PassphraseVerification(newPassphrase), + VerificationOptions().allowE2eMethodSwitch(true) + ).get() + tanker1.stop().get() + + tanker2.start(identity).get() + val e = shouldThrow { + tanker2.verifyIdentity(E2ePassphraseVerification(oldPassphrase)).get() + } + assertThat(e.cause).hasCauseInstanceOf(PreconditionFailed::class.java) + + tanker2.verifyIdentity(PassphraseVerification(newPassphrase)).get() + assertThat(tanker2.getStatus()).isEqualTo(Status.READY) + tanker2.stop().get() + } + + @Test + fun test_cannot_switch_to_e2e_passphrase_without_allow_e2e_switch_flag() { + val oldPassphrase = "alkalosis" + val newPassphrase = "acidosis" + tanker1.start(identity).get() + tanker1.registerIdentity(PassphraseVerification(oldPassphrase)).get() + val e = shouldThrow { + tanker1.setVerificationMethod( + E2ePassphraseVerification(newPassphrase), + ).get() + } + assertThat(e.cause).hasCauseInstanceOf(InvalidArgument::class.java) + tanker1.stop().get() + } } diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/Verification.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/Verification.kt index 034eca2..f1bd39c 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/Verification.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/Verification.kt @@ -8,3 +8,4 @@ data class OIDCIDTokenVerification(val oidcIDToken: String) : Verification() data class PhoneNumberVerification(val phoneNumber: String, val verificationCode: String) : Verification() data class PreverifiedEmailVerification(val preverifiedEmail: String) : Verification() data class PreverifiedPhoneNumberVerification(val preverifiedPhoneNumber: String) : Verification() +data class E2ePassphraseVerification(val e2ePassphrase: String) : Verification() diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationMethod.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationMethod.kt index d165c13..4dbf2ef 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationMethod.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationMethod.kt @@ -11,6 +11,7 @@ object OIDCIDTokenVerificationMethod : VerificationMethod() data class PhoneNumberVerificationMethod(val phoneNumber: String) : VerificationMethod() data class PreverifiedEmailVerificationMethod(val preverifiedEmail: String) : VerificationMethod() data class PreverifiedPhoneNumberVerificationMethod(val preverifiedPhoneNumber: String): VerificationMethod() +object E2ePassphraseVerificationMethod : VerificationMethod() fun verificationMethodFromCVerification(method: TankerVerificationMethod) = when (method.type) { @@ -21,5 +22,6 @@ fun verificationMethodFromCVerification(method: TankerVerificationMethod) = TankerVerification.TypePhoneNumber -> PhoneNumberVerificationMethod(method.value!!) TankerVerification.TypePreverifiedEmail -> PreverifiedEmailVerificationMethod(method.value!!) TankerVerification.TypePreverifiedPhoneNumber -> PreverifiedPhoneNumberVerificationMethod(method.value!!) + TankerVerification.TypeE2ePassphrase -> E2ePassphraseVerificationMethod else -> throw RuntimeException("unknown verification method type: ${method.type}") } diff --git a/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationOptions.kt b/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationOptions.kt index da78291..8d63605 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationOptions.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/api/VerificationOptions.kt @@ -7,8 +7,9 @@ import com.sun.jna.Structure */ open class VerificationOptions: Structure() { // NOTE: Remember to keep the version in sync w/ the c++! - @JvmField var version: Byte = 1 + @JvmField var version: Byte = 2 @JvmField var withSessionToken: Byte = 0 + @JvmField var allowE2eMethodSwitch: Byte = 0 /** * Requests to create a Session Token on verification @@ -18,7 +19,15 @@ open class VerificationOptions: Structure() { return this } + /** + * Allow switching to and from E2E verification methods + */ + fun allowE2eMethodSwitch(allowE2eMethodSwitch: Boolean): VerificationOptions { + this.allowE2eMethodSwitch = if (allowE2eMethodSwitch) 1 else 0 + return this + } + override fun getFieldOrder(): List { - return listOf("version", "withSessionToken") + return listOf("version", "withSessionToken", "allowE2eMethodSwitch") } } diff --git a/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerVerification.kt b/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerVerification.kt index 3d4fbff..bca2fae 100644 --- a/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerVerification.kt +++ b/tanker-bindings/src/main/kotlin/io/tanker/bindings/TankerVerification.kt @@ -12,11 +12,12 @@ class TankerVerification : Structure() { const val TypePhoneNumber: Byte = 5 const val TypePreverifiedEmail: Byte = 6 const val TypePreverifiedPhoneNumber: Byte = 7 + const val TypeE2ePassphrase: Byte = 8 } // NOTE: Remember to keep the version in sync w/ the c++! @JvmField - var version: Byte = 5 + var version: Byte = 6 @JvmField var type: Byte = 0 @JvmField @@ -26,6 +27,8 @@ class TankerVerification : Structure() { @JvmField var passphrase: String? = null @JvmField + var e2ePassphrase: String? = null + @JvmField var oidcIDToken: String? = null @JvmField var phoneNumberVerification: TankerPhoneNumberVerification? = null @@ -35,7 +38,7 @@ class TankerVerification : Structure() { var preverifiedPhoneNumber: String? = null override fun getFieldOrder(): List { - return listOf("version", "type", "verificationKey", "emailVerification", "passphrase", "oidcIDToken", "phoneNumberVerification", "preverifiedEmail", "preverifiedPhoneNumber") + return listOf("version", "type", "verificationKey", "emailVerification", "passphrase", "e2ePassphrase", "oidcIDToken", "phoneNumberVerification", "preverifiedEmail", "preverifiedPhoneNumber") } } @@ -74,6 +77,10 @@ fun Verification.toCVerification(): TankerVerification { out.type = TankerVerification.TypePreverifiedPhoneNumber out.preverifiedPhoneNumber = this.preverifiedPhoneNumber } + is E2ePassphraseVerification -> { + out.type = TankerVerification.TypeE2ePassphrase + out.e2ePassphrase = this.e2ePassphrase + } } return out }