Skip to content

Commit 6be17c0

Browse files
committed
Refactor platform logic and add modular managers
Introduces modular internal managers for authentication, access control, and metadata on Android and iOS. Adds new files for Android (AuthenticationManager, AccessControlManager, MetadataManager and their implementations), and restructures iOS logic to use dependency injection and single-responsibility managers. Updates HybridSensitiveInfo on both platforms to delegate to these managers, improving maintainability and testability. Removes obsolete docs/REFACTORING_SUMMARY.md. Also updates dependencies in example and root package files.
1 parent a520418 commit 6be17c0

27 files changed

+2164
-1672
lines changed

CODE_OF_CONDUCT.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
2+
# Contributor Covenant Code of Conduct
3+
4+
## Our Pledge
5+
6+
We as members, contributors, and leaders pledge to make participation in our
7+
community a harassment-free experience for everyone, regardless of age, body
8+
size, visible or invisible disability, ethnicity, sex characteristics, gender
9+
identity and expression, level of experience, education, socio-economic status,
10+
nationality, personal appearance, race, caste, color, religion, or sexual
11+
identity and orientation.
12+
13+
We pledge to act and interact in ways that contribute to an open, welcoming,
14+
diverse, inclusive, and healthy community.
15+
16+
## Our Standards
17+
18+
Examples of behavior that contributes to a positive environment for our
19+
community include:
20+
21+
* Demonstrating empathy and kindness toward other people
22+
* Being respectful of differing opinions, viewpoints, and experiences
23+
* Giving and gracefully accepting constructive feedback
24+
* Accepting responsibility and apologizing to those affected by our mistakes,
25+
and learning from the experience
26+
* Focusing on what is best not just for us as individuals, but for the overall
27+
community
28+
29+
Examples of unacceptable behavior include:
30+
31+
* The use of sexualized language or imagery, and sexual attention or advances of
32+
any kind
33+
* Trolling, insulting or derogatory comments, and personal or political attacks
34+
* Public or private harassment
35+
* Publishing others' private information, such as a physical or email address,
36+
without their explicit permission
37+
* Other conduct which could reasonably be considered inappropriate in a
38+
professional setting
39+
40+
## Enforcement Responsibilities
41+
42+
Community leaders are responsible for clarifying and enforcing our standards of
43+
acceptable behavior and will take appropriate and fair corrective action in
44+
response to any behavior that they deem inappropriate, threatening, offensive,
45+
or harmful.
46+
47+
Community leaders have the right and responsibility to remove, edit, or reject
48+
comments, commits, code, wiki edits, issues, and other contributions that are
49+
not aligned to this Code of Conduct, and will communicate reasons for moderation
50+
decisions when appropriate.
51+
52+
## Scope
53+
54+
This Code of Conduct applies within all community spaces, and also applies when
55+
an individual is officially representing the community in public spaces.
56+
Examples of representing our community include using an official e-mail address,
57+
posting via an official social media account, or acting as an appointed
58+
representative at an online or offline event.
59+
60+
## Enforcement
61+
62+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
63+
reported to the community leaders responsible for enforcement at
64+
[INSERT CONTACT METHOD].
65+
All complaints will be reviewed and investigated promptly and fairly.
66+
67+
All community leaders are obligated to respect the privacy and security of the
68+
reporter of any incident.
69+
70+
## Enforcement Guidelines
71+
72+
Community leaders will follow these Community Impact Guidelines in determining
73+
the consequences for any action they deem in violation of this Code of Conduct:
74+
75+
### 1. Correction
76+
77+
**Community Impact**: Use of inappropriate language or other behavior deemed
78+
unprofessional or unwelcome in the community.
79+
80+
**Consequence**: A private, written warning from community leaders, providing
81+
clarity around the nature of the violation and an explanation of why the
82+
behavior was inappropriate. A public apology may be requested.
83+
84+
### 2. Warning
85+
86+
**Community Impact**: A violation through a single incident or series of
87+
actions.
88+
89+
**Consequence**: A warning with consequences for continued behavior. No
90+
interaction with the people involved, including unsolicited interaction with
91+
those enforcing the Code of Conduct, for a specified period of time. This
92+
includes avoiding interactions in community spaces as well as external channels
93+
like social media. Violating these terms may lead to a temporary or permanent
94+
ban.
95+
96+
### 3. Temporary Ban
97+
98+
**Community Impact**: A serious violation of community standards, including
99+
sustained inappropriate behavior.
100+
101+
**Consequence**: A temporary ban from any sort of interaction or public
102+
communication with the community for a specified period of time. No public or
103+
private interaction with the people involved, including unsolicited interaction
104+
with those enforcing the Code of Conduct, is allowed during this period.
105+
Violating these terms may lead to a permanent ban.
106+
107+
### 4. Permanent Ban
108+
109+
**Community Impact**: Demonstrating a pattern of violation of community
110+
standards, including sustained inappropriate behavior, harassment of an
111+
individual, or aggression toward or disparagement of classes of individuals.
112+
113+
**Consequence**: A permanent ban from any sort of public interaction within the
114+
community.
115+
116+
## Attribution
117+
118+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119+
version 2.1, available at
120+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121+
122+
Community Impact Guidelines were inspired by
123+
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124+
125+
For answers to common questions about this code of conduct, see the FAQ at
126+
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127+
[https://www.contributor-covenant.org/translations][translations].
128+
129+
[homepage]: https://www.contributor-covenant.org
130+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131+
[Mozilla CoC]: https://github.com/mozilla/diversity
132+
[FAQ]: https://www.contributor-covenant.org/faq
133+
[translations]: https://www.contributor-covenant.org/translations

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

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package com.sensitiveinfo
22

33
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
46
import com.margelo.nitro.core.Promise
57
import com.margelo.nitro.sensitiveinfo.*
8+
import com.sensitiveinfo.internal.auth.AndroidAuthenticationManager
9+
import com.sensitiveinfo.internal.auth.AuthenticationManager
610
import com.sensitiveinfo.internal.auth.BiometricAuthenticator
11+
import com.sensitiveinfo.internal.crypto.AccessControlManager
712
import com.sensitiveinfo.internal.crypto.AccessControlResolver
13+
import com.sensitiveinfo.internal.crypto.AndroidAccessControlManager
814
import com.sensitiveinfo.internal.crypto.CryptoManager
915
import com.sensitiveinfo.internal.crypto.SecurityAvailabilityResolver
16+
import com.sensitiveinfo.internal.metadata.AndroidMetadataManagerImpl
17+
import com.sensitiveinfo.internal.metadata.MetadataManager
1018
import com.sensitiveinfo.internal.response.ResponseBuilder
1119
import com.sensitiveinfo.internal.response.StandardResponseBuilder
1220
import com.sensitiveinfo.internal.storage.PersistedEntry
@@ -41,10 +49,14 @@ import kotlin.jvm.Volatile
4149
* @since 6.0.0
4250
*/
4351
final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
52+
4453
private data class Dependencies(
4554
val context: Context,
4655
val storage: SecureStorage,
4756
val cryptoManager: CryptoManager,
57+
val metadataManager: MetadataManager,
58+
val authenticationManager: AuthenticationManager,
59+
val accessControlManager: AccessControlManager,
4860
val accessControlResolver: AccessControlResolver,
4961
val securityAvailabilityResolver: SecurityAvailabilityResolver,
5062
val serviceNameResolver: ServiceNameResolver,
@@ -58,9 +70,11 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
5870
private val initializationLock = Any()
5971
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
6072
private var rotationEventCallback: ((RotationEvent) -> Unit)? = null
61-
private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
73+
private val mainHandler = Handler(Looper.getMainLooper())
6274
private var rotationCheckRunnable: Runnable? = null
6375

76+
// MARK: - Initialization
77+
6478
private fun initialize(ctx: Context): Dependencies {
6579
dependencies?.let { return it }
6680

@@ -72,10 +86,20 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
7286
val authenticator = BiometricAuthenticator()
7387
val cryptoManager = CryptoManager(authenticator)
7488

89+
// Initialize specialized managers
90+
val metadataManager: MetadataManager = AndroidMetadataManagerImpl()
91+
val authenticationManager: AuthenticationManager = AndroidAuthenticationManager(authenticator)
92+
val accessControlManager: AccessControlManager = AndroidAccessControlManager(
93+
securityAvailabilityResolver
94+
)
95+
7596
Dependencies(
7697
context = ctx,
7798
storage = SecureStorage(ctx),
7899
cryptoManager = cryptoManager,
100+
metadataManager = metadataManager,
101+
authenticationManager = authenticationManager,
102+
accessControlManager = accessControlManager,
79103
accessControlResolver = accessControlResolver,
80104
securityAvailabilityResolver = securityAvailabilityResolver,
81105
serviceNameResolver = serviceNameResolver,
@@ -655,15 +679,21 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
655679
var reEncryptedCount = 0
656680
val errors = mutableListOf<ReEncryptError>()
657681

658-
// Step 4: Re-encrypt items that use old keys
682+
// Step 4: Re-encrypt items that use old keys or have empty metadata alias
659683
for ((key, entry) in entries) {
660684
try {
661-
if (entry.alias != currentKeyVersion && entry.ciphertext != null && entry.iv != null) {
685+
// Re-encrypt if: (1) using old key, (2) metadata alias is empty, or (3) has ciphertext and iv
686+
val shouldReEncrypt = (entry.metadata.alias != currentKeyVersion || entry.metadata.alias.isEmpty()) &&
687+
entry.ciphertext != null && entry.iv != null
688+
689+
if (shouldReEncrypt) {
662690
// Get access control from persisted
663691
val accessControl = accessControlFromPersisted(entry.metadata.accessControl) ?: AccessControl.NONE
664692
val securityLevel = securityLevelFromPersisted(entry.metadata.securityLevel) ?: SecurityLevel.SOFTWARE
665693

666-
// Decrypt with old key
694+
// Decrypt with old key (use metadata.alias or entry.alias as fallback)
695+
val oldKeyAlias = entry.metadata.alias.takeIf { it.isNotEmpty() } ?: entry.alias
696+
667697
val resolution = deps.cryptoManager.buildResolutionForPersisted(
668698
accessControl = accessControl,
669699
securityLevel = securityLevel,
@@ -674,7 +704,7 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
674704
)
675705

676706
val plaintext = deps.cryptoManager.decrypt(
677-
entry.alias,
707+
oldKeyAlias,
678708
entry.ciphertext,
679709
entry.iv,
680710
resolution,
@@ -698,7 +728,7 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
698728
null
699729
)
700730

701-
// Update storage
731+
// Update storage with new key alias
702732
val updatedEntry = entry.copy(
703733
ciphertext = encryption.ciphertext,
704734
iv = encryption.iv,
@@ -797,12 +827,18 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
797827

798828
for ((key, entry) in entries) {
799829
try {
800-
if (entry.metadata.alias != newKeyVersion && entry.ciphertext != null && entry.iv != null) {
830+
// Re-encrypt if: (1) using old key, (2) metadata alias is empty, or (3) has ciphertext and iv
831+
val shouldReEncrypt = (entry.metadata.alias != newKeyVersion || entry.metadata.alias.isEmpty()) &&
832+
entry.ciphertext != null && entry.iv != null
833+
834+
if (shouldReEncrypt) {
801835
// Get access control from persisted
802836
val accessControl = accessControlFromPersisted(entry.metadata.accessControl) ?: AccessControl.NONE
803837
val securityLevel = securityLevelFromPersisted(entry.metadata.securityLevel) ?: SecurityLevel.SOFTWARE
804838

805-
// Decrypt with old key
839+
// Decrypt with old key (use metadata.alias or entry.alias as fallback)
840+
val oldKeyAlias = entry.metadata.alias.takeIf { it.isNotEmpty() } ?: entry.alias
841+
806842
val resolution = deps.cryptoManager.buildResolutionForPersisted(
807843
accessControl = accessControl,
808844
securityLevel = securityLevel,
@@ -813,7 +849,7 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
813849
)
814850

815851
val plaintext = deps.cryptoManager.decrypt(
816-
entry.metadata.alias,
852+
oldKeyAlias,
817853
entry.ciphertext,
818854
entry.iv,
819855
resolution,
@@ -837,7 +873,7 @@ final class HybridSensitiveInfo : HybridSensitiveInfoSpec() {
837873
null
838874
)
839875

840-
// Update storage
876+
// Update storage with new key alias
841877
val updatedEntry = entry.copy(
842878
ciphertext = encryption.ciphertext,
843879
iv = encryption.iv,
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.sensitiveinfo.internal.auth
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import androidx.biometric.BiometricManager
6+
import androidx.biometric.BiometricPrompt
7+
import androidx.fragment.app.FragmentActivity
8+
import com.margelo.nitro.sensitiveinfo.AuthenticationPrompt
9+
import com.sensitiveinfo.internal.util.ReactContextHolder
10+
import kotlinx.coroutines.suspendCancellableCoroutine
11+
import kotlin.coroutines.resume
12+
import kotlin.coroutines.resumeWithException
13+
14+
/**
15+
* Concrete implementation of AuthenticationManager for Android.
16+
*
17+
* Handles biometric and device credential authentication:
18+
* - Biometric prompt presentation
19+
* - Device credential fallback
20+
* - Error mapping and handling
21+
* - Custom prompt customization
22+
*
23+
* @since 6.0.0
24+
*/
25+
class AndroidAuthenticationManager(
26+
private val biometricAuthenticator: BiometricAuthenticator = BiometricAuthenticator()
27+
) : AuthenticationManager {
28+
29+
private val context: Context?
30+
get() = ReactContextHolder.getContext()
31+
32+
override suspend fun isBiometricAvailable(): Boolean {
33+
val ctx = context ?: return false
34+
val manager = BiometricManager.from(ctx)
35+
return manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
36+
}
37+
38+
override suspend fun isDeviceCredentialAvailable(): Boolean {
39+
val ctx = context ?: return false
40+
val manager = BiometricManager.from(ctx)
41+
return manager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS
42+
}
43+
44+
override suspend fun evaluateBiometric(prompt: AuthenticationPrompt?): Boolean {
45+
return suspendCancellableCoroutine { continuation ->
46+
val ctx = context
47+
if (ctx !is FragmentActivity) {
48+
continuation.resumeWithException(
49+
IllegalStateException("Context must be FragmentActivity for biometric authentication")
50+
)
51+
return@suspendCancellableCoroutine
52+
}
53+
54+
val title = prompt?.title ?: "Authenticate"
55+
val subtitle = prompt?.subtitle ?: "Use biometric to continue"
56+
val negativeText = prompt?.cancel ?: "Cancel"
57+
58+
biometricAuthenticator.authenticate(
59+
fragmentActivity = ctx,
60+
title = title,
61+
subtitle = subtitle,
62+
negativeButtonText = negativeText,
63+
onSuccess = { continuation.resume(true) },
64+
onError = { error ->
65+
if (isAuthenticationCanceled(error)) {
66+
continuation.resumeWithException(
67+
Exception("[E_AUTH_CANCELED] Authentication prompt canceled by the user.")
68+
)
69+
} else {
70+
continuation.resumeWithException(error)
71+
}
72+
}
73+
)
74+
}
75+
}
76+
77+
override suspend fun evaluateDeviceCredential(prompt: AuthenticationPrompt?): Boolean {
78+
// Device credential is handled through BiometricPrompt with DEVICE_CREDENTIAL authenticator
79+
return evaluateBiometric(prompt)
80+
}
81+
82+
override fun isAuthenticationCanceled(exception: Exception): Boolean {
83+
val message = exception.message ?: ""
84+
return message.contains("canceled", ignoreCase = true) ||
85+
message.contains("user_cancel", ignoreCase = true) ||
86+
message.contains("negative_button", ignoreCase = true)
87+
}
88+
89+
override fun makeAuthenticationError(exception: Exception): String {
90+
return if (isAuthenticationCanceled(exception)) {
91+
"[E_AUTH_CANCELED] Authentication prompt canceled by the user."
92+
} else {
93+
"Authentication failed: ${exception.message}"
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)