Kotlin SDK

The Fyno TOTP (Time-based One-Time Password) SDK is an Android library that provides secure TOTP generation and management capabilities. It handles tenant enrollment, secret key encryption, TOTP generation with configurable algorithms and parameters, and secure key revocation.

Package Name: io.fyno.kotlin-sdk.totp

Min SDK: 23
Target SDK: 33+
Language: Kotlin

Table of Contents

  1. Installation
  2. Core Components
  3. API Reference
  4. Usage Examples
  5. Data Models
  6. Error Handling
  7. Security Considerations

Installation

Gradle Dependency

Add to your build.gradle file:

1dependencies {
2 implementation 'io.fyno.kotlin-sdk:totp:1.0.0'
3}

Minimum Requirements

  • API Level: 23+
  • Android SDK: API 33+ for compilation
  • Java/Kotlin: Java 8+

Core Components

FynoTOTP Class

The main entry point for all TOTP operations. Handles initialization, tenant management, and OTP generation.

Constructor:

1FynoTOTP(context: Context)

API Reference

init()

Initializes the SDK with workspace and user identifiers.

Signature:

1fun init(
2 wsid: String,
3 distinctId: String,
4 callback: (Result<Unit>) -> Unit
5): Unit

Parameters:

ParameterTypeDescription
wsidStringWorkspace ID for the application
distinctIdStringUnique identifier for the user/device
callback(Result) -> UnitCallback executed on Main thread with initialization result

Returns: Unit (asynchronous via callback)

Throws: Exception details passed via Result.failure()

Example:

1val fynoTotp = FynoTOTP(context)
2fynoTotp.init("workspace_123", "user_456") { result ->
3 result.onSuccess {
4 Log.d("TOTP", "Initialization successful")
5 }
6 result.onFailure { error ->
7 Log.e("TOTP", "Initialization failed: ${error.message}")
8 }
9}

registerTenant()

Registers a tenant and stores their TOTP secret securely.

Signature:

1fun registerTenant(
2 tenantId: String,
3 tenantLabel: String,
4 totpToken: String,
5 callback: (Result<Unit>) -> Unit
6): Unit

Parameters:

ParameterTypeDescription
tenantIdStringUnique identifier for the tenant
tenantLabelStringHuman-readable label/name for the tenant
totpTokenStringTOTP secret key received from Fyno
callback(Result) -> UnitCallback executed on Main thread with registration result

Returns: Unit (asynchronous via callback)

Throws: Exception details passed via Result.failure()

Notes:

  • The TOTP secret is encrypted using Android KeyStore before storage
  • The tenant is automatically marked as ACTIVE upon successful registration
  • This operation requires API 23+ due to KeyStore requirements

Example:

1fynoTotp.registerTenant(
2 tenantId = "tenant_001",
3 tenantLabel = "My App Account",
4 totpToken = "JBSWY3DPEBLW64TMMQQ======",
5 callback = { result ->
6 result.onSuccess {
7 Toast.makeText(context, "Tenant registered", Toast.LENGTH_SHORT).show()
8 }
9 result.onFailure { error ->
10 Toast.makeText(context, "Registration failed: ${error.message}", Toast.LENGTH_SHORT).show()
11 }
12 }
13)

setConfig()

Sets TOTP configuration parameters for a registered tenant.

Signature:

1fun setConfig(
2 tenantId: String,
3 config: TotpConfig,
4 callback: (Result<Unit>) -> Unit
5): Unit

Parameters:

ParameterTypeDescription
tenantIdStringUnique identifier for the tenant
configTotpConfigConfiguration object with TOTP parameters
callback(Result) -> UnitCallback executed on Main thread with result

Returns: Unit (asynchronous via callback)

Throws: Exception details passed via Result.failure()

Notes:

  • Tenant must be registered before setting config
  • Common algorithms: SHA1, SHA256, SHA512
  • Default values: digits=6, period=30, algorithm=SHA1

Example:

1val config = TotpConfig(
2 tenant_name = "My App Account",
3 digits = 6,
4 algorithm = "SHA1",
5 period = 30
6)
7
8fynoTotp.setConfig(
9 tenantId = "tenant_001",
10 config = config,
11 callback = { result ->
12 result.onSuccess {
13 Log.d("TOTP", "Config updated successfully")
14 }
15 result.onFailure { error ->
16 Log.e("TOTP", "Config update failed: ${error.message}")
17 }
18 }
19)

getTotp()

Generates and retrieves the current TOTP code for a tenant.

Signature:

1@RequiresApi(Build.VERSION_CODES.M)
2fun getTotp(
3 tenantId: String,
4 callback: (Result<Unit>) -> Unit
5)

Parameters:

