Skip to content

Commit

Permalink
Import from screen capture #161
Browse files Browse the repository at this point in the history
  • Loading branch information
Levminer committed Dec 9, 2021
1 parent f9fcdfd commit ddf878e
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 32 deletions.
30 changes: 17 additions & 13 deletions app/import/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h4>You can create your import file from QR codes and Google Authenticator QR co
<div class="mx-auto rounded-2xl bg-gray-800 w-2/3 mb-20 mt-20" id="choose">
<h2 class="pt-5">Instructions</h2>
<h3>You can create your import file here. For more information please read the documentation.</h3>
<button class="buttoni" id="but0" onclick="onlineDocs()">
<button class="buttoni mb-8" onclick="onlineDocs()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Expand All @@ -61,26 +61,31 @@ <h4>
<br />
If you need more steps or help:
<br />
<br />
<a href="#qrLink" onclick="qrLink()">Detailed steps</a>
<a class="link" href="#qrLink" onclick="qrLink()">Detailed steps</a>
</h4>
</details>
<div class="flex flex-row justify-center gap-3 flex-wrap">
<button class="buttoni" id="but0" onclick="qrImport()">
<button class="buttoni mb-5" onclick="qrImport()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
Choose image(s)
</button>
<button class="buttoni mb-5" id="but2" onclick="qrCamera()">
<button class="buttoni mb-5" onclick="qrCamera()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Use webcam
</button>
<button class="buttoni mb-5 screenCapture hidden" onclick="qrScreen()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Screen capture
</button>
</div>
<video id="qrCamera" class="hidden mx-auto" autoplay></video>
<video id="qrVideo" class="hidden mx-auto" autoplay></video>
<button class="buttoni mb-5 mt-5 hidden" id="qrStop">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
Expand All @@ -92,34 +97,33 @@ <h4>
<div class="mx-auto rounded-2xl bg-gray-800 w-2/3 mb-20" id="choose">
<h2 class="pt-5">Advanced import</h2>
<h3>Google Authenticator QR code</h3>
<details class="details1" id="dt">
<summary class="summary1">Instructions</summary>
<details class="details mb-8">
<summary class="summary">Instructions</summary>
<h4>
Tap on the three dots on the top right corner of the screen and export your accounts, take a picture of the export QR code(s), and choose the image(s) you saved and transferred to your computer.
<br />
<br />
If you need more steps or help:
<br />
<br />
<a href="#gaLink" onclick="gaLink()">Detailed steps</a>
<a class="link" href="#gaLink" onclick="gaLink()">Detailed steps</a>
</h4>
</details>
<div class="flex flex-row justify-center gap-3 flex-wrap">
<button class="buttoni" id="but1" onclick="gaImport()">
<button class="buttoni mb-5" onclick="gaImport()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
Choose image(s)
</button>
<button class="buttoni mb-5" id="but3" onclick="gaCamera()">
<button class="buttoni mb-5" onclick="gaCamera()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Use webcam
</button>
</div>
<video id="gaCamera" class="hidden mx-auto" autoplay></video>
<video id="gaVideo" class="hidden mx-auto" autoplay></video>
<button class="buttoni mb-5 mt-5 hidden" id="gaStop">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
Expand Down
11 changes: 0 additions & 11 deletions app/import/src/css/index.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
#but0,
#but1,
#but2,
#but3 {
margin-bottom: 30px;
}

#dt {
margin-bottom: 30px;
}

video {
border-radius: 30px;
width: 80%;
Expand Down
2 changes: 1 addition & 1 deletion app/import/src/js/ga.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const gaCamera = () => {

return logger.error("Not found webcam")
} else {
const video = document.querySelector("#gaCamera")
const video = document.querySelector("#gaVideo")
const button = document.querySelector("#gaStop")

const reader = new QrcodeDecoder()
Expand Down
22 changes: 20 additions & 2 deletions app/import/src/js/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { app, dialog, shell } = require("@electron/remote")
const { app, dialog, shell, desktopCapturer, BrowserWindow } = require("@electron/remote")
const QrcodeDecoder = require("qrcode-decoder").default
const logger = require("@levminer/lib/logger/renderer")
const electron = require("electron")
Expand All @@ -22,6 +22,9 @@ if (app.isPackaged === false) {
dev = true
}

// Get current window
const currentWindow = BrowserWindow.getFocusedWindow()

/**
* Get Authme folder path
*/
Expand Down Expand Up @@ -60,10 +63,25 @@ const hide = () => {
ipc.send("toggleImport")
}

// ? build
/**
* Get app information
*/
const res = ipc.sendSync("info")

/**
* Show build number if version is pre release
*/
if (res.build_number.startsWith("alpha")) {
document.querySelector(".build-content").textContent = `You are running an alpha version of Authme - Version ${res.authme_version} - Build ${res.build_number}`
document.querySelector(".build").style.display = "block"
} else if (res.build_number.startsWith("beta")) {
document.querySelector(".build-content").textContent = `You are running a beta version of Authme - Version ${res.authme_version} - Build ${res.build_number}`
document.querySelector(".build").style.display = "block"
}

/**
* Show experimental import screen capture
*/
if (settings.experimental.screen_capture === true) {
document.querySelector(".screenCapture").style.display = "block"
}
150 changes: 149 additions & 1 deletion app/import/src/js/qr.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const qrCamera = () => {

return logger.error("Not found webcam")
} else {
const video = document.querySelector("#qrCamera")
const video = document.querySelector("#qrVideo")
const button = document.querySelector("#qrStop")

const reader = new QrcodeDecoder()
Expand Down Expand Up @@ -280,3 +280,151 @@ const qrCamera = () => {
}
})
}

