Skip to content

Commit

Permalink
Added support of QR Code & Deeplink - Proximity check (#104)
Browse files Browse the repository at this point in the history
* Implement totp

* Fix ktlint

* Fix forward compatibility test

* Klint

* Polishing of the comments, naming

* Add documentation

* Using upgraded networking as a base

* Change styling of comments in documentation.

* Minor improvements

* Fix klint

* Replace empty spaces with empty line

* Remove duplicity code

---------

Co-authored-by: Jan Kobersky <[email protected]>
  • Loading branch information
Hopsaheysa and kober32 authored Nov 20, 2023
1 parent ff6ed68 commit 3c15f31
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 52 deletions.
29 changes: 15 additions & 14 deletions docs/Error-Handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ Every `ApiError` consist of:

## Known API Error codes

| Error Code | Description |
|---|---|
|`ERROR_GENERIC`|When unexpected error happened|
|`POWERAUTH_AUTH_FAIL`|General authentication failure (wrong password, wrong activation state, etc...)|
|`INVALID_REQUEST`|Invalid request sent - missing request object in the request|
|`INVALID_ACTIVATION`|Activation is not valid (it is different from configured activation)|
|`PUSH_REGISTRATION_FAILED`|Error code for a situation when registration to push notification fails|
|`OPERATION_ALREADY_FINISHED`|Operation is already finished|
|`OPERATION_ALREADY_FAILED`|Operation is already failed|
|`OPERATION_ALREADY_CANCELED`|Operation is canceled|
|`OPERATION_EXPIRED`|Operation is expired|
|`ERR_AUTHENTICATION`|Error in case that PowerAuth authentication fails|
|`ERR_SECURE_VAULT`|Error during secure vault unlocking|
|`ERR_ENCRYPTION`|Returned in case encryption or decryption fails|
| Error Code | Description |
|------------------------------|---|
| `ERROR_GENERIC` |When unexpected error happened|
| `POWERAUTH_AUTH_FAIL` |General authentication failure (wrong password, wrong activation state, etc...)|
| `INVALID_REQUEST` |Invalid request sent - missing request object in the request|
| `INVALID_ACTIVATION` |Activation is not valid (it is different from configured activation)|
| `PUSH_REGISTRATION_FAILED` |Error code for a situation when registration to push notification fails|
| `OPERATION_ALREADY_FINISHED` |Operation is already finished|
| `OPERATION_ALREADY_FAILED` |Operation is already failed|
| `OPERATION_ALREADY_CANCELED` |Operation is canceled|
| `OPERATION_EXPIRED` |Operation is expired|
| `OPERATION_FAILED` |Default operation action failure|
| `ERR_AUTHENTICATION` |Error in case that PowerAuth authentication fails|
| `ERR_SECURE_VAULT` |Error during secure vault unlocking|
| `ERR_ENCRYPTION` |Returned in case encryption or decryption fails|
148 changes: 122 additions & 26 deletions docs/Using-Operations-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Operations API Reference](#operations-api-reference)
- [UserOperation](#useroperation)
- [Creating a Custom Operation](#creating-a-custom-operation)
- [TOTP ProximityCheck](#totp-proximitycheck)
## Introduction
<!-- end -->

Expand Down Expand Up @@ -372,41 +373,51 @@ Definition of the `UserOperations`:
```kotlin
class UserOperation: IOperation {

// Unique operation identifier
/** Unique operation identifier */
val id: String

// System name of the operation.
//
// This property lets you adjust the UI for various operation types.
// For example, the "login" operation may display a specialized interface with
// an icon or an illustration, instead of an empty list of attributes,
// "payment" operation can include a special icon that denotes payments, etc.
/**
* System name of the operation.
*
* This property lets you adjust the UI for various operation types.
* For example, the "login" operation may display a specialized interface with
* an icon or an illustration, instead of an empty list of attributes,
* "payment" operation can include a special icon that denotes payments, etc.
*/
val name: String

// Actual data that will be signed.
/** Actual data that will be signed. */
val data: String

// Date and time when the operation was created.
/** Date and time when the operation was created. */
val created: ZonedDateTime

// Date and time when the operation will expire.
/** Date and time when the operation will expire. */
val expires: ZonedDateTime

// Data that should be presented to the user.
/** Data that should be presented to the user. */
val formData: FormData

// Allowed signature types.
//
// This hints if the operation needs a 2nd factor or can be approved simply by
// tapping an approve button. If the operation requires 2FA, this value also hints if
// the user may use the biometry, or if a password is required.
/**
* Allowed signature types.
*
* This hints if the operation needs a 2nd factor or can be approved simply by
* tapping an approve button. If the operation requires 2FA, this value also hints if
* the user may use the biometry, or if a password is required.
*/
val allowedSignatureType: AllowedSignatureType

// Data for the operation UI presented
//
// Accompanying information about the operation additional UI which should be presented such as
// Pre-Approval Screen or Post-Approval Screen

/**
* Data for the operation UI presented
*
* Accompanying information about the operation additional UI which should be presented such as
* Pre-Approval Screen or Post-Approval Screen
*/
val ui: OperationUIData?

/** Proximity Check Data to be passed when OTP is handed to the app */
var proximityCheck: ProximityCheck? = null
}
```

Expand All @@ -415,16 +426,18 @@ Definition of `FormData`:
```kotlin
class FormData {

/// Title of the operation
/** Title of the operation */
val title: String

/// Message for the user
/** Message for the user */
val message: String

/// Other attributes.
///
/// Each attribute presents one line in the UI. Attributes are differentiated by type property
/// and specific classes such as NoteAttribute or AmountAttribute.
/**
* Other attributes.
*
* Each attribute presents one line in the UI. Attributes are differentiated by type property
* and specific classes such as NoteAttribute or AmountAttribute.
*/
val attributes: List<Attribute>
}
```
Expand All @@ -439,6 +452,63 @@ Attributes types:
- `IMAGE` image row
- `UNKNOWN` fallback option when unknown attribute type is passed. Such attribute only contains the label.

Definition of `OperationUIData`:

```kotlin
class OperationUIData {
/** Confirm and Reject buttons should be flipped both in position and style */
val flipButtons: Boolean?

/** Block approval when on call (for example when on a phone or Skype call) */
val blockApprovalOnCall: Boolean?

/** UI for pre-approval operation screen */
val preApprovalScreen: PreApprovalScreen?

/**
* UI for post-approval operation screen
*
* Type of PostApprovalScreen is presented with different classes (Starting with `PostApprovalScreen*`)
*/
val postApprovalScreen: PostApprovalScreen?
}
```

PreApprovalScreen types:

- `WARNING`
- `INFO`
- `QR_SCAN` this type indicates that the `ProximityCheck` must be used for authorization
- `UNKNOWN`

PostApprovalScreen types:
`PostApprovalScreen*` classes commonly contain `heading` and `message` and different payload data

- `REVIEW` provides an array of operations attributes with data: type, id, label, and note
- `REDIRECT` providing text for button, countdown, and redirection URL
- `GENERIC` may contain any object

Definition of `ProximityCheck`:

```kotlin
class ProximityCheck {

/** The actual Time-based one time password */
val totp: String

/** Type of the Proximity check */
val type: ProximityCheckType

/** Timestamp when the operation was scanned (QR Code) or delivered to the device (Deeplink) */
val timestampRequested: ZonedDateTime = ZonedDateTime.now()
}
```

ProximityCheckType types:

- `QR_CODE` TOTP was scanned from QR code
- `DEEPLINK` TOTP was delivered to the app via Deeplink

## Creating a Custom Operation

In some specific scenarios, you might need to approve or reject an operation that you received through a different channel than `getOperations`. In such cases, you can implement the `IOperation` interface in your custom class and then feed created objects to both `authorizeOperation` and `rejectOperation` methods.
Expand All @@ -461,5 +531,31 @@ interface IOperation {
* Data for signing
*/
val data: String

/**
* Additional information with proximity check data
*/
var proximityCheck: ProximityCheck?
}
```

## TOTP ProximityCheck

Two-Factor Authentication (2FA) using Time-Based One-Time Passwords (TOTP) in the Operations Service is facilitated through the use of ProximityCheck. This allows secure approval of operations through QR code scanning or deeplink handling.

- QR Code Flow:

When the `UserOperation` contains a `PreApprovalScreen.QR_SCAN`, the app should open the camera to scan the QR code before confirming the operation. Use the camera to scan the QR code containing the necessary data payload for the operation.

- Deeplink Flow:

When the app is launched via a deeplink, preserve the data from the deeplink and extract the relevant data. When operations are loaded compare the operation ID from the deeplink data to the operations within the app to find a match.

- Assign TOTP and Type to the Operation
Once the QR code is scanned or match from the deeplink is found, create a `WMTProximityCheck` with:
- `totp`: The actual Time-Based One-Time Password.
- `type`: Set to `ProximityCheckType.QR_CODE` or `ProximityCheckType.DEEPLINK`.
- `timestampRequested`: The timestamp when the QR code was scanned (by default, it is created as the current timestamp when the object is instantiated).

- Authorizing the ProximityCheck
When authorizing, the SDK will by default add `timestampSigned` to the `ProximityCheck` object. This timestamp indicates when the operation was signed.
4 changes: 2 additions & 2 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ dependencies {
// DO NOT UPGRADE ABOVE 3.12.X! Version 3.12 is the last version supporting TLS 1 and 1.1
// If upgraded, the app will crash on android 4.4
implementation("com.squareup.okhttp3:okhttp:3.12.13")
implementation("com.wultra.android.powerauth:powerauth-networking:1.2.0")
implementation("com.wultra.android.powerauth:powerauth-networking:1.2.2")

// Dependencies
compileOnly("com.wultra.android.powerauth:powerauth-sdk:1.7.8")
Expand All @@ -93,7 +93,7 @@ dependencies {
// Android tests
androidTestImplementation("com.jakewharton.threetenabp:threetenabp:1.1.1")
androidTestImplementation("com.wultra.android.powerauth:powerauth-sdk:1.7.8")
androidTestImplementation("com.wultra.android.powerauth:powerauth-networking:1.2.0")
androidTestImplementation("com.wultra.android.powerauth:powerauth-networking:1.2.2")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test:core:1.5.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import org.threeten.bp.Instant
import org.threeten.bp.ZoneId
import org.threeten.bp.ZonedDateTime
Expand All @@ -29,7 +32,7 @@ import java.lang.reflect.Type
/**
* Gson deserializer for [ZonedDateTime].
*/
internal class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime> {
internal class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>, JsonSerializer<ZonedDateTime> {

override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ZonedDateTime {
val jsonPrimitive = json.asJsonPrimitive
Expand All @@ -53,4 +56,15 @@ internal class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime> {
}
throw JsonParseException("Unable to parse ZonedDateTime")
}

override fun serialize(src: ZonedDateTime?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
// Custom format for ZonedDateTime
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")

// Format the ZonedDateTime using the defined format
val formattedZonedDateTime = src?.format(formatter)

// Create a JsonPrimitive from the formatted string
return JsonPrimitive(formattedZonedDateTime)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,55 @@
package com.wultra.android.mtokensdk.api.operation.model

import com.google.gson.annotations.SerializedName
import org.threeten.bp.ZonedDateTime

/**
* Authorize request model class.
*
* @property id Operation ID.
* @property data Operation data.
* @property proximityCheck Proximity check OTP data.
*/
internal data class AuthorizeRequestObject(
@SerializedName("id")
val id: String,

@SerializedName("data")
val data: String
val data: String,

@SerializedName("proximityCheck")
val proximityCheck: ProximityCheckData? = null
) {

constructor(operation: IOperation, timestampSigned: ZonedDateTime = ZonedDateTime.now()): this(
operation.id,
operation.data,
operation.proximityCheck?.let {
ProximityCheckData(
it.totp,
it.type,
it.timestampRequested,
timestampSigned
)
}
)
}

internal data class ProximityCheckData(

/** The actual OTP code */
@SerializedName("otp")
val otp: String,

/** Type of the Proximity check */
@SerializedName("type")
val type: ProximityCheckType,

/** Timestamp when the operation was delivered to the app */
@SerializedName("timestampRequested")
val timestampRequested: ZonedDateTime,

/** Timestamp when the operation was signed */
@SerializedName("timestampSigned")
val timestampSigned: ZonedDateTime
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ interface IOperation {

/** Data for signing */
val data: String

/** Additional information with proximity check data */
var proximityCheck: ProximityCheck?
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ data class LocalOperation(
override val id: String,

/** Data for signing */
override val data: String
override val data: String,

/** Proximity check data */
override var proximityCheck: ProximityCheck? = null
): IOperation
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ data class QROperation(
/** Flags associated with the operation */
val flags: QROperationFlags,

/** Additional Time-based one time password for proximity check */
val totp: String?,

/** Data for signature validation */
val signedData: ByteArray,

Expand All @@ -50,7 +53,13 @@ data class QROperation(
/** QR code uses a string in newer format that this class implements. This may be used as warning in UI */
val isNewerFormat: Boolean
) {
fun dataForOfflineSigning() = "$operationId&${operationData.sourceString}".toByteArray()
fun dataForOfflineSigning(): ByteArray {
return if (totp == null) {
"$operationId&${operationData.sourceString}".toByteArray(Charsets.UTF_8)
} else {
"$operationId&${operationData.sourceString}&$totp".toByteArray(Charsets.UTF_8)
}
}
}

/**
Expand Down
Loading

0 comments on commit 3c15f31

Please sign in to comment.