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
20 changes: 17 additions & 3 deletions src/components/Popup/BackupPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<script lang="ts">
import Vue from "vue";
import { isSafari } from "../../browser";
import { OTPUtil } from "../../models/otp";

export default Vue.extend({
data: function () {
Expand Down Expand Up @@ -202,6 +203,15 @@ function getOneLineOtpBackupFile(entryData: { [hash: string]: RawOTPStorage }) {
const otpAuthLines: string[] = [];
for (const hash of Object.keys(entryData)) {
const otpStorage = entryData[hash];
// Key records and EncOTPStorage have no exportable otpauth line
if (
!otpStorage ||
typeof otpStorage !== "object" ||
!("secret" in otpStorage) ||
!otpStorage.secret
) {
continue;
}
if (otpStorage.issuer) {
otpStorage.issuer = removeUnsafeData(otpStorage.issuer);
}
Expand All @@ -211,10 +221,12 @@ function getOneLineOtpBackupFile(entryData: { [hash: string]: RawOTPStorage }) {
const label = otpStorage.issuer
? otpStorage.issuer + ":" + (otpStorage.account || "")
: otpStorage.account || "";
// Normalize numeric or string OTP types (EncOTPStorage round-trips can leave numbers)
const normalizedType = OTPUtil.otpTypeName(otpStorage.type);
let type = "";
if (otpStorage.type === "totp" || otpStorage.type === "hex") {
if (normalizedType === "totp" || normalizedType === "hex") {
type = "totp";
} else if (otpStorage.type === "hotp" || otpStorage.type === "hhex") {
} else if (normalizedType === "hotp" || normalizedType === "hhex") {
type = "hotp";
} else {
continue;
Expand All @@ -233,7 +245,9 @@ function getOneLineOtpBackupFile(entryData: { [hash: string]: RawOTPStorage }) {
? "&period=" + otpStorage.period
: "") +
(otpStorage.digits ? "&digits=" + otpStorage.digits : "") +
(otpStorage.algorithm ? "&algorithm=" + otpStorage.algorithm : "");
(otpStorage.algorithm
? "&algorithm=" + OTPUtil.otpAlgorithmName(otpStorage.algorithm)
: "");

otpAuthLines.push(otpAuthLine);
}
Expand Down
4 changes: 2 additions & 2 deletions src/definitions/otp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ interface RawOTPStorage {
index: number;
issuer?: string;
secret: string;
type: string;
type: string | number;
counter?: number;
period?: number;
digits?: number;
algorithm?: string;
algorithm?: string | number;
pinned?: boolean;
}

Expand Down
57 changes: 53 additions & 4 deletions src/models/otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,57 @@ export class OTPUtil {
return { length: 0 };
}
}

/** Accept stored type as enum number or name string ("totp"). */
static normalizeOTPType(type: unknown): OTPType {
if (typeof type === "number" && typeof OTPType[type] === "string") {
return type as OTPType;
}
if (typeof type === "string") {
const byName = OTPType[type as keyof typeof OTPType];
if (typeof byName === "number") {
return byName;
}
const asNumber = Number(type);
if (!Number.isNaN(asNumber) && typeof OTPType[asNumber] === "string") {
return asNumber as OTPType;
}
}
return OTPType.totp;
}

/** Always export type as a name string for otpauth / JSON backups. */
static otpTypeName(type: unknown): string {
return OTPType[this.normalizeOTPType(type)] as string;
}

/** Accept stored algorithm as enum number or name string ("SHA1"). */
static normalizeOTPAlgorithm(algorithm: unknown): OTPAlgorithm {
if (
typeof algorithm === "number" &&
typeof OTPAlgorithm[algorithm] === "string"
) {
return algorithm as OTPAlgorithm;
}
if (typeof algorithm === "string") {
const byName = OTPAlgorithm[algorithm as keyof typeof OTPAlgorithm];
if (typeof byName === "number") {
return byName;
}
const asNumber = Number(algorithm);
if (
!Number.isNaN(asNumber) &&
typeof OTPAlgorithm[asNumber] === "string"
) {
return asNumber as OTPAlgorithm;
}
}
return OTPAlgorithm.SHA1;
}

static otpAlgorithmName(algorithm: unknown): string {
return OTPAlgorithm[this.normalizeOTPAlgorithm(algorithm)] as string;
}
}

export class OTPEntry implements OTPEntryInterface {
Expand Down Expand Up @@ -215,16 +266,14 @@ export class OTPEntry implements OTPEntryInterface {
}

this.account = decryptedData.account || "";
// @ts-expect-error need a better way to do this
this.algorithm = OTPAlgorithm[decryptedData.algorithm] || OTPAlgorithm.SHA1;
this.algorithm = OTPUtil.normalizeOTPAlgorithm(decryptedData.algorithm);
this.counter = decryptedData.counter || 0;
this.digits = decryptedData.digits || 6;
this.issuer = decryptedData.issuer || "";
this.period = decryptedData.period || 30;
this.pinned = decryptedData.pinned || false;
this.secret = decryptedData.secret;
// @ts-expect-error need a better way to do this
this.type = OTPType[decryptedData.type] || OTPType.totp;
this.type = OTPUtil.normalizeOTPType(decryptedData.type);

if (this.type !== OTPType.hotp && this.type !== OTPType.hhex) {
this.generate();
Expand Down
83 changes: 67 additions & 16 deletions src/models/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Encryption } from "./encryption";
import { OTPEntry, OTPType, OTPAlgorithm, CodeState } from "./otp";
import {
OTPEntry,
OTPType,
OTPAlgorithm,
OTPUtil,
CodeState,
DataType,
} from "./otp";
import { StorageLocation, UserSettings } from "./settings";
import { DataType } from "./otp";
export class BrowserStorage {
private static async getStorageLocation(): Promise<StorageLocation> {
await UserSettings.updateItems();
Expand Down Expand Up @@ -222,7 +228,7 @@ export class EntryStorage {
encrypted,
hash: entry.hash,
index: entry.index,
type: OTPType[entry.type],
type: OTPUtil.otpTypeName(entry.type),
secret,
};

Expand Down Expand Up @@ -251,7 +257,7 @@ export class EntryStorage {
}

if (entry.algorithm && entry.algorithm !== OTPAlgorithm.SHA1) {
storageItem.algorithm = OTPAlgorithm[entry.algorithm];
storageItem.algorithm = OTPUtil.otpAlgorithmName(entry.algorithm);
}

if (entry.encryption?.getEncryptionKeyId()) {
Expand Down Expand Up @@ -384,11 +390,52 @@ export class EntryStorage {
continue;
}

const entry = _data[hash];
let entry = _data[hash];

// TODO: fix this
// EncOTPStorage: keep as-is for encrypted backups; decrypt for plaintext.
if (entry.dataType === "EncOTPStorage") {
continue;
if (encrypted) {
continue;
}

const decrypted = encryption.decryptEncSecret({
encData: entry.data,
hash,
} as OTPEntryInterface);

if (!decrypted?.secret) {
delete _data[hash];
continue;
}

const rawEntry: RawOTPStorage = {
account: decrypted.account,
encrypted: false,
hash: decrypted.hash || hash,
index: entry.index,
issuer: decrypted.issuer,
secret: decrypted.secret,
type: OTPUtil.otpTypeName(decrypted.type),
counter: decrypted.counter,
period: decrypted.period,
digits: decrypted.digits,
algorithm: decrypted.algorithm
? OTPUtil.otpAlgorithmName(decrypted.algorithm)
: undefined,
pinned: decrypted.pinned,
dataType: DataType.OTPStorage,
};

_data[hash] = rawEntry;
entry = rawEntry;
}

// Ensure type/algorithm are name strings before field cleanup
if ("type" in entry && entry.type !== undefined) {
entry.type = OTPUtil.otpTypeName(entry.type);
}
if ("algorithm" in entry && entry.algorithm !== undefined) {
entry.algorithm = OTPUtil.otpAlgorithmName(entry.algorithm);
}

// remove unnecessary fields
Expand All @@ -415,7 +462,10 @@ export class EntryStorage {
delete entry.digits;
}

if (entry.algorithm === OTPAlgorithm[OTPAlgorithm.SHA1]) {
if (
!entry.algorithm ||
entry.algorithm === OTPAlgorithm[OTPAlgorithm.SHA1]
) {
delete entry.algorithm;
}

Expand Down Expand Up @@ -478,7 +528,7 @@ export class EntryStorage {
algorithm: OTPAlgorithm;
pinned: boolean;
} = {
type: (parseInt(data[hash].type) as OTPType) || OTPType[OTPType.totp],
type: OTPUtil.normalizeOTPType(data[hash].type),
index: data[hash].index || 0,
issuer: data[hash].issuer || "",
account: data[hash].account || "",
Expand All @@ -487,9 +537,7 @@ export class EntryStorage {
counter: data[hash].counter || 0,
period: data[hash].period || 30,
digits: data[hash].digits || 6,
algorithm: rawAlgorithm
? (parseInt(rawAlgorithm) as OTPAlgorithm)
: OTPAlgorithm.SHA1,
algorithm: OTPUtil.normalizeOTPAlgorithm(rawAlgorithm),
pinned: data[hash].pinned || false,
hash: data[hash].hash || hash,
};
Expand Down Expand Up @@ -620,15 +668,19 @@ export class EntryStorage {
entryData.type = OTPType[OTPType.totp];
}

// Normalize type whether stored as name ("totp") or enum number (1)
const typeName = OTPUtil.otpTypeName(entryData.type);
entryData.type = typeName;

let type: OTPType;
switch (entryData.type) {
switch (typeName) {
case "totp":
case "hotp":
case "battle":
case "steam":
case "hex":
case "hhex":
type = OTPType[entryData.type];
type = OTPType[typeName];
break;
default:
// we need correct the type here
Expand Down Expand Up @@ -657,8 +709,7 @@ export class EntryStorage {
counter: entryData.counter,
period,
digits: entryData.digits ? Number(entryData.digits) : undefined,
// @ts-expect-error - it's fine if this ends up undefined
algorithm: OTPAlgorithm[entryData.algorithm],
algorithm: OTPUtil.normalizeOTPAlgorithm(entryData.algorithm),
pinned: entryData.pinned,
});

Expand Down
27 changes: 27 additions & 0 deletions src/test/otp-normalize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect } from "chai";
import { OTPAlgorithm, OTPType, OTPUtil } from "../models/otp";

describe("OTPUtil type/algorithm normalization", () => {
it("normalizes OTP type from name or number", () => {
expect(OTPUtil.normalizeOTPType("totp")).to.equal(OTPType.totp);
expect(OTPUtil.normalizeOTPType(OTPType.totp)).to.equal(OTPType.totp);
expect(OTPUtil.normalizeOTPType(1)).to.equal(OTPType.totp);
expect(OTPUtil.normalizeOTPType("hotp")).to.equal(OTPType.hotp);
expect(OTPUtil.normalizeOTPType(undefined)).to.equal(OTPType.totp);
});

it("always exports type as a name string", () => {
expect(OTPUtil.otpTypeName(1)).to.equal("totp");
expect(OTPUtil.otpTypeName("totp")).to.equal("totp");
expect(OTPUtil.otpTypeName(OTPType.hotp)).to.equal("hotp");
});

it("normalizes algorithm from name or number", () => {
expect(OTPUtil.normalizeOTPAlgorithm("SHA1")).to.equal(OTPAlgorithm.SHA1);
expect(OTPUtil.normalizeOTPAlgorithm(1)).to.equal(OTPAlgorithm.SHA1);
expect(OTPUtil.normalizeOTPAlgorithm("SHA256")).to.equal(
OTPAlgorithm.SHA256
);
expect(OTPUtil.otpAlgorithmName(1)).to.equal("SHA1");
});
});