mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
feat: seedpgp v1.1.0 - BIP39 mnemonic PGP encryption tool
- Implement cv25519 PGP encryption/decryption - Add Base45 encoding with CRC16 integrity checks - Create SEEDPGP1 frame format for QR codes - Support BIP39 passphrase flag indicator - Add comprehensive test suite with Trezor BIP39 vectors - 15 passing tests covering all core functionality
This commit is contained in:
62
src/lib/base45.ts
Normal file
62
src/lib/base45.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// RFC 9285 Base45 encoding (strict)
|
||||
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
||||
|
||||
const idx = (() => {
|
||||
const m = new Map<string, number>();
|
||||
for (let i = 0; i < ALPHABET.length; i++) m.set(ALPHABET[i], i);
|
||||
return m;
|
||||
})();
|
||||
|
||||
export function base45Encode(bytes: Uint8Array): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < bytes.length; i += 2) {
|
||||
if (i + 1 < bytes.length) {
|
||||
const x = bytes[i] * 256 + bytes[i + 1];
|
||||
const c = x % 45;
|
||||
const d = Math.floor(x / 45) % 45;
|
||||
const e = Math.floor(x / (45 * 45));
|
||||
out += ALPHABET[c] + ALPHABET[d] + ALPHABET[e];
|
||||
} else {
|
||||
const x = bytes[i];
|
||||
const c = x % 45;
|
||||
const d = Math.floor(x / 45);
|
||||
out += ALPHABET[c] + ALPHABET[d];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function base45Decode(text: string): Uint8Array {
|
||||
if (!text.length) return new Uint8Array();
|
||||
|
||||
for (const ch of text) {
|
||||
if (!idx.has(ch)) {
|
||||
throw new Error(`Base45 decode: invalid character '${ch}' (position ${text.indexOf(ch)})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length % 3 === 1) {
|
||||
throw new Error("Base45 decode: invalid length (length mod 3 == 1)");
|
||||
}
|
||||
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < text.length;) {
|
||||
const remaining = text.length - i;
|
||||
if (remaining >= 3) {
|
||||
const c = idx.get(text[i++])!;
|
||||
const d = idx.get(text[i++])!;
|
||||
const e = idx.get(text[i++])!;
|
||||
const x = c + d * 45 + e * 45 * 45;
|
||||
if (x > 0xffff) throw new Error("Base45 decode: value overflow in 3-tuple");
|
||||
out.push((x >> 8) & 0xff, x & 0xff);
|
||||
} else {
|
||||
const c = idx.get(text[i++])!;
|
||||
const d = idx.get(text[i++])!;
|
||||
const x = c + d * 45;
|
||||
if (x > 0xff) throw new Error("Base45 decode: value overflow in 2-tuple");
|
||||
out.push(x);
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
24
src/lib/bip39.ts
Normal file
24
src/lib/bip39.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Prototype-level BIP39 validation:
|
||||
// - enforces allowed word counts
|
||||
// - normalizes whitespace/case
|
||||
// NOTE: checksum + wordlist membership verification is intentionally omitted here.
|
||||
|
||||
export function normalizeBip39Mnemonic(words: string): string {
|
||||
return words.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
export function validateBip39Mnemonic(words: string): { valid: boolean; error?: string } {
|
||||
const normalized = normalizeBip39Mnemonic(words);
|
||||
const arr = normalized.length ? normalized.split(" ") : [];
|
||||
|
||||
const validCounts = new Set([12, 15, 18, 21, 24]);
|
||||
if (!validCounts.has(arr.length)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Invalid word count: ${arr.length}. Must be 12, 15, 18, 21, or 24.`,
|
||||
};
|
||||
}
|
||||
|
||||
// In production: verify each word is in the selected wordlist + verify checksum.
|
||||
return { valid: true };
|
||||
}
|
||||
12
src/lib/crc16.ts
Normal file
12
src/lib/crc16.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF, xorout 0x0000, refin/refout false
|
||||
export function crc16CcittFalse(data: Uint8Array): string {
|
||||
let crc = 0xffff;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
crc ^= data[i] << 8;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
||||
crc &= 0xffff;
|
||||
}
|
||||
}
|
||||
return crc.toString(16).toUpperCase().padStart(4, "0");
|
||||
}
|
||||
249
src/lib/seedpgp.test.ts
Normal file
249
src/lib/seedpgp.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { describe, test, expect, beforeAll } from "bun:test";
|
||||
import { encryptToSeedPgp, decryptSeedPgp, buildPlaintext } from "./seedpgp";
|
||||
import * as openpgp from "openpgp";
|
||||
|
||||
// Official BIP39 test vectors from Trezor
|
||||
const TREZOR_VECTORS = [
|
||||
{
|
||||
name: "12-word all zeros",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
},
|
||||
{
|
||||
name: "12-word all 0x7f",
|
||||
mnemonic: "legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||
},
|
||||
{
|
||||
name: "12-word all 0x80",
|
||||
mnemonic: "letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||
},
|
||||
{
|
||||
name: "12-word all 0xff",
|
||||
mnemonic: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||
},
|
||||
{
|
||||
name: "18-word all zeros",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||
},
|
||||
{
|
||||
name: "24-word all zeros",
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
},
|
||||
{
|
||||
name: "24-word random entropy",
|
||||
mnemonic: "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
|
||||
},
|
||||
{
|
||||
name: "12-word random entropy",
|
||||
mnemonic: "scheme spot photo card baby mountain device kick cradle pact join borrow",
|
||||
},
|
||||
];
|
||||
|
||||
// Your existing tests
|
||||
describe("seedpgp test vectors", () => {
|
||||
let testPublicKey: string;
|
||||
let testPrivateKey: string;
|
||||
const testPassphrase = "test-passphrase-123";
|
||||
|
||||
beforeAll(async () => {
|
||||
const { privateKey, publicKey } = await openpgp.generateKey({
|
||||
type: "ecc",
|
||||
curve: "curve25519Legacy",
|
||||
userIDs: [{ name: "Test User", email: "test@example.com" }],
|
||||
passphrase: testPassphrase,
|
||||
format: "armored",
|
||||
});
|
||||
|
||||
testPublicKey = publicKey;
|
||||
testPrivateKey = privateKey;
|
||||
});
|
||||
|
||||
test("vector 1: standard 24-word mnemonic", async () => {
|
||||
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
|
||||
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
const decrypted = await decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: testPassphrase,
|
||||
});
|
||||
|
||||
expect(decrypted.v).toBe(1);
|
||||
expect(decrypted.t).toBe("bip39");
|
||||
expect(decrypted.l).toBe("en");
|
||||
expect(decrypted.w).toBe(mnemonic);
|
||||
expect(decrypted.pp).toBe(0);
|
||||
});
|
||||
|
||||
test("vector 2: 12-word mnemonic", async () => {
|
||||
const mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow";
|
||||
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
const decrypted = await decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: testPassphrase,
|
||||
});
|
||||
|
||||
expect(decrypted.w).toBe(mnemonic);
|
||||
});
|
||||
|
||||
test("vector 3: mnemonic with BIP39 passphrase flag", async () => {
|
||||
const mnemonic = "test wallet seed phrase example only do not use real funds";
|
||||
|
||||
const plaintext = buildPlaintext(mnemonic, true);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
expect(encrypted.framed).toContain("SEEDPGP1:");
|
||||
expect(encrypted.recipientFingerprint).toBeDefined();
|
||||
|
||||
const decrypted = await decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: testPassphrase,
|
||||
});
|
||||
|
||||
expect(decrypted.w).toBe(mnemonic);
|
||||
expect(decrypted.pp).toBe(1);
|
||||
});
|
||||
|
||||
test("vector 4: reject wrong private key", async () => {
|
||||
const mnemonic = "test seed phrase";
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const { privateKey: wrongKey } = await openpgp.generateKey({
|
||||
type: "ecc",
|
||||
curve: "curve25519Legacy",
|
||||
userIDs: [{ name: "Wrong User", email: "wrong@example.com" }],
|
||||
passphrase: "wrong-pw",
|
||||
format: "armored",
|
||||
});
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
await expect(
|
||||
decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: wrongKey,
|
||||
privateKeyPassphrase: "wrong-pw",
|
||||
})
|
||||
).rejects.toThrow(/not encrypted to the provided private key/);
|
||||
});
|
||||
|
||||
test("vector 5: reject invalid passphrase", async () => {
|
||||
const mnemonic = "test seed phrase";
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
await expect(
|
||||
decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: "wrong-passphrase",
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("vector 6: frame format validation", async () => {
|
||||
const mnemonic = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong";
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
const parts = encrypted.framed.split(":");
|
||||
expect(parts[0]).toBe("SEEDPGP1");
|
||||
expect(parts[1]).toBe("0");
|
||||
expect(parts[2]).toMatch(/^[0-9A-F]{4}$/);
|
||||
expect(parts.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
// NEW: Add the Trezor vectors as a second describe block
|
||||
describe("BIP39 Trezor test vectors", () => {
|
||||
let testPublicKey: string;
|
||||
let testPrivateKey: string;
|
||||
const testPassphrase = "test-passphrase-123";
|
||||
|
||||
beforeAll(async () => {
|
||||
const { privateKey, publicKey } = await openpgp.generateKey({
|
||||
type: "ecc",
|
||||
curve: "curve25519Legacy",
|
||||
userIDs: [{ name: "Test User", email: "test@example.com" }],
|
||||
passphrase: testPassphrase,
|
||||
format: "armored",
|
||||
});
|
||||
|
||||
testPublicKey = publicKey;
|
||||
testPrivateKey = privateKey;
|
||||
});
|
||||
|
||||
TREZOR_VECTORS.forEach(({ name, mnemonic }) => {
|
||||
test(`${name}`, async () => {
|
||||
const plaintext = buildPlaintext(mnemonic, false);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
expect(encrypted.framed).toMatch(/^SEEDPGP1:0:[0-9A-F]{4}:.+$/);
|
||||
expect(encrypted.recipientFingerprint).toBeDefined();
|
||||
|
||||
const decrypted = await decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: testPassphrase,
|
||||
});
|
||||
|
||||
expect(decrypted.v).toBe(1);
|
||||
expect(decrypted.t).toBe("bip39");
|
||||
expect(decrypted.l).toBe("en");
|
||||
expect(decrypted.pp).toBe(0);
|
||||
expect(decrypted.w).toBe(mnemonic);
|
||||
});
|
||||
});
|
||||
|
||||
test("with BIP39 passphrase flag", async () => {
|
||||
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||
const plaintext = buildPlaintext(mnemonic, true);
|
||||
|
||||
const encrypted = await encryptToSeedPgp({
|
||||
plaintext,
|
||||
publicKeyArmored: testPublicKey,
|
||||
});
|
||||
|
||||
const decrypted = await decryptSeedPgp({
|
||||
frameText: encrypted.framed,
|
||||
privateKeyArmored: testPrivateKey,
|
||||
privateKeyPassphrase: testPassphrase,
|
||||
});
|
||||
|
||||
expect(decrypted.w).toBe(mnemonic);
|
||||
expect(decrypted.pp).toBe(1);
|
||||
});
|
||||
});
|
||||
190
src/lib/seedpgp.ts
Normal file
190
src/lib/seedpgp.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import * as openpgp from "openpgp";
|
||||
import { base45Encode, base45Decode } from "./base45";
|
||||
import { crc16CcittFalse } from "./crc16";
|
||||
import type { SeedPgpPlaintext, ParsedSeedPgpFrame } from "./types";
|
||||
|
||||
function nonEmptyTrimmed(s?: string): string | undefined {
|
||||
if (!s) return undefined;
|
||||
const t = s.trim();
|
||||
return t.length ? t : undefined;
|
||||
}
|
||||
|
||||
export function normalizeMnemonic(words: string): string {
|
||||
return words.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
export function buildPlaintext(
|
||||
mnemonic: string,
|
||||
bip39PassphraseUsed: boolean,
|
||||
recipientFingerprints?: string[]
|
||||
): SeedPgpPlaintext {
|
||||
const plain: SeedPgpPlaintext = {
|
||||
v: 1,
|
||||
t: "bip39",
|
||||
w: normalizeMnemonic(mnemonic),
|
||||
l: "en",
|
||||
pp: bip39PassphraseUsed ? 1 : 0,
|
||||
};
|
||||
if (recipientFingerprints && recipientFingerprints.length > 0) {
|
||||
plain.fpr = recipientFingerprints;
|
||||
}
|
||||
return plain;
|
||||
}
|
||||
|
||||
export function frameEncode(pgpBinary: Uint8Array): string {
|
||||
const crc = crc16CcittFalse(pgpBinary);
|
||||
const b45 = base45Encode(pgpBinary);
|
||||
return `SEEDPGP1:0:${crc}:${b45}`;
|
||||
}
|
||||
|
||||
export function frameParse(text: string): ParsedSeedPgpFrame {
|
||||
const s = text.trim().replace(/^["']|["']$/g, "").replace(/[\n\r\t]/g, "");
|
||||
if (!s.startsWith("SEEDPGP1:")) throw new Error("Missing SEEDPGP1: prefix");
|
||||
|
||||
const parts = s.split(":");
|
||||
if (parts.length < 4) {
|
||||
throw new Error("Invalid frame format (need at least 4 colon-separated parts)");
|
||||
}
|
||||
|
||||
const prefix = parts[0];
|
||||
const frame = parts[1];
|
||||
const crc16 = parts[2].toUpperCase();
|
||||
const b45 = parts.slice(3).join(":");
|
||||
|
||||
if (prefix !== "SEEDPGP1") throw new Error("Invalid prefix");
|
||||
if (frame !== "0") throw new Error("Multipart frames not supported in this prototype");
|
||||
if (!/^[0-9A-F]{4}$/.test(crc16)) throw new Error("Invalid CRC16 format (must be 4 hex chars)");
|
||||
|
||||
return { kind: "single", crc16, b45 };
|
||||
}
|
||||
|
||||
export function frameDecodeToPgpBytes(frameText: string): Uint8Array {
|
||||
const f = frameParse(frameText);
|
||||
const pgp = base45Decode(f.b45);
|
||||
const crc = crc16CcittFalse(pgp);
|
||||
if (crc !== f.crc16) {
|
||||
throw new Error(`CRC16 mismatch! Expected: ${f.crc16}, Got: ${crc}. QR scan may be corrupted.`);
|
||||
}
|
||||
return pgp;
|
||||
}
|
||||
|
||||
export async function encryptToSeedPgp(params: {
|
||||
plaintext: SeedPgpPlaintext;
|
||||
publicKeyArmored?: string;
|
||||
messagePassword?: string;
|
||||
}): Promise<{ framed: string; pgpBytes: Uint8Array; recipientFingerprint?: string }> {
|
||||
const pub = nonEmptyTrimmed(params.publicKeyArmored);
|
||||
const pw = nonEmptyTrimmed(params.messagePassword);
|
||||
|
||||
if (!pub && !pw) {
|
||||
throw new Error("Provide either a PGP public key or a message password (or both).");
|
||||
}
|
||||
|
||||
let encryptionKeys: openpgp.PublicKey[] = [];
|
||||
let recipientFingerprint: string | undefined;
|
||||
|
||||
if (pub) {
|
||||
const pubKey = await openpgp.readKey({ armoredKey: pub });
|
||||
try {
|
||||
await pubKey.getEncryptionKey();
|
||||
} catch {
|
||||
throw new Error("This public key has no usable encryption subkey (E).");
|
||||
}
|
||||
|
||||
recipientFingerprint = pubKey.getFingerprint().toUpperCase();
|
||||
encryptionKeys = [pubKey];
|
||||
}
|
||||
|
||||
const message = await openpgp.createMessage({ text: JSON.stringify(params.plaintext) });
|
||||
|
||||
const encrypted = await openpgp.encrypt({
|
||||
message,
|
||||
encryptionKeys,
|
||||
passwords: pw ? [pw] : [],
|
||||
format: "binary",
|
||||
config: {
|
||||
preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256,
|
||||
},
|
||||
});
|
||||
|
||||
const pgpBytes = new Uint8Array(encrypted as Uint8Array);
|
||||
return { framed: frameEncode(pgpBytes), pgpBytes, recipientFingerprint };
|
||||
}
|
||||
|
||||
export async function decryptSeedPgp(params: {
|
||||
frameText: string;
|
||||
privateKeyArmored?: string;
|
||||
privateKeyPassphrase?: string;
|
||||
messagePassword?: string;
|
||||
}): Promise<SeedPgpPlaintext> {
|
||||
const pgpBytes = frameDecodeToPgpBytes(params.frameText);
|
||||
const message = await openpgp.readMessage({ binaryMessage: pgpBytes });
|
||||
const encKeyIds: openpgp.KeyID[] = message.getEncryptionKeyIDs?.() ?? [];
|
||||
|
||||
const privArmored = nonEmptyTrimmed(params.privateKeyArmored);
|
||||
const privPw = nonEmptyTrimmed(params.privateKeyPassphrase);
|
||||
const msgPw = nonEmptyTrimmed(params.messagePassword);
|
||||
|
||||
let decryptionKeys: openpgp.PrivateKey[] = [];
|
||||
|
||||
if (privArmored) {
|
||||
let privKey = await openpgp.readPrivateKey({ armoredKey: privArmored });
|
||||
|
||||
if (!privKey.isDecrypted()) {
|
||||
if (!privPw) {
|
||||
throw new Error("Private key is still locked. Enter the private key passphrase.");
|
||||
}
|
||||
privKey = await openpgp.decryptKey({ privateKey: privKey, passphrase: privPw });
|
||||
if (!privKey.isDecrypted()) {
|
||||
throw new Error("Private key passphrase incorrect (key still locked).");
|
||||
}
|
||||
}
|
||||
|
||||
// Preflight: validate the private key matches a recipient
|
||||
if (encKeyIds.length) {
|
||||
const dec = await privKey.getDecryptionKeys();
|
||||
const decArr = Array.isArray(dec) ? dec : [dec];
|
||||
const decKeyIds = decArr.map((k: any) => k.getKeyID().toHex().toUpperCase());
|
||||
const want = new Set(encKeyIds.map((kid: openpgp.KeyID) => kid.toHex().toUpperCase()));
|
||||
const matched = decKeyIds.some((id: string) => want.has(id));
|
||||
|
||||
if (!matched) {
|
||||
throw new Error(
|
||||
"This payload is not encrypted to the provided private key (no matching recipient KeyID)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
decryptionKeys = [privKey];
|
||||
}
|
||||
|
||||
let decryptResult;
|
||||
try {
|
||||
const decryptOptions: any = {
|
||||
message,
|
||||
format: "utf8",
|
||||
};
|
||||
|
||||
if (decryptionKeys.length > 0) {
|
||||
decryptOptions.decryptionKeys = decryptionKeys;
|
||||
}
|
||||
|
||||
if (msgPw) {
|
||||
decryptOptions.passwords = [msgPw];
|
||||
}
|
||||
|
||||
decryptResult = await openpgp.decrypt(decryptOptions);
|
||||
} catch (err: any) {
|
||||
console.error("SeedPGP: decrypt failed:", err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { data } = decryptResult;
|
||||
const obj = JSON.parse(data as string) as SeedPgpPlaintext;
|
||||
|
||||
if (obj.v !== 1) throw new Error(`Unsupported version: ${obj.v}`);
|
||||
if (obj.t !== "bip39") throw new Error(`Unsupported type: ${obj.t}`);
|
||||
if (obj.l !== "en") throw new Error(`Unsupported language: ${obj.l}`);
|
||||
|
||||
return obj;
|
||||
}
|
||||
14
src/lib/types.ts
Normal file
14
src/lib/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type SeedPgpPlaintext = {
|
||||
v: 1;
|
||||
t: "bip39";
|
||||
w: string;
|
||||
l: "en";
|
||||
pp: 0 | 1;
|
||||
fpr?: string[];
|
||||
};
|
||||
|
||||
export type ParsedSeedPgpFrame = {
|
||||
kind: "single";
|
||||
crc16: string;
|
||||
b45: string;
|
||||
};
|
||||
Reference in New Issue
Block a user