Skip to content

Commit 4454883

Browse files
committed
fix(auth): treat authentication cancellations as soft-failures and map native cancel codes
- Add CODE_OF_CONDUCT - Bump LICENSE copyright range to 2016-2025 - Android: - Introduce SensitiveInfoException.AuthenticationCanceled and throw/resume with it when user cancels biometric/device-credential prompts - Simplify device credential flow to always return cipher after prompt - iOS: - Map relevant OSStatus values to an E_AUTH_CANCELED runtime error for friendly messaging - Internal errors: - Add AUTH_CANCELED marker and helper hasErrorMarker/isAuthenticationCanceledError - Centralize detection of auth-cancelled errors - Hooks & utilities: - Export and use isAuthenticationCanceledError in error-utils - Create user-friendly hook error message for canceled auths and export detector - Update hooks (useSecretItem, useHasSecret, useSecureOperation, useSecureStorage, useSecurityAvailability) to treat auth cancellations as non-fatal: preserve/clear state appropriately and avoid surfacing HookError when user dismisses prompts - Add applyError helper in useSecureStorage to centralize error handling - Update hook types and exports - Nitro/native layers & types: - Type and formatting fixes across sensitive-info.nitro.ts, internal/native, options, core/storage and index exports - Tests & tooling: - Apply consistent code style (semicolons, trailing commas) across tests and configs - Update many test files to match changes and ensure behavior for canceled auth flows - Misc: - Update package.json description - ESLint config formatting fixes This change makes authentication prompt cancellations explicit (E_AUTH_CANCELED) and prevents noisy error states in hooks when users dismiss biometric / device credential prompts.
1 parent 0c6bd68 commit 4454883

43 files changed

Lines changed: 1054 additions & 817 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Mateus Andrade
3+
Copyright (c) 2016-2025 Mateus Andrade
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

android/src/main/java/com/sensitiveinfo/internal/auth/BiometricAuthenticator.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
1010
import kotlinx.coroutines.suspendCancellableCoroutine
1111
import kotlinx.coroutines.withContext
1212
import com.sensitiveinfo.internal.util.ReactContextHolder
13+
import com.sensitiveinfo.internal.util.SensitiveInfoException
1314
import javax.crypto.Cipher
1415
import kotlin.coroutines.cancellation.CancellationException
1516
import kotlin.coroutines.resume
@@ -42,11 +43,8 @@ internal class BiometricAuthenticator {
4243

4344
return withContext(Dispatchers.Main) {
4445
if (cipher == null && allowLegacyDeviceCredential && !canUseBiometric()) {
45-
if (DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)) {
46-
cipher
47-
} else {
48-
throw IllegalStateException("Device credential authentication canceled.")
49-
}
46+
DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)
47+
cipher
5048
} else {
5149
try {
5250
authenticateWithBiometricPrompt(
@@ -59,9 +57,8 @@ internal class BiometricAuthenticator {
5957
} catch (error: Throwable) {
6058
if (error is CancellationException) throw error
6159
if (allowLegacyDeviceCredential) {
62-
if (DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)) {
63-
return@withContext cipher
64-
}
60+
DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)
61+
return@withContext cipher
6562
}
6663
throw error
6764
}
@@ -86,11 +83,12 @@ internal class BiometricAuthenticator {
8683
}
8784

8885
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
89-
if (errorCode == BiometricPrompt.ERROR_CANCELED ||
86+
if (
87+
errorCode == BiometricPrompt.ERROR_CANCELED ||
9088
errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
9189
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON
9290
) {
93-
continuation.cancel()
91+
continuation.resumeWithException(SensitiveInfoException.AuthenticationCanceled())
9492
} else {
9593
continuation.resumeWithException(IllegalStateException(errString.toString()))
9694
}

android/src/main/java/com/sensitiveinfo/internal/auth/DeviceCredentialPromptFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.os.Build
88
import androidx.fragment.app.Fragment
99
import androidx.fragment.app.FragmentActivity
1010
import com.margelo.nitro.sensitiveinfo.AuthenticationPrompt
11+
import com.sensitiveinfo.internal.util.SensitiveInfoException
1112
import kotlin.coroutines.resume
1213
import kotlin.coroutines.resumeWithException
1314
import kotlinx.coroutines.CancellableContinuation
@@ -58,7 +59,7 @@ internal class DeviceCredentialPromptFragment : Fragment() {
5859
if (resultCode == Activity.RESULT_OK) {
5960
cont.resume(true)
6061
} else {
61-
cont.cancel()
62+
cont.resumeWithException(SensitiveInfoException.AuthenticationCanceled())
6263
}
6364
cleanup()
6465
}

android/src/main/java/com/sensitiveinfo/internal/util/SensitiveInfoExceptions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,10 @@ sealed class SensitiveInfoException(
1212
code = "E_NOT_FOUND",
1313
message = "[E_NOT_FOUND] No secret found for key \"$key\" in service \"$service\"."
1414
)
15+
16+
class AuthenticationCanceled : SensitiveInfoException(
17+
code = "E_AUTH_CANCELED",
18+
message = "[E_AUTH_CANCELED] Authentication prompt canceled by the user."
19+
)
1520
}
1621

eslint.config.mts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import { fixupPluginRules } from '@eslint/compat'
2-
import { FlatCompat } from '@eslint/eslintrc'
3-
import js from '@eslint/js'
4-
import typescriptEslint from '@typescript-eslint/eslint-plugin'
5-
import tsParser from '@typescript-eslint/parser'
6-
import importHelpers from 'eslint-plugin-import-helpers'
7-
import prettier from 'eslint-plugin-prettier'
8-
import react from 'eslint-plugin-react'
9-
import reactHooks from 'eslint-plugin-react-hooks'
10-
import globals from 'globals'
11-
import path from 'node:path'
12-
import { fileURLToPath } from 'node:url'
1+
import { fixupPluginRules } from '@eslint/compat';
2+
import { FlatCompat } from '@eslint/eslintrc';
3+
import js from '@eslint/js';
4+
import typescriptEslint from '@typescript-eslint/eslint-plugin';
5+
import tsParser from '@typescript-eslint/parser';
6+
import importHelpers from 'eslint-plugin-import-helpers';
7+
import prettier from 'eslint-plugin-prettier';
8+
import react from 'eslint-plugin-react';
9+
import reactHooks from 'eslint-plugin-react-hooks';
10+
import globals from 'globals';
11+
import path from 'node:path';
12+
import { fileURLToPath } from 'node:url';
1313

14-
const __filename = fileURLToPath(import.meta.url)
15-
const __dirname = path.dirname(__filename)
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = path.dirname(__filename);
1616

1717
const compat = new FlatCompat({
1818
baseDirectory: __dirname,
1919
recommendedConfig: js.configs.recommended,
2020
allConfig: js.configs.all,
21-
})
21+
});
2222

