Skip to content
Open
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
87 changes: 32 additions & 55 deletions src/components/Import/QrImport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,74 +92,51 @@ export default Vue.extend({
},
});

function isOtpAuthUrl(text: string): boolean {
return (
text.indexOf("otpauth://") === 0 ||
text.indexOf("otpauth-migration://") === 0
);
}

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;
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);
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
);
const qrImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// jsQR decodes byte-mode QR data as UTF-8, unlike qrcode-reader below
// which decodes it as Latin-1 and garbles non-ASCII text (e.g. Chinese).
const jsQrCode = jsQR(qrImageData.data, canvas.width, canvas.height);

if (jsQrCode && jsQrCode.data) {
if (
jsQrCode.data.indexOf("otpauth://") !== 0 &&
jsQrCode.data.indexOf("otpauth-migration://") !== 0
) {
return resolve(null);
}
return resolve(jsQrCode.data);
} else {
return resolve(null);
}
};
image.src = imageUrl;
} else {
if (
text.result.indexOf("otpauth://") !== 0 &&
text.result.indexOf("otpauth-migration://") !== 0
) {
if (jsQrCode && jsQrCode.data && isOtpAuthUrl(jsQrCode.data)) {
return resolve(jsQrCode.data);
}

// fallback: qrcode-reader can decode some images jsQR fails on,
// but mangles non-ASCII text - only used when jsQR finds nothing.
const qrReader = new QRCode();
qrReader.callback = (error: string, text: { result: string }) => {
if (error || !isOtpAuthUrl(text.result)) {
return resolve(null);
}
return resolve(text.result);
}
};
qrReader.decode(imageUrl);
};
qrReader.decode(imageUrl);
image.src = imageUrl;
};
reader.readAsDataURL(file);
});
Expand Down
55 changes: 53 additions & 2 deletions src/components/Popup/EntryComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,23 @@
>
<IconRedo />
</div>
<div class="issuer">
<div
class="issuer"
v-on:mousedown="startLongPress('issuer')"
v-on:mouseup="cancelLongPress"
v-on:mouseleave="cancelLongPress"
v-on:touchstart="startLongPress('issuer')"
v-on:touchend="cancelLongPress"
v-on:touchmove="cancelLongPress"
>
{{
entry.issuer.split("::")[0] +
(theme === "compact" ? ` (${entry.account})` : "")
}}
</div>
<div class="issuerEdit">
<input
ref="issuerInput"
v-bind:placeholder="i18n.issuer"
type="text"
v-model="entry.issuer"
Expand All @@ -60,9 +69,20 @@
}"
v-html="style.isEditing ? showBulls(entry) : showCode(entry.code)"
></div>
<div class="issuer account">{{ entry.account }}</div>
<div
class="issuer account"
v-on:mousedown="startLongPress('account')"
v-on:mouseup="cancelLongPress"
v-on:mouseleave="cancelLongPress"
v-on:touchstart="startLongPress('account')"
v-on:touchend="cancelLongPress"
v-on:touchmove="cancelLongPress"
>
{{ entry.account }}
</div>
<div class="issuerEdit">
<input
ref="accountInput"
v-bind:placeholder="i18n.accountName"
type="text"
v-model="entry.account"
Expand Down Expand Up @@ -116,13 +136,44 @@ for (const module of computedPrototype) {
Object.assign(computed, module);
}

const LONG_PRESS_MS = 500;

export default Vue.extend({
computed,
props: {
entry: OTPEntry,
tabindex: Number,
},
data() {
return {
longPressTimer: null as number | null,
};
},
methods: {
startLongPress(field: "issuer" | "account") {
this.cancelLongPress();
this.longPressTimer = window.setTimeout(() => {
this.enableRename(field);
}, LONG_PRESS_MS);
},
cancelLongPress() {
if (this.longPressTimer !== null) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
},
enableRename(field: "issuer" | "account") {
if (!this.$store.state.style.style.isEditing) {
this.$store.commit("style/toggleEdit");
}
this.$nextTick(() => {
const input = (field === "issuer"
? this.$refs.issuerInput
: this.$refs.accountInput) as HTMLInputElement;
input.focus();
input.select();
});
},
noCopy(code: string) {
return (
code === CodeState.Encrypted ||
Expand Down
58 changes: 25 additions & 33 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,43 +240,35 @@ async function qrDecode(
canvas.height = imageData.height;
canvas.getContext("2d")?.putImageData(imageData, 0, 0);

const qrReader = new QRCode();
qrReader.callback = (
error: string,
text: {
result: string;
points: Array<{
x: number;
y: number;
count: number;
estimatedModuleSize: number;
}>;
}
) => {
let qrRes = "";
if (error) {
console.error(error);
const jsQrCode = jsQR(
imageData.data,
imageData.width,
imageData.height
);

if (jsQrCode) {
qrRes = jsQrCode.data;
} else {
alert(chrome.i18n.getMessage("errorqr"));
}
} else {
qrRes = text.result;
}
// jsQR decodes byte-mode QR data as UTF-8, unlike qrcode-reader below
// which decodes it as Latin-1 and garbles non-ASCII text (e.g. Chinese).
const jsQrCode = jsQR(imageData.data, imageData.width, imageData.height);

if (jsQrCode) {
chrome.runtime.sendMessage({
action: "getTotp",
info: qrRes,
info: jsQrCode.data,
});
};
qrReader.decode(imageData);
} else {
// fallback: qrcode-reader can decode some images jsQR fails on,
// but mangles non-ASCII text - only used when jsQR finds nothing.
const qrReader = new QRCode();
qrReader.callback = (error: string, text: { result: string }) => {
let qrRes = "";
if (error) {
console.error(error);
alert(chrome.i18n.getMessage("errorqr"));
} else {
qrRes = text.result;
}

chrome.runtime.sendMessage({
action: "getTotp",
info: qrRes,
});
};
qrReader.decode(imageData);
}
}
};
qr.src = url;
Expand Down
4 changes: 3 additions & 1 deletion src/models/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ function wordArrayToByteArray(wordArray: CryptoJS.lib.WordArray) {
}

function byteArray2String(bytes: number[]) {
return String.fromCharCode.apply(null, bytes);
// account/issuer are UTF-8 encoded in the migration protobuf;
// String.fromCharCode treats each byte as Latin-1, garbling non-ASCII text.
return new TextDecoder().decode(new Uint8Array(bytes));
}

function subBytesArray(bytes: number[], start: number, length: number) {
Expand Down