Skip to content

Commit

Permalink
feat: support importing entried from other authenticators (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
IZUMI-Zu authored Oct 10, 2024
1 parent e3f7ab5 commit 308b546
Show file tree
Hide file tree
Showing 6 changed files with 5,492 additions and 3,391 deletions.
29 changes: 26 additions & 3 deletions HomePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import EnterAccountDetails from "./EnterAccountDetails";
import ScanQRCode from "./ScanQRCode";
import EditAccountDetails from "./EditAccountDetails";
import AvatarWithFallback from "./AvatarWithFallback";
import {useImportManager} from "./ImportManager";
import useStore from "./useStorage";
import {calculateCountdown} from "./totpUtil";
import {generateToken, validateSecret} from "./totpUtil";
Expand Down Expand Up @@ -57,6 +58,16 @@ export default function HomePage() {
const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
const {notify} = useNotifications();

const {showImportOptions} = useImportManager((data) => {
handleAddAccount(data);
}, (err) => {
notify("error", {
params: {title: "Import error", description: err.message},
});
}, () => {
setShowScanner(true);
});

useEffect(() => {
refreshAccounts();
}, []);
Expand Down Expand Up @@ -107,7 +118,7 @@ export default function HomePage() {

const handleAddAccount = async(accountDataInput) => {
if (Array.isArray(accountDataInput)) {
insertAccounts(accountDataInput);
await insertAccounts(accountDataInput);
} else {
await setAccount(accountDataInput);
await insertAccount();
Expand Down Expand Up @@ -175,6 +186,11 @@ export default function HomePage() {
closeOptions();
};

const openImportAccountModal = () => {
showImportOptions();
closeOptions();
};

const closeEnterAccountModal = () => setShowEnterAccountModal(false);

const closeSwipeableMenu = () => {
Expand Down Expand Up @@ -291,11 +307,11 @@ export default function HomePage() {
padding: 20,
borderRadius: 10,
width: 300,
height: 150,
height: 225,
position: "absolute",
top: "50%",
left: "50%",
transform: [{translateX: -150}, {translateY: -75}],
transform: [{translateX: -150}, {translateY: -112.5}],
}}
>
<TouchableOpacity
Expand All @@ -312,6 +328,13 @@ export default function HomePage() {
<IconButton icon={"keyboard"} size={35} />
<Text style={{fontSize: 18}}>Enter Secret code</Text>
</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>
</Portal>

Expand Down
56 changes: 56 additions & 0 deletions ImportManager.js
Original file line number Diff line number Diff line change
@@ -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};
};
84 changes: 84 additions & 0 deletions MSAuthImportLogic.js
Original file line number Diff line number Diff line change
@@ -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});
}
};
Loading

0 comments on commit 308b546

Please sign in to comment.