2323
export default [
2424
...compat.extends(
@@ -66,4 +66,4 @@ export default [
6666
'import/extensions': 'off',
6767
},
6868
},
69-
]
69+
];

ios/HybridSensitiveInfo.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,19 @@ final class HybridSensitiveInfo: HybridSensitiveInfoSpec {
378378
}
379379

380380
private func runtimeError(for status: OSStatus, operation: String) -> RuntimeError {
381+
if isAuthenticationCanceled(status: status) {
382+
return RuntimeError.error(withMessage: "[E_AUTH_CANCELED] Authentication prompt canceled by the user.")
383+
}
381384
let message = SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus(\(status))"
382385
return RuntimeError.error(withMessage: "Keychain \(operation) failed: \(message)")
383386
}
387+
388+
private func isAuthenticationCanceled(status: OSStatus) -> Bool {
389+
switch status {
390+
case errSecUserCanceled, errSecInteractionNotAllowed:
391+
return true
392+
default:
393+
return false
394+
}
395+
}
384396
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "react-native-sensitive-info",
33
"version": "6.0.0-rc.8",
4-
"description": "react-native-sensitive-info is a react native package built with Nitro",
4+
"description": "🔐 React Native secure storage, rebuilt with Nitro Modules ⚡️ Biometric-ready, StrongBox-aware, and metadata-rich for modern mobile apps",
55
"main": "./lib/commonjs/index.js",
66
"module": "./lib/module/index.js",
77
"types": "./lib/typescript/src/index.d.ts",
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
export class MockHybridObject {
2-
static instances: MockHybridObject[] = []
2+
static instances: MockHybridObject[] = [];
33

44
constructor() {
5-
MockHybridObject.instances.push(this)
5+
MockHybridObject.instances.push(this);
66
}
77
}
88

99
export const getHybridObjectConstructor = jest
1010
.fn(() => MockHybridObject)
11-
.mockName('getHybridObjectConstructor')
11+
.mockName('getHybridObjectConstructor');
1212

1313
export const __resetMocks = () => {
14-
MockHybridObject.instances = []
15-
getHybridObjectConstructor.mockReset()
16-
getHybridObjectConstructor.mockReturnValue(MockHybridObject)
17-
}
14+
MockHybridObject.instances = [];
15+
getHybridObjectConstructor.mockReset();
16+
getHybridObjectConstructor.mockReturnValue(MockHybridObject);
17+
};
1818

19-
__resetMocks()
19+
__resetMocks();
2020

2121
export default {
2222
getHybridObjectConstructor,
23-
}
23+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export const NativeModules = {}
1+
export const NativeModules = {};
22

33
export default {
44
NativeModules,
5-
}
5+
};

0 commit comments

Comments
 (0)