Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,22 @@
"message": "Import Backup File",
"description": "Import backup file."
},
"import_backup_qr": {
"message": "Import QR Image Backup",
"description": "Import qr image backup."
},
"import_backup_code": {
"message": "Import Text Backup",
"description": "Import backup code."
},
"import_backup_qr_partly_failed": {
"message": "Import successfuly, but some QR image cannot be recognized.",
"description": "Import successfuly, but some QR image cannot be recognized."
},
"import_backup_qr_in_batches": {
"message": "You can select multiple files to import backup in batches.",
"description": "You can select multiple image files to import backup in batches."
},
"show_all_entries": {
"message": "Show all entries",
"description": "Show all entries."
Expand Down
2 changes: 1 addition & 1 deletion sass/import.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ p {
}

#import {
width: 600px;
width: 900px;
position: relative;
margin: 0 auto;
}
Expand Down
9 changes: 9 additions & 0 deletions src/components/Import.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
v-model="importType"
/>
<label for="import_file_radio">{{ i18n.import_backup_file }}</label>
<input
type="radio"
id="import_qr_radio"
value="QrImport"
v-model="importType"
/>
<label for="import_qr_radio">{{ i18n.import_backup_qr }}</label>
<input
type="radio"
id="import_code_radio"
Expand Down Expand Up @@ -37,6 +44,7 @@
<script lang="ts">
import Vue from "vue";
import FileImport from "./Import/FileImport.vue";
import QrImport from "./Import/QrImport.vue";
import TextImport from "./Import/TextImport.vue";

export default Vue.extend({
Expand All @@ -48,6 +56,7 @@ export default Vue.extend({
},
components: {
FileImport,
QrImport,
TextImport
},
mounted() {
Expand Down
154 changes: 154 additions & 0 deletions src/components/Import/QrImport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<template>
<div>
<p style="margin: 10px 20px 20px 20px;">
{{ i18n.import_backup_qr_in_batches }}
</p>
<div class="import_file">
<label for="import_qr">{{ i18n.import_backup_qr }}</label>
<input
id="import_qr"
type="file"
v-on:change="importQr($event, true)"
accept="image/*"
multiple
/>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
// @ts-ignore
import QRCode from "qrcode-reader";
import jsQR from "jsqr";
import { getEntryDataFromOTPAuthPerLine } from "../../import";
import { EntryStorage } from "../../models/storage";
import { Encryption } from "../../models/encryption";

export default Vue.extend({
methods: {
async importQr(event: Event, closeWindow: Boolean) {
const target = event.target as HTMLInputElement;
if (!target || !target.files) {
return;
}
if (target.files.length) {
const otpUrlList: string[] = [];
let hasFailedResults = false;
for (let fileIndex = 0; fileIndex < target.files.length; fileIndex++) {
const file = target.files[fileIndex];
const otpUrl = await getOtpUrlFromQrFile(file);
if (otpUrl !== null) {
otpUrlList.push(otpUrl);
} else {
hasFailedResults = true;
}
}

let importData: {
// @ts-ignore
key?: { enc: string; hash: string };
[hash: string]: OTPStorage;
} = await getEntryDataFromOTPAuthPerLine(otpUrlList.join("\n"));

let decryptedFileData: { [hash: string]: OTPStorage } = importData;

if (Object.keys(decryptedFileData).length) {
await EntryStorage.import(
this.$encryption as Encryption,
decryptedFileData
);

if (hasFailedResults) {
alert(this.i18n.import_backup_qr_partly_failed);
} else {
alert(this.i18n.updateSuccess);
}

if (closeWindow) {
window.close();
}
} else {
alert(this.i18n.errorqr);
if (closeWindow) {
window.close();
}
}
} else {
alert(this.i18n.updateFailure);
if (closeWindow) {
window.alert(this.i18n.updateFailure);
window.close();
}
}
return;
}
}
});

async function getOtpUrlFromQrFile(file: File): Promise<string | null> {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
const imageUrl = reader.result as string;
const qrReader = new QRCode();
qrReader.callback = (
error: string,
text: {
result: string;
points: Array<{
x: number;
y: number;
count: number;
estimatedModuleSize: number;
}>;
}
) => {
if (error) {
console.error(error);

const image: HTMLImageElement = document.createElement("img");
image.onload = () => {
const canvas: HTMLCanvasElement = document.createElement("canvas");
const ctx: CanvasRenderingContext2D = canvas.getContext(
"2d"
) as CanvasRenderingContext2D;

canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);

const qrImageData = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);
const jsQrCode = jsQR(
qrImageData.data,
canvas.width,
canvas.height
);

if (jsQrCode && jsQrCode.data) {
if (jsQrCode.data.indexOf("otpauth://") !== 0) {
return resolve(null);
}
return resolve(jsQrCode.data);
} else {
return resolve(null);
}
};
image.src = imageUrl;
} else {
if (text.result.indexOf("otpauth://") !== 0) {
return resolve(null);
}
return resolve(text.result);
}
};
qrReader.decode(imageUrl);
};
reader.readAsDataURL(file);
});
}
</script>