Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@

# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Mateus Andrade
Copyright (c) 2016-2025 Mateus Andrade

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ Modern secure storage for React Native, powered by Nitro Modules. Version 6 ship
> This README tracks the in-progress v6 work on `master`. For the stable legacy release, switch to the `v5.x` branch.

> [!NOTE]
> **Choosing between 5.6.0 and 6.x**
> **Choosing between 5.6.x and 6.x**
>
> - **Need bridge stability?** `5.6.0` is the last pre-Nitro release with the latest biometric fixes, docs, and Android namespace cleanups. It’s drop-in for any `5.5.x` app already running on React Native’s Fabric architecture, but you keep the legacy JS bridge overhead—Paper is no longer supported.
> - **Need bridge stability?** `5.6.x` is the last pre-Nitro release with the latest biometric fixes, docs, and Android namespace cleanups. It’s drop-in for any `5.5.x` app already running on React Native’s Fabric architecture, but you keep the legacy JS bridge overhead—Paper is no longer supported.
> - **Ready for Nitro speed?** `6.x` swaps in the Nitro hybrid core, auto-enforces Class 3/StrongBox biometrics, and ships the refreshed sample app plus richer metadata. Upgrade when you can adopt the Nitro toolchain (RN 0.76+, Node 18+, `react-native-nitro-modules`).
> - **Staying back on 5.5.x?** You remain on the legacy (Paper) architecture and miss the Android 13 prompt fixes, the manual credential fallback restoration, and the new docs—migrate to `5.6.0` at minimum before planning the Nitro jump.
> - **Staying back on 5.5.x?** You remain on the legacy (Paper) architecture and miss the Android 13 prompt fixes, the manual credential fallback restoration, and the new docs—migrate to `5.6.x` at minimum before planning the Nitro jump.

## Table of contents