/**
* Read QR code from screen capture
*/
const qrScreen = () => {
desktopCapturer.getSources({ types: ["screen"] }).then(async (sources) => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[0].id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720,
},
},
})

qrHandleStream(stream)
} catch (error) {
dialog.showMessageBox({
title: "Authme",
buttons: ["Close"],
type: "error",
message: `Error starting capture! \n\n${error}`,
})

logger.error("Error starting capture!", error.stack)
}
})

const qrHandleStream = async (stream) => {
const track = stream.getTracks()[0]

const video = document.querySelector("#qrVideo")
video.style.display = "flex"
video.srcObject = stream

const button = document.querySelector("#qrStop")
button.style.display = "inline"

button.addEventListener("click", () => {
video.style.display = "none"
button.style.display = "none"

reader.stop()
track.stop()
})

await dialog.showMessageBox(currentWindow, {
title: "Authme",
buttons: ["Close"],
type: "info",
defaultId: 0,
message: "Authme is trying to detect a QR code from your screen! \n\nMake sure the QR code is visible.",
})

const reader = new QrcodeDecoder()

const res = await reader.decodeFromVideo(video)

if (res.data.startsWith("otpauth://totp/")) {
// construct
let url = res.data.replaceAll(/\s/g, "")
url = url.slice(15)

// get name
const name_index = url.match(/[?]/)
const name = url.slice(0, name_index.index)
url = url.slice(name.length + 1)

// get secret
const secret_index = url.match(/[&]/)
const secret = url.slice(7, secret_index.index)
url = url.slice(secret.length + 14 + 1)

// get issuer
const issuer = url

const str = `\nName: ${name} \nSecret: ${secret} \nIssuer: ${issuer} \nType: OTP_TOTP\n`

dialog
.showMessageBox(currentWindow, {
title: "Authme",
buttons: ["Close"],
type: "info",
defaultId: 0,
message: "QR codes found on camera! \n\nNow select where do you want to save the file!",
})
.then(() => {
dialog
.showSaveDialog(currentWindow, {
title: "Save file",
filters: [{ name: "Authme file", extensions: ["authme"] }],
defaultPath: "~/import.authme",
})
.then((result) => {
canceled = result.canceled
output = result.filePath

/**
* .authme import file
* @type {LibAuthmeFile}
*/
const save_file = {
role: "import",
encrypted: false,
codes: Buffer.from(str).toString("base64"),
date: time.timestamp(),
version: 3,
}

if (canceled === false) {
fs.writeFile(output, JSON.stringify(save_file, null, "\t"), (err) => {
if (err) {
logger.error(`Error creating file - ${err}`)
} else {
logger.log("File created")
}
})
} else {
return logger.warn("Saving canceled")
}
})
})
} else if (res.data !== "") {
dialog.showMessageBox(currentWindow, {
title: "Authme",
buttons: ["Close"],
type: "error",
message: "Wrong QR code found on camera! \n\nMake sure this is a correct QR code and try again!",
})

return logger.error("Wrong QR code found (QR)")
}

if (res.data !== "") {
video.style.display = "none"
button.style.display = "none"

reader.stop()
track.stop()
}
}
}
3 changes: 1 addition & 2 deletions lib/typedef.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
* @property {Boolean} settings.search_bar_filter.description - Filter for description
*
* @property {Object} experimental - Experimental
* @property {Null|Number} experimental.offset - Codes time offset
* @property {Null|Number} experimental.sort - Sort codes
* @property {Null|Number} experimental.webcam - Webcam
* @property {Boolean} experimental.screen_capture - Import screen capture
*
* @property {Object} security - Security
* @property {Null|Boolean} security.require_password - Requires password
Expand Down
1 change: 1 addition & 0 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const settings_file = {
},
experimental: {
sort: null,
screen_capture: false,
},
security: {
require_password: null,
Expand Down
Loading

0 comments on commit ddf878e

Please sign in to comment.