feat: support importing entried from other authenticators (#30)
This commit is contained in:
parent
e3f7ab5203
commit
308b546395
29
HomePage.js
29
HomePage.js
|
@ -26,6 +26,7 @@ import EnterAccountDetails from "./EnterAccountDetails";
|
||||||
import ScanQRCode from "./ScanQRCode";
|
import ScanQRCode from "./ScanQRCode";
|
||||||
import EditAccountDetails from "./EditAccountDetails";
|
import EditAccountDetails from "./EditAccountDetails";
|
||||||
import AvatarWithFallback from "./AvatarWithFallback";
|
import AvatarWithFallback from "./AvatarWithFallback";
|
||||||
|
import {useImportManager} from "./ImportManager";
|
||||||
import useStore from "./useStorage";
|
import useStore from "./useStorage";
|
||||||
import {calculateCountdown} from "./totpUtil";
|
import {calculateCountdown} from "./totpUtil";
|
||||||
import {generateToken, validateSecret} from "./totpUtil";
|
import {generateToken, validateSecret} from "./totpUtil";
|
||||||
|
@ -57,6 +58,16 @@ export default function HomePage() {
|
||||||
const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
|
const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
|
||||||
const {notify} = useNotifications();
|
const {notify} = useNotifications();
|
||||||
|
|
||||||
|
const {showImportOptions} = useImportManager((data) => {
|
||||||
|
handleAddAccount(data);
|
||||||
|
}, (err) => {
|
||||||
|
notify("error", {
|
||||||
|
params: {title: "Import error", description: err.message},
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
setShowScanner(true);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshAccounts();
|
refreshAccounts();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -107,7 +118,7 @@ export default function HomePage() {
|
||||||
|
|
||||||
const handleAddAccount = async(accountDataInput) => {
|
const handleAddAccount = async(accountDataInput) => {
|
||||||
if (Array.isArray(accountDataInput)) {
|
if (Array.isArray(accountDataInput)) {
|
||||||
insertAccounts(accountDataInput);
|
await insertAccounts(accountDataInput);
|
||||||
} else {
|
} else {
|
||||||
await setAccount(accountDataInput);
|
await setAccount(accountDataInput);
|
||||||
await insertAccount();
|
await insertAccount();
|
||||||
|
@ -175,6 +186,11 @@ export default function HomePage() {
|
||||||
closeOptions();
|
closeOptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openImportAccountModal = () => {
|
||||||
|
showImportOptions();
|
||||||
|
closeOptions();
|
||||||
|
};
|
||||||
|
|
||||||
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
||||||
|
|
||||||
const closeSwipeableMenu = () => {
|
const closeSwipeableMenu = () => {
|
||||||
|
@ -291,11 +307,11 @@ export default function HomePage() {
|
||||||
padding: 20,
|
padding: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 150,
|
height: 225,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
left: "50%",
|
left: "50%",
|
||||||
transform: [{translateX: -150}, {translateY: -75}],
|
transform: [{translateX: -150}, {translateY: -112.5}],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -312,6 +328,13 @@ export default function HomePage() {
|
||||||
<IconButton icon={"keyboard"} size={35} />
|
<IconButton icon={"keyboard"} size={35} />
|
||||||
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
|
||||||
|
onPress={openImportAccountModal}
|
||||||
|
>
|
||||||
|
<IconButton icon={"import"} size={35} />
|
||||||
|
<Text style={{fontSize: 18}}>Import from other app</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {useActionSheet} from "@expo/react-native-action-sheet";
|
||||||
|
import {importFromMSAuth} from "./MSAuthImportLogic";
|
||||||
|
|
||||||
|
const importApps = [
|
||||||
|
{name: "Google Authenticator", useScanner: true},
|
||||||
|
{name: "Microsoft Authenticator", importFunction: importFromMSAuth},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useImportManager = (onImportComplete, onError, onOpenScanner) => {
|
||||||
|
const {showActionSheetWithOptions} = useActionSheet();
|
||||||
|
|
||||||
|
const showImportOptions = () => {
|
||||||
|
const options = [...importApps.map(app => app.name), "Cancel"];
|
||||||
|
const cancelButtonIndex = options.length - 1;
|
||||||
|
|
||||||
|
showActionSheetWithOptions(
|
||||||
|
{
|
||||||
|
options,
|
||||||
|
cancelButtonIndex,
|
||||||
|
title: "Select app to import from",
|
||||||
|
},
|
||||||
|
(selectedIndex) => {
|
||||||
|
if (selectedIndex !== cancelButtonIndex) {
|
||||||
|
const selectedApp = importApps[selectedIndex];
|
||||||
|
if (selectedApp.useScanner) {
|
||||||
|
onOpenScanner();
|
||||||
|
} else if (selectedApp.importFunction) {
|
||||||
|
selectedApp.importFunction()
|
||||||
|
.then(result => {
|
||||||
|
if (result) {onImportComplete(result);}
|
||||||
|
})
|
||||||
|
.catch(onError);
|
||||||
|
} else {
|
||||||
|
onError(new Error(`Import function not implemented for ${selectedApp.name}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {showImportOptions};
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import * as DocumentPicker from "expo-document-picker";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import {openDatabaseSync} from "expo-sqlite/next";
|
||||||
|
|
||||||
|
const SQLITE_DIR = `${FileSystem.documentDirectory}SQLite`;
|
||||||
|
|
||||||
|
const getRandomDBName = () => {
|
||||||
|
const randomString = Math.random().toString(36).substring(2, 15);
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return `${randomString}_${timestamp}.db`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDirectory = async(dir) => {
|
||||||
|
try {
|
||||||
|
if (!(await FileSystem.getInfoAsync(dir)).exists) {
|
||||||
|
await FileSystem.makeDirectoryAsync(dir, {intermediates: true});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error creating directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryMicrosoftAuthenticatorDatabase = async(db) => {
|
||||||
|
return await db.getAllAsync("SELECT name, username, oath_secret_key FROM accounts WHERE account_type = 0");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMicrosoftAuthenticatorData = (rows) => {
|
||||||
|
return rows.map(row => {
|
||||||
|
const {name, username, oath_secret_key} = row;
|
||||||
|
return {issuer: name, accountName: username, secretKey: oath_secret_key};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importFromMSAuth = async() => {
|
||||||
|
const dbName = getRandomDBName();
|
||||||
|
const internalDbName = `${SQLITE_DIR}/${dbName}`;
|
||||||
|
try {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({
|
||||||
|
multiple: false,
|
||||||
|
copyToCacheDirectory: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled) {
|
||||||
|
const file = result.assets[0];
|
||||||
|
if ((await FileSystem.getInfoAsync(file.uri)).exists) {
|
||||||
|
await createDirectory(SQLITE_DIR);
|
||||||
|
await FileSystem.copyAsync({from: file.uri, to: internalDbName});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = openDatabaseSync(dbName);
|
||||||
|
|
||||||
|
const rows = await queryMicrosoftAuthenticatorDatabase(db);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error("No data found in Microsoft Authenticator database");
|
||||||
|
}
|
||||||
|
return formatMicrosoftAuthenticatorData(rows);
|
||||||
|
} catch (dbError) {
|
||||||
|
if (dbError.message.includes("file is not a database")) {
|
||||||
|
throw new Error("file is not a database");
|
||||||
|
}
|
||||||
|
throw new Error(dbError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error importing from Microsoft Authenticator: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
await FileSystem.deleteAsync(internalDbName, {idempotent: true});
|
||||||
|
}
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -21,18 +21,19 @@
|
||||||
"casdoor-react-native-sdk": "1.1.0",
|
"casdoor-react-native-sdk": "1.1.0",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"eslint-plugin-import": "^2.28.1",
|
"eslint-plugin-import": "^2.28.1",
|
||||||
"expo": "~51.0.34",
|
"expo": "~51.0.37",
|
||||||
"expo-asset": "~10.0.10",
|
"expo-asset": "~10.0.10",
|
||||||
"expo-camera": "~15.0.16",
|
"expo-camera": "~15.0.16",
|
||||||
"expo-crypto": "~13.0.2",
|
"expo-crypto": "~13.0.2",
|
||||||
"expo-dev-client": "~4.0.27",
|
"expo-dev-client": "~4.0.28",
|
||||||
|
"expo-document-picker": "~12.0.2",
|
||||||
"expo-drizzle-studio-plugin": "^0.0.2",
|
"expo-drizzle-studio-plugin": "^0.0.2",
|
||||||
"expo-image": "~1.13.0",
|
"expo-image": "~1.13.0",
|
||||||
"expo-image-picker": "~15.0.7",
|
"expo-image-picker": "~15.0.7",
|
||||||
"expo-sqlite": "^14.0.6",
|
"expo-sqlite": "^14.0.6",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-updates": "~0.25.25",
|
"expo-updates": "~0.25.27",
|
||||||
"hi-base32": "^0.5.1",
|
"hi-base32": "^0.5.1",
|
||||||
"hotp-totp": "^1.0.6",
|
"hotp-totp": "^1.0.6",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
|
|
@ -129,7 +129,7 @@ const useEditAccountStore = create((set, get) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
insertAccounts: (accounts) => {
|
insertAccounts: async(accounts) => {
|
||||||
try {
|
try {
|
||||||
db.transaction((tx) => {
|
db.transaction((tx) => {
|
||||||
const insertWithDuplicateCheck = (baseAccName, issuer, secretKey) => {
|
const insertWithDuplicateCheck = (baseAccName, issuer, secretKey) => {
|
||||||
|
|
Loading…
Reference in New Issue