Show icon in react native app (#771)

This commit is contained in:
Leendert de Borst
2025-04-14 22:17:21 +02:00
parent 3a8d08c53b
commit ba5f81ee86
17 changed files with 433 additions and 182 deletions

View File

@@ -9,6 +9,7 @@ import { useDb } from '@/context/DbContext';
import { Credential } from '@/utils/types/Credential';
import SqliteClient from '@/utils/SqliteClient';
import { ThemedScrollView } from '@/components/ThemedScrollView';
import { CredentialIcon } from '@/components/CredentialIcon';
interface FormInputCopyToClipboardProps {
label: string;
@@ -89,7 +90,7 @@ export default function CredentialDetailsScreen() {
useEffect(() => {
const loadCredential = async () => {
if (!dbContext.dbAvailable || !id) return;
try {
const cred = await dbContext.sqliteClient!.getCredentialById(id as string);
if (cred?.Alias?.BirthDate) {
@@ -132,12 +133,7 @@ export default function CredentialDetailsScreen() {
return (
<ThemedScrollView style={styles.container}>
<ThemedView style={styles.header}>
{credential.Logo && (
<Image
source={{ uri: SqliteClient.imgSrcFromBytes(credential.Logo) }}
style={styles.logo}
/>
)}
<CredentialIcon logo={credential.Logo} style={styles.logo} />
<View style={styles.headerText}>
<ThemedText type="title" style={styles.serviceName}>
{credential.ServiceName}
@@ -292,4 +288,4 @@ const styles = StyleSheet.create({
notes: {
fontSize: 14,
},
});
});

View File

@@ -1,4 +1,4 @@
import { StyleSheet, View, Text, FlatList, ActivityIndicator, useColorScheme, TouchableOpacity, TextInput, Keyboard } from 'react-native';
import { StyleSheet, View, Text, FlatList, ActivityIndicator, useColorScheme, TouchableOpacity, TextInput, Keyboard, Image } from 'react-native';
import { useState, useEffect } from 'react';
import { router, Stack } from 'expo-router';
@@ -8,6 +8,7 @@ import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
import { useDb } from '@/context/DbContext';
import { useAuth } from '@/context/AuthContext';
import { Credential } from '@/utils/types/Credential';
import { CredentialIcon } from '@/components/CredentialIcon';
export default function CredentialsScreen() {
const colorScheme = useColorScheme();
@@ -109,19 +110,24 @@ export default function CredentialsScreen() {
style={[styles.credentialItem, dynamicStyles.credentialItem]}
activeOpacity={0.7}
>
<Text style={[styles.serviceName, dynamicStyles.serviceName]}>
{item.ServiceName ?? 'Unknown Service'}
</Text>
{item.Username && (
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
Username: {item.Username}
</Text>
)}
{item.Alias?.Email && (
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
Email: {item.Alias.Email}
</Text>
)}
<View style={styles.credentialContent}>
<CredentialIcon logo={item.Logo} style={styles.logo} />
<View style={styles.credentialInfo}>
<Text style={[styles.serviceName, dynamicStyles.serviceName]}>
{item.ServiceName ?? 'Unknown Service'}
</Text>
{item.Username && (
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
Username: {item.Username}
</Text>
)}
{item.Alias?.Email && (
<Text style={[styles.credentialText, dynamicStyles.credentialText]}>
Email: {item.Alias.Email}
</Text>
)}
</View>
</View>
</TouchableOpacity>
)}
ListEmptyComponent={
@@ -157,11 +163,24 @@ const styles = StyleSheet.create({
gap: 8,
},
credentialItem: {
padding: 16,
padding: 12,
borderRadius: 8,
marginBottom: 8,
borderWidth: 1,
},
credentialContent: {
flexDirection: 'row',
alignItems: 'center',
},
logo: {
width: 32,
height: 32,
borderRadius: 4,
marginRight: 12,
},
credentialInfo: {
flex: 1,
},
serviceName: {
fontSize: 16,
fontWeight: '600',
@@ -181,4 +200,4 @@ const styles = StyleSheet.create({
marginBottom: 16,
fontSize: 16,
},
});
});

View File

@@ -49,7 +49,6 @@ export default function TabTwoScreen() {
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>

View File

View File

@@ -208,7 +208,7 @@ export default function LoginScreen() {
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">AliasVault</ThemedText>
</ThemedView>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
@@ -391,4 +391,4 @@ const styles = StyleSheet.create({
textAlign: 'center',
marginTop: 16,
},
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,135 @@
import { Image, ImageStyle, StyleSheet } from 'react-native';
import { Buffer } from 'buffer';
import { SvgUri } from 'react-native-svg';
type CredentialIconProps = {
logo?: Uint8Array | number[] | string | null;
style?: ImageStyle;
};
export function CredentialIcon({ logo, style }: CredentialIconProps) {
const getLogoSource = (logoData: Uint8Array | number[] | string | null | undefined) => {
if (!logoData) {
return { type: 'image', source: require('@/assets/images/service-placeholder.webp') };
}
try {
// If logo is already a base64 string (from iOS SQLite query result)
if (typeof logoData === 'string') {
const mimeType = detectMimeTypeFromBase64(logoData);
return {
type: mimeType === 'image/svg+xml' ? 'svg' : 'image',
source: `data:${mimeType};base64,${logoData}`
};
}
// Handle binary data (from Android or other sources)
const logoBytes = toUint8Array(logoData);
const base64Logo = Buffer.from(logoBytes).toString('base64');
const mimeType = detectMimeType(logoBytes);
return {
type: mimeType === 'image/svg+xml' ? 'svg' : 'image',
source: `data:${mimeType};base64,${base64Logo}`
};
} catch (error) {
console.error('Error converting logo:', error);
return { type: 'image', source: require('@/assets/images/service-placeholder.webp') };
}
};
const logoSource = getLogoSource(logo);
if (logoSource.type === 'svg') {
// SVGs are not supported in React Native Image component,
// so we use SvgUri from react-native-svg.
return (
<SvgUri
uri={logoSource.source}
width={Number(style?.width ?? styles.logo.width)}
height={Number(style?.height ?? styles.logo.height)}
style={{
borderRadius: styles.logo.borderRadius,
...(style as any),
}}
/>
);
}
return (
<Image
source={typeof logoSource.source === 'string' ? { uri: logoSource.source } : logoSource.source}
style={[styles.logo, style]}
defaultSource={require('@/assets/images/service-placeholder.webp')}
/>
);
}
/**
* Detect MIME type from base64 string by decoding first few bytes
*/
function detectMimeTypeFromBase64(base64: string): string {
try {
const binaryString = atob(base64.slice(0, 8));
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return detectMimeType(bytes);
} catch (error) {
console.warn('Error detecting mime type from base64:', error);
return 'image/x-icon';
}
}
/**
* Detect MIME type from file signature (magic numbers)
*/
function detectMimeType(bytes: Uint8Array): string {
const isSvg = (): boolean => {
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
return header.includes('<?xml') || header.includes('<svg');
};
const isIco = (): boolean => {
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
};
const isPng = (): boolean => {
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
};
if (isSvg()) return 'image/svg+xml';
if (isIco()) return 'image/x-icon';
if (isPng()) return 'image/png';
return 'image/x-icon';
}
/**
* Convert various binary data formats to Uint8Array
*/
function toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
if (buffer instanceof Uint8Array) {
return buffer;
}
if (Array.isArray(buffer)) {
return new Uint8Array(buffer);
}
const length = Object.keys(buffer).length;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = buffer[i];
}
return arr;
}
const styles = StyleSheet.create({
logo: {
width: 32,
height: 32,
borderRadius: 4,
},
});

View File

@@ -13,17 +13,17 @@ class SharedCredentialStore {
private let encryptedDbFileName = "encrypted_db.sqlite"
private var db: Connection?
private var encryptionKey: Data?
public init() {}
// MARK: - Vault Status
func isVaultInitialized() -> Bool {
// Check if encrypted database file exists
let hasDatabase = FileManager.default.fileExists(atPath: getEncryptedDbPath().path)
return hasDatabase
}
// MARK: - Encryption Key Management
private func getEncryptionKey() throws -> Data {
if let key = encryptionKey {
@@ -31,32 +31,32 @@ class SharedCredentialStore {
print("Key found in memory: \(key.base64EncodedString())")
return key
}
guard let keyData = try? keychain
.authenticationPrompt("Authenticate to unlock your vault")
.getData(encryptionKeyKey) else {
throw NSError(domain: "SharedCredentialStore", code: 2, userInfo: [NSLocalizedDescriptionKey: "No encryption key found"])
}
encryptionKey = keyData
// print as base64 for debugging
print("Key found in keychain: \(keyData.base64EncodedString())")
return keyData
}
func storeEncryptionKey(_ base64Key: String) throws {
print("Storing encryption key")
// Convert base64 string to bytes
guard let keyData = Data(base64Encoded: base64Key) else {
throw NSError(domain: "SharedCredentialStore", code: 6, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 key"])
}
// Validate key length (AES-256 requires 32 bytes)
guard keyData.count == 32 else {
throw NSError(domain: "SharedCredentialStore", code: 7, userInfo: [NSLocalizedDescriptionKey: "Invalid key length. Expected 32 bytes"])
}
do {
try keychain
.authenticationPrompt("Authenticate to unlock your vault")
@@ -69,21 +69,21 @@ class SharedCredentialStore {
throw error
}
}
// MARK: - Database Management
private func getEncryptedDbPath() -> URL {
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.aliasvault.autofill") else {
fatalError("Failed to get shared container URL")
}
return containerURL.appendingPathComponent(encryptedDbFileName)
}
func storeEncryptedDatabase(_ base64EncryptedDb: String) throws {
// Store the encrypted database (base64 encoded) in the app's documents directory
try base64EncryptedDb.write(to: getEncryptedDbPath(), atomically: true, encoding: .utf8)
}
// Get the encrypted database as a base64 encoded string
func getEncryptedDatabase() -> String? {
do {
@@ -93,13 +93,13 @@ class SharedCredentialStore {
return nil
}
}
func initializeDatabase() throws {
// Get the encrypted database
guard let encryptedDbBase64 = getEncryptedDatabase() else {
throw NSError(domain: "SharedCredentialStore", code: 1, userInfo: [NSLocalizedDescriptionKey: "No encrypted database found"])
}
let encryptedDbData = Data(base64Encoded: encryptedDbBase64)!
// Get the encryption key
@@ -114,60 +114,60 @@ class SharedCredentialStore {
// Create a temporary file for the decrypted database in the same directory as the encrypted one
let tempDbPath = FileManager.default.temporaryDirectory.appendingPathComponent("temp_db.sqlite")
try decryptedDbData.write(to: tempDbPath)
// Create an in-memory database
db = try Connection(":memory:")
// Import the decrypted database into memory
try db?.attach(.uri(tempDbPath.path, parameters: [.mode(.readOnly)]), as: "source")
try db?.execute("BEGIN TRANSACTION")
// Copy all tables from source to memory
let tables = try db?.prepare("SELECT name FROM source.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
for table in tables! {
let tableName = table[0] as! String
try db?.execute("CREATE TABLE \(tableName) AS SELECT * FROM source.\(tableName)")
}
try db?.execute("COMMIT")
try db?.execute("DETACH DATABASE source")
// Clean up the temporary file
try? FileManager.default.removeItem(at: tempDbPath)
// Setup database pragmas
try db?.execute("PRAGMA journal_mode = WAL")
try db?.execute("PRAGMA synchronous = NORMAL")
try db?.execute("PRAGMA foreign_keys = ON")
}
// MARK: - Encryption/Decryption
private func encrypt(data: Data, key: Data) throws -> Data {
// TODO: make sure we encrypt the data the same as decryption works with combined iv/content/tag.
let key = SymmetricKey(data: key)
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
}
private func decrypt(data: Data, key: Data) throws -> Data {
let key = SymmetricKey(data: key)
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
}
// MARK: - Credential Operations
func addCredential(_ credential: Credential) throws {
if db == nil {
try initializeDatabase()
}
// After initialization attempt, check if db is still nil
guard let db = db else {
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
}
// TODO: update this to use the actual database schema.
// TODO: having the add logic here means we have duplicate code with the react native implementation.
let credentials = Table("credentials")
@@ -177,7 +177,7 @@ class SharedCredentialStore {
let service = Expression<String>("service")
let createdAt = Expression<Date>("created_at")
let updatedAt = Expression<Date>("updated_at")
try db.run(credentials.insert(
id <- UUID().uuidString,
username <- credential.username,
@@ -187,17 +187,17 @@ class SharedCredentialStore {
updatedAt <- Date()
))
}
func getAllCredentials() throws -> [Credential] {
if db == nil {
try initializeDatabase()
}
// After initialization attempt, check if db is still nil
guard let db = db else {
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
}
let query = """
SELECT DISTINCT
c.Username,
@@ -209,13 +209,13 @@ class SharedCredentialStore {
WHERE c.IsDeleted = 0
ORDER BY c.CreatedAt DESC
"""
var result: [Credential] = []
for row in try db.prepare(query) {
let username = row[0] as? String ?? ""
let service = row[1] as? String ?? ""
let password = row[2] as? String ?? ""
result.append(Credential(
username: username,
password: password,
@@ -229,7 +229,7 @@ class SharedCredentialStore {
func clearCache() {
// Clear the cached encryption key
encryptionKey = nil
// Clear the cached encrypted database
db = nil
}
@@ -244,48 +244,67 @@ class SharedCredentialStore {
clearCache()
}
// MARK: - Query Execution
func executeQuery(_ query: String, params: [Binding?]) throws -> [[String: Any]] {
guard let db = db else {
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
}
let statement = try db.prepare(query)
var results: [[String: Any]] = []
for row in try statement.run(params) {
var rowDict: [String: Any] = [:]
for (index, column) in statement.columnNames.enumerated() {
rowDict[column] = row[index]
// Handle different SQLite data types appropriately
let value = row[index]
switch value {
case let data as SQLite.Blob:
// Convert SQLite blob to base64 string for React Native bridge
let binaryData = Data(data.bytes)
rowDict[column] = binaryData.base64EncodedString()
case let date as Date:
rowDict[column] = date
case let number as Int64:
rowDict[column] = number
case let number as Double:
rowDict[column] = number
case let text as String:
rowDict[column] = text
case .none:
rowDict[column] = NSNull()
default:
rowDict[column] = value
}
}
results.append(rowDict)
}
return results
}
func executeUpdate(_ query: String, params: [Binding?]) throws -> Int {
guard let db = db else {
throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
}
let statement = try db.prepare(query)
try statement.run(params)
return db.changes
}
// MARK: - Biometric Authentication
func authenticateWithBiometrics() async throws -> Bool {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw error ?? NSError(domain: "SharedCredentialStore", code: 5, userInfo: [NSLocalizedDescriptionKey: "Biometrics not available"])
}
return try await withCheckedThrowingContinuation { continuation in
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your credentials") { success, error in

View File

@@ -2095,6 +2095,49 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.11.2):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.10.14.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.11.2)
- Yoga
- RNSVG/common (15.11.2):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.10.14.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SocketRocket (0.7.1)
- SQLite.swift (0.14.1):
- SQLite.swift/standard (= 0.14.1)
@@ -2201,6 +2244,7 @@ DEPENDENCIES:
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSVG (from `../node_modules/react-native-svg`)
- SQLite.swift (~> 0.14.0)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -2405,6 +2449,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
RNSVG:
:path: "../node_modules/react-native-svg"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -2507,6 +2553,7 @@ SPEC CHECKSUMS:
RNGestureHandler: fffddeb8af59709c6d8de11b6461a6af63cad532
RNReanimated: 2e5069649cbab2c946652d3b97589b2ae0526220
RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1
RNSVG: 3a1cce2e940268a7d3554e3cf2bbd2195871f4fe
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SQLite.swift: 2992550ebf3c5b268bf4352603e3df87d2a4ed72
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a

View File

@@ -412,6 +412,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
@@ -427,6 +428,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",

View File

@@ -37,6 +37,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.2",
"react-native-toast-message": "^2.2.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
@@ -5128,6 +5129,12 @@
"node": ">=0.6"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -5935,6 +5942,47 @@
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -6178,6 +6226,32 @@
"node": ">=8"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@@ -6192,6 +6266,35 @@
"node": ">=12"
}
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -6313,7 +6416,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -10079,6 +10181,12 @@
"node": ">=0.10"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -10916,6 +11024,18 @@
"node": ">=4"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -12013,6 +12133,21 @@
"react-native": "*"
}
},
"node_modules/react-native-svg": {
"version": "15.11.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
"integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-toast-message": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.2.1.tgz",

View File

@@ -44,6 +44,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.2",
"react-native-toast-message": "^2.2.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",

View File

@@ -4,11 +4,6 @@ import { EncryptionKey } from './types/EncryptionKey';
import { TotpCode } from './types/TotpCode';
import { PasswordSettings } from './types/PasswordSettings';
/**
* Placeholder base64 image for credentials without a logo.
*/
const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==';
type SQLiteBindValue = string | number | null | Uint8Array;
/**
@@ -106,12 +101,9 @@ class SqliteClient {
const results = await this.executeQuery(query, [credentialId]);
if (results.length === 0) {
console.log('No results found for credential:', credentialId);
return null;
}
console.log('Results found for credential:', results);
// Convert the first row to a Credential object
const row = results[0] as any;
return {
@@ -161,9 +153,9 @@ class SqliteClient {
WHERE c.IsDeleted = 0
ORDER BY c.CreatedAt DESC`;
const results = await this.executeQuery(query);
const results = await this.executeQuery<any>(query);
return results.map((row: any) => ({
return results.map((row) => ({
Id: row.Id,
Username: row.Username,
Password: row.Password,
@@ -243,7 +235,7 @@ class SqliteClient {
*/
public async getPasswordSettings(): Promise<PasswordSettings> {
const settingsJson = await this.getSetting('PasswordGenerationSettings');
// Default settings if none found or parsing fails
const defaultSettings: PasswordSettings = {
Length: 18,
@@ -253,7 +245,7 @@ class SqliteClient {
UseSpecialChars: true,
UseNonAmbiguousChars: false
};
try {
if (settingsJson) {
return { ...defaultSettings, ...JSON.parse(settingsJson) };
@@ -261,7 +253,7 @@ class SqliteClient {
} catch (error) {
console.warn('Failed to parse password settings:', error);
}
return defaultSettings;
}
@@ -445,100 +437,6 @@ class SqliteClient {
}
}
/**
* Convert binary data to a base64 encoded image source.
*/
public static imgSrcFromBytes(bytes: Uint8Array | number[] | undefined): string {
// Handle base64 image data
if (bytes) {
try {
const logoBytes = this.toUint8Array(bytes);
const base64Logo = this.base64Encode(logoBytes);
// Detect image type from first few bytes
const mimeType = this.detectMimeType(logoBytes);
return `data:${mimeType};base64,${base64Logo}`;
} catch (error) {
console.error('Error setting logo:', error);
return `data:image/x-icon;base64,${placeholderBase64}`;
}
} else {
return `data:image/x-icon;base64,${placeholderBase64}`;
}
}
/**
* Detect MIME type from file signature (magic numbers)
*/
private static detectMimeType(bytes: Uint8Array): string {
/**
* Check if the file is an SVG file.
*/
const isSvg = () : boolean => {
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
return header.includes('<?xml') || header.includes('<svg');
};
/**
* Check if the file is an ICO file.
*/
const isIco = () : boolean => {
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
};
/**
* Check if the file is an PNG file.
*/
const isPng = () : boolean => {
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
};
if (isSvg()) {
return 'image/svg+xml';
}
if (isIco()) {
return 'image/x-icon';
}
if (isPng()) {
return 'image/png';
}
return 'image/x-icon';
}
/**
* Convert various binary data formats to Uint8Array
*/
private static toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
if (buffer instanceof Uint8Array) {
return buffer;
}
if (Array.isArray(buffer)) {
return new Uint8Array(buffer);
}
const length = Object.keys(buffer).length;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = buffer[i];
}
return arr;
}
/**
* Base64 encode binary data.
*/
private static base64Encode(buffer: Uint8Array | number[] | {[key: number]: number}): string | null {
try {
const arr = Array.from(this.toUint8Array(buffer));
return btoa(arr.reduce((data, byte) => data + String.fromCharCode(byte), ''));
} catch (error) {
console.error('Error encoding to base64:', error);
return null;
}
}
/**
* Check if a table exists in the database
* @param tableName - The name of the table to check