diff --git a/.gitignore b/.gitignore index d2335a07e..9528693d0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build chrome* firefox* edge* +safari* dist .vscode .atom-build.yml diff --git a/manifests/manifest-safari.json b/manifests/manifest-safari.json new file mode 100644 index 000000000..e4e650533 --- /dev/null +++ b/manifests/manifest-safari.json @@ -0,0 +1,76 @@ +{ + "manifest_version": 3, + "name": "__MSG_extName__", + "short_name": "__MSG_extShortName__", + "version": "8.0.2", + "default_locale": "en", + "description": "__MSG_extDesc__", + "icons": { + "16": "images/icon16.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "action": { + "default_icon": { + "19": "images/icon19.png", + "38": "images/icon38.png" + }, + "default_title": "__MSG_extShortName__", + "default_popup": "view/popup.html" + }, + "commands": { + "_execute_action": {}, + "scan-qr": { + "description": "Scan a QR code" + }, + "autofill": { + "description": "Autofill the matched code" + } + }, + "options_ui": { + "page": "view/options.html", + "open_in_tab": false + }, + "storage": { + "managed_schema": "schema.json" + }, + "oauth2": { + "client_id": "292457304165-u8ve4j79gag5o231n5u2pdtdrbfdo1hh.apps.googleusercontent.com", + "scopes": [ + "https://www.googleapis.com/auth/drive.file" + ] + }, + "background": { + "service_worker": "dist/background.js" + }, + "sandbox": { + "pages": [ + "view/argon.html" + ] + }, + "permissions": [ + "activeTab", + "storage", + "identity", + "alarms", + "scripting" + ], + "host_permissions": [ + "https://www.google.com/", + "https://www.googleapis.com/*", + "https://accounts.google.com/*" + ], + "optional_permissions": [ + "clipboardWrite", + "contextMenus" + ], + "optional_host_permissions": [ + "https://*.dropboxapi.com/*", + "https://graph.microsoft.com/me/*", + "https://login.microsoftonline.com/common/oauth2/v2.0/token" + ], + "content_security_policy": { + "extension_pages": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/ https://oauth2.googleapis.com/ https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'", + "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval';" + } +} diff --git a/package.json b/package.json index 4970fa5be..f1cdd4cc2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "compile": "bash scripts/build.sh firefox", "dev:chrome": "npm run pretest && webpack --config ./webpack.watch.js", "chrome": "bash scripts/build.sh chrome", + "safari": "bash scripts/build.sh safari", "firefox": "bash scripts/build.sh firefox", "edge": "bash scripts/build.sh edge", "prod": "bash scripts/build.sh prod", diff --git a/scripts/build.sh b/scripts/build.sh index 3da00c0e2..80a324976 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -13,14 +13,14 @@ CREDREGEX='^.*".+".*".+".*".+".*".+".*".+".*$' STYLEFILES="./src/* ./src/**/* ./src/**/**/* ./src/**/**/**/* ./sass/*.scss" set -e -if [[ $PLATFORM != "chrome" ]] && [[ $PLATFORM != "firefox" ]] && [[ $PLATFORM != "edge" ]] && [[ $PLATFORM != "prod" ]] && [[ $PLATFORM != "test" ]]; then - echo "Invalid platform type. Supported platforms are 'chrome', 'firefox', 'test', and 'prod'" +if [[ $PLATFORM != "chrome" ]] && [[ $PLATFORM != "firefox" ]] && [[ $PLATFORM != "edge" ]] && [[ $PLATFORM != "safari" ]] && [[ $PLATFORM != "prod" ]] && [[ $PLATFORM != "test" ]]; then + echo "Invalid platform type. Supported platforms are 'chrome', 'firefox', 'safari', 'test', and 'prod'" exit 1 fi echo "Removing old build files..." rm -rf build dist -rm -rf firefox chrome edge release test +rm -rf firefox chrome edge safari release test echo "Checking style..." if ./node_modules/.bin/prettier --check $STYLEFILES 1> /dev/null ; then true @@ -39,7 +39,7 @@ if ! [[ $CREDS =~ $CREDREGEX ]] ; then fi fi -if ! [[ $REMOTE = *"https://github.com/Authenticator-Extension/Authenticator.git"* || $REMOTE = *"git@github.com:Authenticator-Extension/Authenticator.git"* || $CI ]] ; then +if [[ $PLATFORM != "safari" ]] && ! [[ $REMOTE = *"https://github.com/Authenticator-Extension/Authenticator.git"* || $REMOTE = *"git@github.com:Authenticator-Extension/Authenticator.git"* || $CI ]] ; then echo echo -e "\e[7m\033[33mNotice\033[0m" echo @@ -80,7 +80,7 @@ postCompile () { cp manifests/manifest-$1.json $1/manifest.json fi - if [[ $1 = "chrome" ]] || [[ $1 = "edge" ]]; then + if [[ $1 = "chrome" ]] || [[ $1 = "edge" ]] || [[ $1 = "safari" ]]; then cp manifests/schema-chrome.json $1/schema.json fi diff --git a/src/argon.ts b/src/argon.ts index ea27b3967..5693cabb2 100644 --- a/src/argon.ts +++ b/src/argon.ts @@ -1,10 +1,18 @@ import argon2 from "argon2-browser"; +function isExtensionOrigin(origin: string): boolean { + return ( + origin.indexOf("chrome-extension://") === 0 || + origin.indexOf("safari-web-extension://") === 0 || + origin.indexOf("moz-extension://") === 0 + ); +} + window.addEventListener("message", (event) => { const message = event.data; const source = event.source as Window; - if (!source) { + if (!source || !isExtensionOrigin(event.origin)) { return; } diff --git a/src/background.ts b/src/background.ts index bb59a2f9d..fb78749eb 100644 --- a/src/background.ts +++ b/src/background.ts @@ -11,7 +11,13 @@ import { import { CodeState } from "./models/otp"; import { getOTPAuthPerLineFromOPTAuthMigration } from "./models/migration"; -import { isChrome, isFirefox } from "./browser"; +import { + containingAppId, + driveRedirect, + isChrome, + isFirefox, + isSafari, +} from "./browser"; import { UserSettings } from "./models/settings"; let contentTab: chrome.tabs.Tab | undefined; @@ -19,6 +25,18 @@ let contentTab: chrome.tabs.Tab | undefined; chrome.runtime.onMessage.addListener(async (message, sender) => { await UserSettings.updateItems(); + if ( + message.action === "cachePassphrase" || + message.action === "lock" || + message.action === "dropbox" || + message.action === "drive" || + message.action === "onedrive" + ) { + if (sender.id !== chrome.runtime.id || sender.tab) { + return true; + } + } + if (message.action === "getCapture") { if (!sender.tab) { return; @@ -240,7 +258,79 @@ async function getTotp(text: string, silent = false) { } } +async function exchangeDriveCode(code: string, encodedRedirect: string) { + const response = await fetch( + "https://www.googleapis.com/oauth2/v4/token?client_id=" + + getCredentials().drive.client_id + + "&client_secret=" + + getCredentials().drive.client_secret + + "&code=" + + code + + "&redirect_uri=" + + encodedRedirect + + "&grant_type=authorization_code", + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + } + ); + const res = await response.json(); + if (res.error) { + console.error(res.error_description); + return false; + } + UserSettings.items.driveToken = res.access_token; + UserSettings.items.driveRefreshToken = res.refresh_token; + await UserSettings.commitItems(); + return true; +} + +function signInDriveSafari() { + const encodedRedirect = encodeURIComponent(driveRedirect); + const authUrl = + "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=" + + getCredentials().drive.client_id + + "&scope=https%3A//www.googleapis.com/auth/drive.file&prompt=consent&redirect_uri=" + + encodedRedirect; + chrome.runtime.sendNativeMessage( + containingAppId, + { action: "drive-auth", url: authUrl }, + async (response: { ok?: boolean; url?: string }) => { + if (!response || !response.ok || !response.url) { + return; + } + let callback: URL; + try { + callback = new URL(response.url); + } catch (error) { + console.error(error); + return; + } + if (callback.origin + callback.pathname !== driveRedirect) { + return; + } + const code = callback.searchParams.get("code"); + if (!code) { + return; + } + const exchanged = await exchangeDriveCode(code, encodedRedirect); + if (!exchanged) { + return; + } + chrome.runtime.sendMessage({ action: "drivetoken" }); + uploadBackup("drive"); + } + ); +} + function getBackupToken(service: string) { + if (isSafari && service === "drive") { + signInDriveSafari(); + return; + } if (isChrome && service === "drive") { chrome.identity.getAuthToken( { diff --git a/src/browser.ts b/src/browser.ts index c8b9fc7a2..ab1772c36 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -4,6 +4,8 @@ export const isEdge = navigator.userAgent.indexOf("Edg") >= 0; export const isChromium = navigator.userAgent.indexOf("Chrome") >= 0; export const isSafari = !isChromium && navigator.userAgent.indexOf("Safari") >= 0; +export const containingAppId = "com.yourCompany.Authenticator"; +export const driveRedirect = "https://authenticator.cc/oauth"; export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1 && navigator.userAgent.indexOf("Edg") === -1; diff --git a/src/components/Popup/BackupPage.vue b/src/components/Popup/BackupPage.vue index 8e718b46b..358d8765d 100644 --- a/src/components/Popup/BackupPage.vue +++ b/src/components/Popup/BackupPage.vue @@ -57,14 +57,24 @@ {{ i18n.import_backup }}
-
+
{{ i18n.storage_sync_info }}

Google Drive - OneDrive - Dropbox + + OneDrive + + + Dropbox +
@@ -119,6 +129,11 @@ export default Vue.extend({ ); return; } else if (tab === "DrivePage") { + if (isSafari) { + this.$store.commit("style/showInfo"); + this.$store.commit("currentView/changeView", tab); + return; + } chrome.permissions.request( { origins: [ diff --git a/src/components/Popup/DrivePage.vue b/src/components/Popup/DrivePage.vue index 351a61bbf..0e74a05b5 100644 --- a/src/components/Popup/DrivePage.vue +++ b/src/components/Popup/DrivePage.vue @@ -100,6 +100,7 @@ export default Vue.extend({ xhr.send(); }); UserSettings.removeItem("driveToken"); + UserSettings.removeItem("driveRefreshToken"); this.$store.commit("backup/setToken", { service, value: false }); this.$store.commit("style/hideInfo"); }, @@ -130,6 +131,15 @@ export default Vue.extend({ if (this.backupToken) { this.email = await this.getUser(); } + chrome.runtime.onMessage.addListener((message) => { + if (message.action !== "drivetoken") { + return; + } + this.$store.commit("backup/setToken", { service, value: true }); + this.getUser().then((email: string) => { + this.email = email; + }); + }); }, }); diff --git a/src/models/storage.ts b/src/models/storage.ts index 4b3699fb2..ad92edba4 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -1,10 +1,43 @@ import { Encryption } from "./encryption"; import { OTPEntry, OTPType, OTPAlgorithm, CodeState } from "./otp"; +import { isSafari } from "../browser"; import { StorageLocation, UserSettings } from "./settings"; import { DataType } from "./otp"; export class BrowserStorage { + private static async migrateSyncToLocal(): Promise { + const flag = await chrome.storage.local.get("safariStorageMigrated"); + if (flag.safariStorageMigrated === true) { + return; + } + const sync = await chrome.storage.sync.get(); + const syncKeys = Object.keys(sync); + const local = await chrome.storage.local.get(); + const localKeys = Object.keys(local).filter( + (key) => key !== "safariStorageMigrated" + ); + if (syncKeys.length === 0 || localKeys.length > 0) { + await chrome.storage.local.set({ safariStorageMigrated: true }); + return; + } + await chrome.storage.local.set(sync); + const copied = await chrome.storage.local.get(syncKeys); + if (Object.keys(copied).length !== syncKeys.length) { + return; + } + await chrome.storage.sync.remove(syncKeys); + await chrome.storage.local.set({ safariStorageMigrated: true }); + } + private static async getStorageLocation(): Promise { await UserSettings.updateItems(); + if (isSafari) { + await this.migrateSyncToLocal(); + if (UserSettings.items.storageLocation !== StorageLocation.Local) { + UserSettings.items.storageLocation = StorageLocation.Local; + await UserSettings.commitItems(); + } + return StorageLocation.Local; + } const managedLocation = await ManagedStorage.get( "storageArea" ); diff --git a/src/popup.ts b/src/popup.ts index 173bc033e..44484cf5c 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -18,6 +18,7 @@ import { Notification } from "./store/Notification"; import { Qr } from "./store/Qr"; import { Advisor } from "./store/Advisor"; import { Dropbox, Drive, OneDrive } from "./models/backup"; +import { isSafari } from "./browser"; import { syncTimeWithGoogle } from "./syncTime"; import { StorageLocation, UserSettings } from "./models/settings"; @@ -241,46 +242,48 @@ async function runScheduledBackup(clientTime: number, instance: Vue) { ); } if (instance.$store.state.backup.driveToken) { - chrome.permissions.contains( - { - origins: [ - "https://www.googleapis.com/*", - "https://accounts.google.com/o/oauth2/revoke", - ], - }, - async (hasPermission) => { - if (hasPermission) { - try { - const drive = new Drive(); - const res = await drive.upload( - instance.$store.state.accounts.encryption.get( - instance.$store.state.accounts.defaultEncryption - ) + const uploadDrive = async (hasPermission: boolean) => { + if (hasPermission) { + try { + const drive = new Drive(); + const res = await drive.upload( + instance.$store.state.accounts.encryption.get( + instance.$store.state.accounts.defaultEncryption + ) + ); + if (res) { + UserSettings.items.lastRemindingBackupTime = clientTime; + UserSettings.commitItems(); + return; + } else if (UserSettings.items.driveRevoked === true) { + instance.$store.commit( + "notification/alert", + chrome.i18n.getMessage("token_revoked", ["Google Drive"]) ); - if (res) { - UserSettings.items.lastRemindingBackupTime = clientTime; - UserSettings.commitItems(); - return; - } else if (UserSettings.items.driveRevoked === true) { - instance.$store.commit( - "notification/alert", - chrome.i18n.getMessage("token_revoked", ["Google Drive"]) - ); - UserSettings.items.driveRevoked = undefined; - UserSettings.removeItem("driveRevoked"); - } - } catch (error) { - // ignore + UserSettings.items.driveRevoked = undefined; + UserSettings.removeItem("driveRevoked"); } + } catch (error) { + // ignore } - instance.$store.commit( - "notification/alert", - instance.i18n.remind_backup - ); - UserSettings.items.lastRemindingBackupTime = clientTime; - UserSettings.commitItems(); } - ); + instance.$store.commit("notification/alert", instance.i18n.remind_backup); + UserSettings.items.lastRemindingBackupTime = clientTime; + UserSettings.commitItems(); + }; + if (isSafari) { + uploadDrive(true); + } else { + chrome.permissions.contains( + { + origins: [ + "https://www.googleapis.com/*", + "https://accounts.google.com/o/oauth2/revoke", + ], + }, + uploadDrive + ); + } } if (instance.$store.state.backup.oneDriveToken) { chrome.permissions.contains(