Skip to content

Commit 0bd3be1

Browse files
committed
feat(rotation): enhance key rotation process with improved error handling and manual rotation support
1 parent 05bdf16 commit 0bd3be1

7 files changed

Lines changed: 381 additions & 198 deletions

File tree

android/src/main/java/com/sensitiveinfo/HybridSensitiveInfo.kt

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
500500
return Promise.async(coroutineScope) {
501501
val deps = ensureInitialized()
502502

503+
// Set rotation in progress
504+
deps.keyRotationManager.setRotationInProgress(true)
505+
503506
// Emit started event
504507
rotationEventCallback?.invoke(RotationEvent(
505508
type = "rotation:started",
@@ -511,67 +514,82 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
511514

512515
val startTime = System.currentTimeMillis()
513516

514-
// Generate a new key
515-
val newKeyId = System.currentTimeMillis().toString()
516-
val success = deps.keyRotationManager.generateNewKey(newKeyId, requiresBiometry = false)
517-
if (!success) {
518-
rotationEventCallback?.invoke(RotationEvent(
519-
type = "rotation:failed",
520-
timestamp = System.currentTimeMillis().toDouble(),
521-
reason = "Failed to generate new key",
522-
itemsReEncrypted = null,
523-
duration = null
524-
))
525-
throw IllegalStateException("Failed to generate new key for rotation")
526-
}
517+
try {
518+
// Generate a new key
519+
val newKeyId = System.currentTimeMillis().toString()
520+
val success = deps.keyRotationManager.generateNewKey(newKeyId, requiresBiometry = false)
521+
if (!success) {
522+
// Set rotation not in progress on failure
523+
deps.keyRotationManager.setRotationInProgress(false)
524+
525+
rotationEventCallback?.invoke(RotationEvent(
526+
type = "rotation:failed",
527+
timestamp = System.currentTimeMillis().toDouble(),
528+
reason = "Failed to generate new key",
529+
itemsReEncrypted = null,
530+
duration = null
531+
))
532+
throw IllegalStateException("Failed to generate new key for rotation")
533+
}
527534

528-
// Rotate to the new key
529-
val rotateSuccess = deps.keyRotationManager.rotateToNewKey(newKeyId)
530-
if (!rotateSuccess) {
531-
rotationEventCallback?.invoke(RotationEvent(
532-
type = "rotation:failed",
533-
timestamp = System.currentTimeMillis().toDouble(),
534-
reason = "Failed to rotate to new key",
535-
itemsReEncrypted = null,
536-
duration = null
537-
))
538-
throw IllegalStateException("Failed to rotate to new key")
539-
}
535+
// Rotate to the new key
536+
val rotateSuccess = deps.keyRotationManager.rotateToNewKey(newKeyId)
537+
if (!rotateSuccess) {
538+
// Set rotation not in progress on failure
539+
deps.keyRotationManager.setRotationInProgress(false)
540+
541+
rotationEventCallback?.invoke(RotationEvent(
542+
type = "rotation:failed",
543+
timestamp = System.currentTimeMillis().toDouble(),
544+
reason = "Failed to rotate to new key",
545+
itemsReEncrypted = null,
546+
duration = null
547+
))
548+
throw IllegalStateException("Failed to rotate to new key")
549+
}
540550

541-
// Perform re-encryption if enabled
542-
val preferences = deps.context.getSharedPreferences(
543-
"com.sensitiveinfo.keyrotation",
544-
Context.MODE_PRIVATE
545-
)
546-
val backgroundReEncryption = preferences.getBoolean("background_re_encryption", true)
547-
var itemsReEncrypted = 0.0
548-
if (backgroundReEncryption) {
549-
val result = reEncryptAllItemsImpl(deps, newKeyId)
550-
itemsReEncrypted = result.itemsReEncrypted
551-
}
551+
// Perform re-encryption if enabled
552+
val preferences = deps.context.getSharedPreferences(
553+
"com.sensitiveinfo.keyrotation",
554+
Context.MODE_PRIVATE
555+
)
556+
val backgroundReEncryption = preferences.getBoolean("background_re_encryption", true)
557+
var itemsReEncrypted = 0.0
558+
if (backgroundReEncryption) {
559+
val result = reEncryptAllItemsImpl(deps, newKeyId)
560+
itemsReEncrypted = result.itemsReEncrypted
561+
}
552562

553-
// Update last rotation timestamp
554-
preferences.edit().putLong("last_rotation_timestamp", System.currentTimeMillis()).apply()
563+
// Update last rotation timestamp
564+
preferences.edit().putLong("last_rotation_timestamp", System.currentTimeMillis()).apply()
555565

556-
val duration = System.currentTimeMillis() - startTime
566+
val duration = System.currentTimeMillis() - startTime
557567

558-
// Emit completed event
559-
rotationEventCallback?.invoke(RotationEvent(
560-
type = "rotation:completed",
561-
timestamp = System.currentTimeMillis().toDouble(),
562-
reason = request.reason ?: "Manual rotation",
563-
itemsReEncrypted = itemsReEncrypted,
564-
duration = duration.toDouble()
565-
))
568+
// Set rotation not in progress
569+
deps.keyRotationManager.setRotationInProgress(false)
566570

567-
// Return result
568-
RotationResult(
569-
success = true,
570-
newKeyVersion = KeyVersion(id = newKeyId),
571-
itemsReEncrypted = itemsReEncrypted,
572-
duration = duration.toDouble(),
573-
reason = request.reason ?: "Manual rotation"
574-
)
571+
// Emit completed event
572+
rotationEventCallback?.invoke(RotationEvent(
573+
type = "rotation:completed",
574+
timestamp = System.currentTimeMillis().toDouble(),
575+
reason = request.reason ?: "Manual rotation",
576+
itemsReEncrypted = itemsReEncrypted,
577+
duration = duration.toDouble()
578+
))
579+
580+
// Return result
581+
RotationResult(
582+
success = true,
583+
newKeyVersion = KeyVersion(id = newKeyId),
584+
itemsReEncrypted = itemsReEncrypted,
585+
duration = duration.toDouble(),
586+
reason = request.reason ?: "Manual rotation"
587+
)
588+
} catch (e: Exception) {
589+
// Set rotation not in progress on any error
590+
deps.keyRotationManager.setRotationInProgress(false)
591+
throw e
592+
}
575593
}
576594
}
577595

@@ -585,9 +603,10 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
585603
val currentKey = deps.keyRotationManager.getCurrentKeyVersion()
586604
val availableVersions = deps.keyRotationManager.getAvailableKeyVersions()
587605
val lastRotationTimestamp = deps.keyRotationManager.getLastRotationTimestamp()
606+
val isRotating = deps.keyRotationManager.isRotationInProgress()
588607

589608
RotationStatus(
590-
isRotating = false, // TODO: Track rotation state
609+
isRotating = isRotating,
591610
currentKeyVersion = currentKey?.let { KeyVersion(id = it) },
592611
availableKeyVersions = availableVersions.map { KeyVersion(id = it) }.toTypedArray(),
593612
lastRotationTimestamp = lastRotationTimestamp?.toDouble()
@@ -600,6 +619,8 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
600619
*/
601620
override fun onRotationEvent(callback: (RotationEvent) -> Unit): () -> Unit {
602621
rotationEventCallback = callback
622+
// Also set the biometric change callback to the same callback
623+
dependencies?.keyRotationManager?.setBiometricChangeCallback(callback)
603624
return { rotationEventCallback = null }
604625
}
605626

android/src/main/java/com/sensitiveinfo/KeyRotation.kt

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import android.security.keystore.KeyGenParameterSpec
2828
import android.security.keystore.KeyPermanentlyInvalidatedException
2929
import android.security.keystore.KeyProperties
3030
import androidx.biometric.BiometricManager
31+
import com.margelo.nitro.sensitiveinfo.RotationEvent
3132
import javax.crypto.KeyGenerator
3233
import java.security.KeyStore
3334
import java.util.Calendar
@@ -48,6 +49,7 @@ class AndroidKeyRotationManager(private val context: Context) {
4849

4950
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER)
5051
private val biometricManager: BiometricManager = BiometricManager.from(context)
52+
private var biometricChangeCallback: ((RotationEvent) -> Unit)? = null
5153

5254
init {
5355
keyStore.load(null)
@@ -391,14 +393,36 @@ class AndroidKeyRotationManager(private val context: Context) {
391393
}
392394

393395
/**
394-
* Updates the last rotation timestamp.
396+
* Checks if key rotation is currently in progress.
395397
*/
396-
private fun setLastRotationTimestamp() {
398+
fun isRotationInProgress(): Boolean {
399+
return try {
400+
val preferences = context.getSharedPreferences(
401+
"com.sensitiveinfo.keyrotation",
402+
Context.MODE_PRIVATE
403+
)
404+
preferences.getBoolean("rotation_in_progress", false)
405+
} catch (exception: Exception) {
406+
false
407+
}
408+
}
409+
410+
/**
411+
* Sets the rotation in progress state.
412+
*/
413+
fun setRotationInProgress(inProgress: Boolean) {
397414
val preferences = context.getSharedPreferences(
398415
"com.sensitiveinfo.keyrotation",
399416
Context.MODE_PRIVATE
400417
)
401-
preferences.edit().putLong("last_rotation_timestamp", System.currentTimeMillis()).apply()
418+
preferences.edit().putBoolean("rotation_in_progress", inProgress).apply()
419+
}
420+
421+
/**
422+
* Sets the callback for biometric change events.
423+
*/
424+
fun setBiometricChangeCallback(callback: (RotationEvent) -> Unit) {
425+
biometricChangeCallback = callback
402426
}
403427

404428
// MARK: - Notifications
@@ -410,9 +434,14 @@ class AndroidKeyRotationManager(private val context: Context) {
410434
* @note Implementation depends on how the native bridge is structured
411435
*/
412436
private fun notifyBiometricChangeToJavaScript() {
413-
// TODO: Implement notification to JS side using appropriate bridge mechanism
414-
// This could use RCTNativeModule event emitter or similar
415-
android.util.Log.i("KeyRotation", "Biometric change notification sent to JavaScript")
437+
val event = RotationEvent(
438+
type = "biometric:changed",
439+
timestamp = System.currentTimeMillis().toDouble(),
440+
reason = "Biometric enrollment changed",
441+
itemsReEncrypted = null,
442+
duration = null
443+
)
444+
biometricChangeCallback?.invoke(event)
416445
}
417446
}
418447

example/src/components/SecretsList.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ const SecretsList: React.FC<SecretsListProps> = ({
5050
) : (
5151
<Text style={styles.secretValueMuted}>Locked value</Text>
5252
)}
53+
<Text style={styles.secretMeta}>
54+
Security · {item.metadata.securityLevel}
55+
</Text>
56+
<Text style={styles.secretMeta}>
57+
Backend · {item.metadata.backend}
58+
</Text>
5359
<Text style={styles.secretMeta}>
5460
Access · {item.metadata.accessControl}
5561
</Text>
62+
<Text style={styles.secretMeta}>
63+
Key Alias · {item.metadata.alias}
64+
</Text>
5665
<Text style={styles.secretMeta}>
5766
Stored ·{' '}
5867
{new Date(item.metadata.timestamp * 1000).toLocaleString()}

0 commit comments

Comments
 (0)