ParameterTypeDescription
tenantIdStringUnique identifier for the tenant
callback(Result) -> UnitCallback executed on Main thread with OTP or null if inactive

Returns: Unit (asynchronous via callback)

Returns in Callback:

  • String: TOTP code
  • null: If tenant is inactive or not found
  • Exception in Result.failure() if generation fails

Notes:

  • Requires API 23+ (uses KeyStore for decryption)
  • Returns null if tenant status is INACTIVE
  • Current server time is used for generation (client-side)
  • Generated code changes every 30 seconds (or configured period)
  • Automatically decrypts the stored secret

Example:

1fynoTotp.getTotp(
2 tenantId = "tenant_001",
3 callback = { result ->
4 result.onSuccess { otp ->
5 if (otp != null) {
6 etOtpInput.setText(otp)
7 Log.d("TOTP", "OTP: $otp")
8 } else {
9 Log.d("TOTP", "Tenant is inactive")
10 }
11 }
12 result.onFailure { error ->
13 Log.e("TOTP", "Failed to get OTP: ${error.message}")
14 }
15 }
16)

revokeTenant()

Revokes a tenant’s TOTP enrollment and permanently deletes the stored secret.

Signature:

1@RequiresApi(Build.VERSION_CODES.M)
2fun revokeTenant(
3 tenantId: String,
4 callback: (Result<Unit>) -> Unit
5): Unit

Parameters:

ParameterTypeDescription
tenantIdStringUnique identifier for the tenant to revoke
callback(Result) -> UnitCallback executed on Main thread with revocation result

Returns: Unit (asynchronous via callback)

Throws: Exception details passed via Result.failure()

Example:

1fynoTotp.revokeTenant(
2 tenantId = "tenant_001",
3 callback = { result ->
4 result.onSuccess {
5 Log.d("TOTP", "Tenant revoked successfully")
6 // Update UI to remove tenant from list
7 }
8 result.onFailure { error ->
9 Log.e("TOTP", "Revocation failed: ${error.message}")
10 }
11 }
12)

Usage Examples

Complete Integration Flow

1class TotpSetupActivity : AppCompatActivity() {
2
3 private lateinit var fynoTotp: FynoTOTP
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7
8 // Initialize SDK
9 fynoTotp = FynoTOTP(this)
10 fynoTotp.init("workspace_id", "user_id") { result ->
11 result.onSuccess {
12 Log.d("Setup", "SDK initialized")
13 }
14 result.onFailure { e ->
15 Log.e("Setup", "Init failed", e)
16 }
17 }
18 }
19
20 private fun enrollTenant() {
21 val tenantId = "tenant_001"
22 val totpSecret = "JBSWY3DPEBLW64TMMQQ======" // Base32 encoded
23
24 fynoTotp.registerTenant(
25 tenantId = tenantId,
26 tenantLabel = "Primary Account",
27 totpToken = totpSecret
28 ) { result ->
29 result.onSuccess {
30 setupTotpConfig(tenantId)
31 }
32 result.onFailure { error ->
33 showError("Enrollment failed: ${error.message}")
34 }
35 }
36 }
37
38 private fun setupTotpConfig(tenantId: String) {
39 val config = TotpConfig(
40 tenant_name = "Primary Account",
41 digits = 6,
42 algorithm = "SHA1",
43 period = 30
44 )
45
46 fynoTotp.setConfig(tenantId, config) { result ->
47 result.onSuccess {
48 Log.d("Setup", "Config set successfully")
49 displayTotpCode(tenantId)
50 }
51 result.onFailure { error ->
52 showError("Config failed: ${error.message}")
53 }
54 }
55 }
56
57 private fun displayTotpCode(tenantId: String) {
58 fynoTotp.getTotp(tenantId) { result ->
59 result.onSuccess { otp ->
60 if (otp != null) {
61 findViewById<TextView>(R.id.tvOtpCode).text = otp
62 }
63 }
64 result.onFailure { error ->
65 Log.e("TOTP", "Failed to get OTP", error)
66 }
67 }
68 }
69
70 private fun revokeTenant(tenantId: String) {
71 fynoTotp.revokeTenant(tenantId) { result ->
72 result.onSuccess {
73 Log.d("Revoke", "Tenant revoked")
74 }
75 result.onFailure { error ->
76 Log.e("Revoke", "Revocation failed", error)
77 }
78 }
79 }
80}

Periodic OTP Refresh

1class TotpRefreshService {
2
3 private lateinit var fynoTotp: FynoTOTP
4 private val handler = Handler(Looper.getMainLooper())
5 private val refreshInterval = 1000L // 1 second
6
7 fun startPeriodicRefresh(tenantId: String, onCodeUpdate: (String?) -> Unit) {
8 val runnable = object : Runnable {
9 override fun run() {
10 fynoTotp.getTotp(tenantId) { result ->
11 result.onSuccess { otp ->
12 onCodeUpdate(otp)
13 }
14 }
15 handler.postDelayed(this, refreshInterval)
16 }
17 }
18 handler.post(runnable)
19 }
20}

