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
Original file line number Diff line number Diff line change
Expand Up @@ -940,8 +940,18 @@ class NativeBiometricVaultModule(reactContext: ReactApplicationContext) : Native
return
}
try {
// CHECKED Keystore delete first. The wrapped secret prefs remain
// intact until the OS-gated key is proven gone, so a Keystore
// failure leaves the vault in a retryable, still-intact state.
// Previously the prefs were removed before this checked delete;
// if the Keystore delete then failed, the wallet secret was
// unrecoverable even though reset rejected.
deleteKeystoreKeyChecked(keyAlias)

// Use `commit()` so a prefs write failure is surfaced before
// the JS layer clears the reset sentinel.
// the JS layer clears the reset sentinel. A failure here leaves
// stale ciphertext prefs without a Keystore key; the next
// sentinel retry will re-run this idempotently and remove them.
val prefsRemoved = prefs()
.edit()
.remove(ivKey(keyAlias))
Expand All @@ -954,16 +964,6 @@ class NativeBiometricVaultModule(reactContext: ReactApplicationContext) : Native
)
return
}
// CHECKED Keystore delete. Any failure here
// (KeyStoreException, ProviderException, IOException, or
// the silent-OEM-fail caught by the post-delete
// containsAlias() guard) propagates to JS via promise.reject
// so `useAgentStore.reset()` can persist the
// VAULT_RESET_PENDING_KEY sentinel and surface the error to
// the user. Previously, `deleteKeystoreKey` swallowed every
// exception and `deleteSecret` resolved successfully even
// when the OS-gated key remained on disk.
deleteKeystoreKeyChecked(keyAlias)
// Both halves succeeded — missing alias is a vacuous
// success path inside `deleteKeystoreKeyChecked` (it
// skips the deleteEntry call when containsAlias is false).
Expand Down
32 changes: 32 additions & 0 deletions ios/EnboxMobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
BV000001A00000000000000A /* RCTNativeBiometricVault.mm in Sources */ = {isa = PBXBuildFile; fileRef = BV000002A00000000000000B /* RCTNativeBiometricVault.mm */; };
BV000003A00000000000000C /* RCTNativeBiometricVault.h in Headers */ = {isa = PBXBuildFile; fileRef = BV000004A00000000000000D /* RCTNativeBiometricVault.h */; };
CR000001A00000000000000A /* RCTNativeCrypto.mm in Sources */ = {isa = PBXBuildFile; fileRef = CR000002A00000000000000B /* RCTNativeCrypto.mm */; };
LA000001A00000000000F001 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = LA000002A00000000000F002 /* LocalAuthentication.framework */; };
PR000001A00000000000000A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
SE000001A00000000000F003 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = SE000002A00000000000F004 /* Security.framework */; };
SS000001A00000000000000A /* RCTNativeSecureStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = SS000002A00000000000000B /* RCTNativeSecureStorage.mm */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -30,8 +33,12 @@
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
BV000002A00000000000000B /* RCTNativeBiometricVault.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTNativeBiometricVault.mm; path = RCTNativeBiometricVault.mm; sourceTree = "<group>"; };
BV000004A00000000000000D /* RCTNativeBiometricVault.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTNativeBiometricVault.h; path = RCTNativeBiometricVault.h; sourceTree = "<group>"; };
CR000002A00000000000000B /* RCTNativeCrypto.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTNativeCrypto.mm; path = RCTNativeCrypto.mm; sourceTree = "<group>"; };
CR000004A00000000000000D /* RCTNativeCrypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTNativeCrypto.h; path = RCTNativeCrypto.h; sourceTree = "<group>"; };
LA000002A00000000000F002 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; };
SE000002A00000000000F004 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
SS000002A00000000000000B /* RCTNativeSecureStorage.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTNativeSecureStorage.mm; path = RCTNativeSecureStorage.mm; sourceTree = "<group>"; };
SS000004A00000000000000D /* RCTNativeSecureStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTNativeSecureStorage.h; path = RCTNativeSecureStorage.h; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -57,10 +64,22 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
BV000005A00000000000000E /* NativeBiometricVault */,
SS000005A00000000000000E /* NativeSecureStorage */,
CR000005A00000000000000E /* NativeCrypto */,
);
name = EnboxMobile;
sourceTree = "<group>";
};
CR000005A00000000000000E /* NativeCrypto */ = {
isa = PBXGroup;
children = (
CR000004A00000000000000D /* RCTNativeCrypto.h */,
CR000002A00000000000000B /* RCTNativeCrypto.mm */,
);
name = NativeCrypto;
path = EnboxMobile/NativeCrypto;
sourceTree = "<group>";
};
BV000005A00000000000000E /* NativeBiometricVault */ = {
isa = PBXGroup;
children = (
Expand All @@ -71,6 +90,16 @@
path = EnboxMobile/NativeBiometricVault;
sourceTree = "<group>";
};
SS000005A00000000000000E /* NativeSecureStorage */ = {
isa = PBXGroup;
children = (
SS000004A00000000000000D /* RCTNativeSecureStorage.h */,
SS000002A00000000000000B /* RCTNativeSecureStorage.mm */,
);
name = NativeSecureStorage;
path = EnboxMobile/NativeSecureStorage;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -182,6 +211,7 @@
files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
PR000001A00000000000000A /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -269,6 +299,8 @@
files = (
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
BV000001A00000000000000A /* RCTNativeBiometricVault.mm in Sources */,
SS000001A00000000000000A /* RCTNativeSecureStorage.mm in Sources */,
CR000001A00000000000000A /* RCTNativeCrypto.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
5 changes: 3 additions & 2 deletions ios/EnboxMobile/NativeCrypto/RCTNativeCrypto.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "RCTNativeCrypto.h"
#import <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonKeyDerivation.h>
#import <Security/SecRandom.h>

Expand Down Expand Up @@ -53,11 +54,11 @@ - (void)pbkdf2:(NSString *)password

int status = CCKeyDerivationPBKDF(
kCCPBKDF2,
passwordData.bytes, passwordData.length,
(const char *)passwordData.bytes, passwordData.length,
(const uint8_t *)saltData.bytes, saltData.length,
kCCPRFHmacAlgSHA256,
(uint)iterations,
derivedKey.mutableBytes, keyLen
(uint8_t *)derivedKey.mutableBytes, keyLen
);

if (status != kCCSuccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function WalletConnectRequestScreen() {

useEffect(() => {
if (!selectedDid && identities.length > 0) {
setSelectedDid(identities[0].metadata.uri);
setSelectedDid(identities[0].metadata?.uri ?? identities[0].did?.uri);
}
}, [identities, selectedDid]);

Expand Down Expand Up @@ -128,7 +128,7 @@ export function WalletConnectRequestScreen() {
setCreatingIdentity(true);
try {
const identity = await createIdentity(identityName.trim());
setSelectedDid(identity.metadata.uri);
setSelectedDid(identity.metadata?.uri ?? identity.did?.uri);
setIdentityName('');
} catch (err) {
Alert.alert('Identity creation failed', err instanceof Error ? err.message : 'Could not create identity');
Expand Down Expand Up @@ -273,7 +273,7 @@ export function WalletConnectRequestScreen() {
) : (
<View style={styles.identityList}>
{identities.map((identity) => {
const did = identity.metadata.uri;
const did = identity.metadata?.uri ?? identity.did?.uri;
const selected = selectedDid === did;
return (
<AppButton
Expand Down
134 changes: 130 additions & 4 deletions src/features/identities/screens/identities-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function IdentitiesScreen() {
const theme = useAppTheme();
const identities = useAgentStore((s) => s.identities);
const createIdentity = useAgentStore((s) => s.createIdentity);
const updateIdentityName = useAgentStore((s) => s.updateIdentityName);
const deleteIdentity = useAgentStore((s) => s.deleteIdentity);
const refreshIdentities = useAgentStore((s) => s.refreshIdentities);
const agent = useAgentStore((s) => s.agent);
const isInitializing = useAgentStore((s) => s.isInitializing);
Expand All @@ -29,6 +31,14 @@ export function IdentitiesScreen() {
const [showCreate, setShowCreate] = useState(false);
const [name, setName] = useState('');
const [creating, setCreating] = useState(false);
const [selectedDid, setSelectedDid] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [saving, setSaving] = useState(false);

const selectedIdentity = identities.find((identity) => {
const did = identity.metadata?.uri ?? identity.did?.uri;
return did === selectedDid;
});

const handleCreate = useCallback(async () => {
if (!name.trim() || creating) return;
Expand All @@ -45,6 +55,53 @@ export function IdentitiesScreen() {
}
}, [name, creating, createIdentity]);

const handleSelect = useCallback((identity: any) => {
const did = identity.metadata?.uri ?? identity.did?.uri;
if (!did) return;
setSelectedDid(did);
setEditName(identity.metadata?.name ?? '');
}, []);

const handleSaveName = useCallback(async () => {
if (!selectedDid || !editName.trim() || saving) return;
setSaving(true);
try {
await updateIdentityName(selectedDid, editName.trim());
} catch (err) {
Alert.alert('Update failed', err instanceof Error ? err.message : 'Could not update identity');
} finally {
setSaving(false);
}
}, [editName, saving, selectedDid, updateIdentityName]);

const handleDelete = useCallback(() => {
if (!selectedDid) return;
Alert.alert(
'Delete identity',
'This removes the identity and its local key material from this wallet. Make sure you have an identity backup before continuing.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => {
deleteIdentity(selectedDid)
.then(() => {
setSelectedDid(null);
setEditName('');
})
.catch((err) => {
Alert.alert(
'Delete failed',
err instanceof Error ? err.message : 'Could not delete identity',
);
});
},
},
],
);
}, [deleteIdentity, selectedDid]);

useEffect(() => {
if (agent) {
refreshIdentities().catch(() => {});
Expand Down Expand Up @@ -130,13 +187,72 @@ export function IdentitiesScreen() {
</View>
)}

{selectedIdentity ? (
<View
style={[
styles.card,
{ backgroundColor: theme.colors.surface, borderColor: theme.colors.border },
]}
>
<Text style={[styles.cardTitle, { color: theme.colors.text }]}>Identity details</Text>
<TextInput
accessibilityLabel="Edit identity name"
onChangeText={setEditName}
placeholder="Display name"
placeholderTextColor={theme.colors.textMuted}
style={[styles.input, { backgroundColor: theme.colors.surfaceMuted, borderColor: theme.colors.border, color: theme.colors.text }]}
value={editName}
/>
<Text style={[styles.detailLabel, { color: theme.colors.textMuted }]}>DID</Text>
<Text style={[styles.detailValue, { color: theme.colors.text }]} selectable>
{selectedIdentity.metadata?.uri ?? selectedIdentity.did?.uri}
</Text>
{selectedIdentity.metadata?.tenant ? (
<>
<Text style={[styles.detailLabel, { color: theme.colors.textMuted }]}>Tenant</Text>
<Text style={[styles.detailValue, { color: theme.colors.text }]} selectable>
{selectedIdentity.metadata.tenant}
</Text>
</>
) : null}
{selectedIdentity.metadata?.connectedDid ? (
<>
<Text style={[styles.detailLabel, { color: theme.colors.textMuted }]}>Connected DID</Text>
<Text style={[styles.detailValue, { color: theme.colors.text }]} selectable>
{selectedIdentity.metadata.connectedDid}
</Text>
</>
) : null}
<View style={styles.buttons}>
<AppButton
label="Close"
variant="secondary"
onPress={() => setSelectedDid(null)}
/>
<AppButton
label="Save"
loading={saving}
disabled={!editName.trim()}
onPress={handleSaveName}
/>
</View>
<AppButton
label="Delete identity"
variant="secondary"
onPress={handleDelete}
/>
</View>
) : null}

{identities.length > 0 && (
<>
<FlatList
data={identities}
keyExtractor={(item) => item.metadata.uri}
keyExtractor={(item) => item.metadata?.uri ?? item.did?.uri}
scrollEnabled={false}
renderItem={({ item }) => <IdentityRow identity={item} theme={theme} />}
renderItem={({ item }) => (
<IdentityRow identity={item} theme={theme} onPress={() => handleSelect(item)} />
)}
ItemSeparatorComponent={Separator}
style={[styles.list, { borderColor: theme.colors.border }]}
/>
Expand All @@ -154,12 +270,20 @@ function Separator() {
return <View style={styles.separator} />;
}

function IdentityRow({ identity, theme }: { identity: any; theme: AppTheme }) {
function IdentityRow({
identity,
theme,
onPress,
}: {
identity: any;
theme: AppTheme;
onPress: () => void;
}) {
const didUri = identity.metadata?.uri ?? identity.did?.uri ?? 'Unknown DID';
const displayName = identity.metadata?.name ?? 'Unnamed';

return (
<Pressable style={styles.row} accessibilityRole="button">
<Pressable style={styles.row} accessibilityRole="button" onPress={onPress}>
<View style={[styles.avatar, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.avatarText, { color: theme.colors.accent }]}>
{displayName.charAt(0).toUpperCase()}
Expand Down Expand Up @@ -187,6 +311,8 @@ const styles = StyleSheet.create({
emptyBody: { fontSize: 15, lineHeight: 22, textAlign: 'center' },
card: { borderRadius: 24, borderWidth: 1, padding: 20, gap: 12 },
cardTitle: { fontSize: 18, fontWeight: '700' },
detailLabel: { fontSize: 12, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.5 },
detailValue: { fontSize: 12, fontFamily: 'monospace', lineHeight: 18 },
input: { borderRadius: 16, borderWidth: 1, fontSize: 16, paddingHorizontal: 16, paddingVertical: 14 },
buttons: { flexDirection: 'row', gap: 12 },
list: { borderRadius: 16, borderWidth: 1, overflow: 'hidden' },
Expand Down
Loading
Loading