Expand All @@ -31,6 +31,7 @@ Modern secure storage for React Native, powered by Nitro Modules. Version 6 ship
- [⚡️ Quick start](#-quick-start)
- [📚 API reference](#-api-reference)
- [🔐 Access control & metadata](#-access-control--metadata)
- [❗ Error handling](#-error-handling)
- [🧪 Simulators and emulators](#-simulators-and-emulators)
- [📈 Performance benchmarks](#-performance-benchmarks)
- [🎮 Example application](#-example-application)
Expand Down Expand Up @@ -219,6 +220,47 @@ function YourComponent() {

For comprehensive examples and advanced patterns, see [`HOOKS.md`](./HOOKS.md).

## ❗ Error handling

Every public hook returns failures as `HookError` instances. Besides `message`, each error carries:

- `operation` – the hook action that failed (for example, `useSecureStorage.saveSecret`).
- `cause` – the original native error for additional diagnostics.
- `hint` – a short suggestion shown in the example app and useful for toast copy.

Biometric or device-credential prompts cancelled by the user now surface as a friendly message (`Authentication prompt canceled by the user.`) and *do not* poison hook state. Imperative calls still reject with the raw error so you can decide how to react.

```tsx
import { Text } from 'react-native'
import { useSecureStorage } from 'react-native-sensitive-info'

function SecretsList() {
const { items, error } = useSecureStorage({ service: 'auth', includeValues: true })

if (error) {
if (error.message.includes('Authentication prompt canceled')) {
return <Text>The user dismissed biometric authentication.</Text>
}

return (
<Text testID="secure-error">
{error.message}
{'\n'}Hint: {error.hint ?? 'Check your secure storage configuration.'}
</Text>
)
}

return items.length === 0 ? (
<Text>No secrets stored yet.</Text>
) : (
<Text>{items.map((item) => item.key).join(', ')}</Text>
)
}
```

> [!TIP]
> When using the imperative API, look for the `[E_AUTH_CANCELED]` marker in the thrown error message to detect cancellations.

## Imperative API

```tsx
Expand Down
31 changes: 31 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Security Policy

## Supported Versions

| Version | Supported |
| --- | --- |
| 6.x | ✅ Supported
| 5.6.x | ✅ Supported
| < 5.6.0 | ❌ Not supported

We ship security fixes for the current v6 line and the latest v5 maintenance branch (≥ 5.6.0). Releases prior to 5.6.0 no longer receive patches—upgrade as soon as possible to stay protected.

## Reporting a Vulnerability

1. **Contact**: Email security reports to <mtw.andrade@gmail.com>.
2. **Disclosure Window**: We aim to acknowledge reports within 3 business days and provide a remediation plan within 10 business days.
3. **Coordinated Disclosure**: Please refrain from publicly disclosing the issue until a fix is available or 30 days have passed since acknowledgement.

## Patch Process

- Critical fixes ship in a point release for the supported branches (6.x and ≥ 5.6.0).
- Vulnerability advisories are published on the GitHub release page and npm once patches are available.
- We credit reporters who follow coordinated disclosure and wish to be acknowledged.

## Hardening Recommendations

- Stay on the latest minor release within your major version to receive defense-in-depth updates.
- Review the [Access control & metadata](README.md#-access-control--metadata) section for guidance on choosing the strongest policies.
- Test secure storage flows on physical hardware before shipping; emulators often omit secure elements.

Thank you for helping us keep `react-native-sensitive-info` secure.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import com.sensitiveinfo.internal.util.ReactContextHolder
import com.sensitiveinfo.internal.util.SensitiveInfoException
import javax.crypto.Cipher
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
Expand Down Expand Up @@ -42,11 +43,8 @@ internal class BiometricAuthenticator {

return withContext(Dispatchers.Main) {
if (cipher == null && allowLegacyDeviceCredential && !canUseBiometric()) {
if (DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)) {
cipher
} else {
throw IllegalStateException("Device credential authentication canceled.")
}
DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)
cipher
} else {
try {
authenticateWithBiometricPrompt(
Expand All @@ -59,9 +57,8 @@ internal class BiometricAuthenticator {
} catch (error: Throwable) {
if (error is CancellationException) throw error
if (allowLegacyDeviceCredential) {
if (DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)) {
return@withContext cipher
}
DeviceCredentialPromptFragment.authenticate(activity, effectivePrompt)
return@withContext cipher
}
throw error
}
Expand All @@ -86,11 +83,12 @@ internal class BiometricAuthenticator {
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
if (errorCode == BiometricPrompt.ERROR_CANCELED ||
if (
errorCode == BiometricPrompt.ERROR_CANCELED ||
errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON
) {
continuation.cancel()
continuation.resumeWithException(SensitiveInfoException.AuthenticationCanceled())
} else {
continuation.resumeWithException(IllegalStateException(errString.toString()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.os.Build
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.margelo.nitro.sensitiveinfo.AuthenticationPrompt
import com.sensitiveinfo.internal.util.SensitiveInfoException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
Expand Down Expand Up @@ -58,7 +59,7 @@ internal class DeviceCredentialPromptFragment : Fragment() {
if (resultCode == Activity.RESULT_OK) {
cont.resume(true)
} else {
cont.cancel()
cont.resumeWithException(SensitiveInfoException.AuthenticationCanceled())
}
cleanup()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@ sealed class SensitiveInfoException(
code = "E_NOT_FOUND",
message = "[E_NOT_FOUND] No secret found for key \"$key\" in service \"$service\"."
)

class AuthenticationCanceled : SensitiveInfoException(
code = "E_AUTH_CANCELED",
message = "[E_AUTH_CANCELED] Authentication prompt canceled by the user."
)
}

32 changes: 16 additions & 16 deletions eslint.config.mts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { fixupPluginRules } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import importHelpers from 'eslint-plugin-import-helpers'
import prettier from 'eslint-plugin-prettier'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import globals from 'globals'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { fixupPluginRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import importHelpers from 'eslint-plugin-import-helpers';
import prettier from 'eslint-plugin-prettier';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
});

export default [
...compat.extends(
Expand Down Expand Up @@ -66,4 +66,4 @@ export default [
'import/extensions': 'off',
},
},
]
];
Loading
Loading