Data Models

TotpConfig

Configuration model for TOTP generation parameters.

1data class TotpConfig(
2 val tenant_name: String, // Human-readable tenant name
3 val digits: Int = 6, // Number of digits in OTP (typically 6-8)
4 val algorithm: String = "SHA1", // Hash algorithm (SHA1, SHA256, SHA512)
5 val period: Int = 30 // Time period in seconds (typically 30)
6)

Field Descriptions:

FieldTypeDefaultDescription
tenant_nameString-Display name for the tenant
digitsInt6Length of generated OTP code
algorithmStringSHA1HMAC algorithm for OTP generation
periodInt30Time window for OTP validity (seconds)

Common Configurations:

1// Standard Google Authenticator style
2val standard = TotpConfig(
3 tenant_name = "Google Account",
4 digits = 6,
5 algorithm = "SHA1",
6 period = 30
7)
8
9// Enhanced security with SHA256
10val enhanced = TotpConfig(
11 tenant_name = "Banking App",
12 digits = 8,
13 algorithm = "SHA256",
14 period = 30
15)
16
17// Custom period (60 seconds)
18val custom = TotpConfig(
19 tenant_name = "Custom Service",
20 digits = 6,
21 algorithm = "SHA1",
22 period = 60
23)

Error Handling

Exception Types

The SDK passes exceptions through Result.failure(). Common exceptions include:

ExceptionCauseHandling
InvalidKeyExceptionEncryption/decryption key errorCheck KeyStore availability
NoSuchAlgorithmExceptionAlgorithm not supportedVerify algorithm parameter
SQLExceptionDatabase errorCheck storage permissions
KeyPermanentlyInvalidatedExceptionDevice unlock invalidated keyRe-register tenant
IOExceptionNetwork or storage I/O errorRetry operation

Proper Error Handling Pattern

1fynoTotp.getTotp(tenantId) { result ->
2 result.onSuccess { otp ->
3 // Handle success
4 displayOtp(otp)
5 }
6 result.onFailure { error ->
7 when (error) {
8 is KeyPermanentlyInvalidatedException -> {
9 // Device was unlocked and key was invalidated
10 Toast.makeText(context, "Please re-enroll", Toast.LENGTH_SHORT).show()
11 revokeTenant(tenantId)
12 }
13 is InvalidKeyException -> {
14 Toast.makeText(context, "Security error", Toast.LENGTH_SHORT).show()
15 }
16 else -> {
17 Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_SHORT).show()
18 }
19 }
20 }
21}

Security Considerations

Encryption

  • Storage: TOTP secrets are encrypted using Android KeyStore before being saved to the database
  • Algorithm: Uses Android’s default encryption (AES-GCM on API 23+)
  • Key Generation: Hardware-backed keys when available
  • IV (Initialization Vector): Unique IV generated for each secret and stored separately

Key Management

  • Secrets are never stored in plain text
  • Keys are stored with PURPOSE_DECRYPT and PURPOSE_ENCRYPT only
  • Biometric authentication can be enforced per-key for additional security
  • KeyStore integration requires API 23+

Best Practices

  1. Always check for null results when calling getTotp() for inactive tenants
  2. Handle KeyPermanentlyInvalidatedException after device unlock changes
  3. Use HTTPS only for tenant registration communication (not handled by SDK)
  4. Implement timeout logic for OTP input validation
  5. Do not log or share TOTP secrets in production code
  6. Regularly audit tenant revocations and enrollments
  7. Test on both physical and emulated devices for encryption behavior

API Level Considerations

  • API < 23: KeyStore encryption unavailable; SDK requires API 23+ for full functionality
  • API 23-27: Basic KeyStore support
  • API 28+: Enhanced KeyStore features and performance
  • API 30+: Biometric integration possible

Database Security

  • Database is stored in app-private storage directory
  • No sensitive data is cached in SharedPreferences
  • All queries use parameterized statements to prevent SQL injection

Changelog

Version 1.0.0

  • Initial release
  • Core TOTP generation (SHA1, SHA256, SHA512)
  • Tenant enrollment and revocation
  • Secure secret storage using Android KeyStore
  • Support for customizable TOTP parameters
  • API 23+ support

Support & Documentation

For additional support, refer to:

  • Android KeyStore documentation
  • TOTP RFC 6238 specification
  • Fyno SDK integration guides

Last Updated: January 2026
SDK Version: 1.0.0
Min API Level: 23
Target API Level: 33+