@planetarium/account-web3-secret-storage
Version:
Libplanet account implementation using Ethereum Web3 Secret Storage
670 lines (661 loc) • 21.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/crypto/browser.ts
var crypto = window.crypto;
// src/KeyId.ts
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function isKeyId(keyId) {
return !!keyId.match(UUID_PATTERN);
}
__name(isKeyId, "isKeyId");
function generateKeyId() {
return crypto.randomUUID();
}
__name(generateKeyId, "generateKeyId");
// src/TtyPassphraseEntry.ts
import * as readline from "node:readline";
import { Writable } from "node:stream";
function isNewLine(chunk) {
return typeof chunk === "string" && chunk.endsWith("\n") || chunk instanceof Buffer && chunk.at(chunk.length - 1) === 10;
}
__name(isNewLine, "isNewLine");
function readPassphrase(prompt, { input, output, mask }) {
let masking = false;
const rl = readline.createInterface({
input,
output: new Writable({
write: (chunk, encoding, callback) => {
if (masking && !isNewLine(chunk)) {
output.write(
mask,
encoding.endsWith("buffer") ? "utf8" : encoding,
callback
);
} else
output.write(chunk, encoding, callback);
}
}),
terminal: true
});
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer);
});
setTimeout(() => masking = mask !== false, 0);
});
}
__name(readPassphrase, "readPassphrase");
var prompts = {
authenticate: {
"": "Passphrase for {keyId}: ",
ko: "{keyId} \uD0A4\uC758 \uC554\uD638: ",
qts: "AUTHENTICATE:{keyId}"
// for unit test
},
authenticateRetry: {
"": "Incorrect; try again: ",
ko: "\uD2C0\uB9BC. \uC7AC\uC2DC\uB3C4: ",
qts: "AUTHENTICATE_RETRY:{keyId}"
// for unit test
},
configurePassphrase: {
"": "New passphrase: ",
ko: "\uC0C8 \uC554\uD638: ",
qts: "CONFIGURE_PASSPHRASE"
// for unit test
},
configurePassphraseRetry: {
"": "Mismatch; new passphrase: ",
ko: "\uD2C0\uB9BC. \uC0C8 \uC554\uD638 \uC7AC\uC785\uB825:",
qts: "CONFIGURE_PASSPHRASE_RETRY"
// for unit test
},
confirmConfigurePassphrase: {
"": "Confirm passphrase: ",
ko: "\uC554\uD638 \uD655\uC778:",
qts: "CONFIRM_CONFIGURE_PASSPHRASE"
// for unit test
}
};
function getLocale() {
return Intl.DateTimeFormat().resolvedOptions().locale;
}
__name(getLocale, "getLocale");
var TtyPassphraseEntry = class {
#options;
constructor(options = {}) {
this.#options = {
input: options.input ?? process.stdin,
output: options.output ?? process.stdout,
mask: options.mask ?? "*",
locale: options.locale ?? getLocale()
};
}
#getPrompt(promptType) {
const table = prompts[promptType];
let locale = this.#options.locale;
let msg;
while (msg == null) {
msg = table[locale];
locale = locale.replace(/(^|[-_.])[^-_.]*$/, "");
}
return msg;
}
authenticate(keyId, firstAttempt) {
const message = this.#getPrompt(
firstAttempt ? "authenticate" : "authenticateRetry"
);
const prompt = message.replace(/\{keyId\}/g, keyId);
return readPassphrase(prompt, this.#options);
}
async configurePassphrase() {
let prompt = this.#getPrompt("configurePassphrase");
const retryPrompt = this.#getPrompt("configurePassphraseRetry");
const confirmPrompt = this.#getPrompt("confirmConfigurePassphrase");
while (true) {
const passphrase = await readPassphrase(prompt, this.#options);
const confirmPassphrase = await readPassphrase(
confirmPrompt,
this.#options
);
if (passphrase === confirmPassphrase)
return passphrase;
prompt = retryPrompt;
}
}
};
__name(TtyPassphraseEntry, "TtyPassphraseEntry");
// src/Web3Account.ts
import {
Address,
RawPrivateKey
} from "@planetarium/account";
import { pbkdf2Async } from "@noble/hashes/pbkdf2";
import { scryptAsync } from "@noble/hashes/scrypt";
import { sha256 } from "@noble/hashes/sha256";
import { keccak_256 } from "@noble/hashes/sha3";
var Web3Account = class {
#keyObject;
#passphraseEntry;
#options;
constructor(keyObject, passphraseEntry, options = {}) {
this.#keyObject = keyObject;
this.#passphraseEntry = passphraseEntry;
this.#options = options;
}
async #exportPrivateKey(options = {}) {
let firstAttempt = true;
let privateKey;
while (true) {
const passphrase = await this.#passphraseEntry.authenticate(
this.#keyObject.id,
firstAttempt
);
try {
const result = await decryptKeyObject(this.#keyObject, passphrase, {
...this.#options,
...options
});
privateKey = result.privateKey;
} catch (e) {
if (e instanceof IncorrectPassphraseError) {
firstAttempt = false;
continue;
}
throw e;
}
break;
}
return privateKey;
}
exportPrivateKey() {
return this.#exportPrivateKey({ allowWeakPrivateKey: true });
}
getAddress() {
return Promise.resolve(Address.fromHex(this.#keyObject.address, true));
}
async getPublicKey() {
const key = await this.#exportPrivateKey({ allowWeakPrivateKey: true });
return await key.getPublicKey();
}
async sign(message) {
const key = await this.#exportPrivateKey();
return await key.sign(message);
}
};
__name(Web3Account, "Web3Account");
function toHex(bytes) {
const array = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
let hex = "";
for (let i = 0; i < array.length; i++) {
hex += array[i].toString(16).padStart(2, "0");
}
return hex;
}
__name(toHex, "toHex");
async function encryptKeyObject(keyId, privateKey, passphrase) {
if (!isKeyId(keyId)) {
throw new Error(`Invalid key ID: ${keyId}`);
}
const salt = new Uint8Array(32);
crypto.getRandomValues(salt);
const kdf = {
kdf: "pbkdf2",
kdfparams: {
c: 10240,
dklen: 32,
prf: "hmac-sha256",
salt: toHex(salt)
}
};
const derivedKey = await deriveKey(kdf, passphrase);
const cipher = await encipher({
derivedKey,
privateKey: privateKey.toBytes()
});
const address = await Address.deriveFrom(privateKey);
return {
version: 3,
id: keyId,
address: address.toHex("lower"),
crypto: { ...cipher, ...kdf }
};
}
__name(encryptKeyObject, "encryptKeyObject");
function isKeyObject(json) {
if (typeof json !== "object" || json == null || !("version" in json) || json.version !== 3 || !("id" in json) || typeof json.id !== "string" || !isKeyId(json.id) || !("address" in json) || typeof json.address !== "string") {
return false;
}
try {
Address.fromHex(json.address, true);
} catch (e) {
return false;
}
return "crypto" in json && typeof json.crypto === "object" && isKeyObjectCipher(json.crypto) && isKeyObjectKdf(json.crypto);
}
__name(isKeyObject, "isKeyObject");
function isKeyObjectCipher(json) {
return typeof json === "object" && json != null && "cipher" in json && json.cipher === "aes-128-ctr" && "cipherparams" in json && typeof json.cipherparams === "object" && json.cipherparams != null && "iv" in json.cipherparams && typeof json.cipherparams.iv === "string" && "ciphertext" in json && typeof json.ciphertext === "string" && "mac" in json && typeof json.mac === "string";
}
__name(isKeyObjectCipher, "isKeyObjectCipher");
function isKeyObjectKdf(json) {
if (typeof json !== "object" || json == null || !("kdf" in json)) {
return false;
}
switch (json.kdf) {
case "pbkdf2":
return "kdfparams" in json && typeof json.kdfparams === "object" && json.kdfparams != null && "c" in json.kdfparams && typeof json.kdfparams.c === "number" && "dklen" in json.kdfparams && typeof json.kdfparams.dklen === "number" && "prf" in json.kdfparams && json.kdfparams.prf === "hmac-sha256" && "salt" in json.kdfparams && typeof json.kdfparams.salt === "string";
case "scrypt":
return "kdfparams" in json && typeof json.kdfparams === "object" && json.kdfparams != null && "dklen" in json.kdfparams && typeof json.kdfparams.dklen === "number" && "n" in json.kdfparams && typeof json.kdfparams.n === "number" && "p" in json.kdfparams && typeof json.kdfparams.p === "number" && "r" in json.kdfparams && typeof json.kdfparams.r === "number" && "salt" in json.kdfparams && typeof json.kdfparams.salt === "string";
default:
return false;
}
}
__name(isKeyObjectKdf, "isKeyObjectKdf");
var WeakPrivateKeyError = class extends Error {
name = "WeakPrivateKeyError";
};
__name(WeakPrivateKeyError, "WeakPrivateKeyError");
async function decryptKeyObject(keyObject, passphrase, options = {}) {
if (keyObject == null) {
throw new Error("Key object is null.");
} else if (keyObject.version !== 3) {
throw new Error(`Unsupported key object version: ${keyObject?.version}`);
} else if (keyObject.crypto == null) {
throw new Error("Key object does not have crypto field.");
}
const keyObjectAddress = Address.fromHex(keyObject.address, true);
const derivedKey = await deriveKey(keyObject.crypto, passphrase);
let privateKeyBytes = await decipher(keyObject.crypto, derivedKey);
if (privateKeyBytes.length < 32) {
const zeroPadded = new Uint8Array(32);
zeroPadded.set(privateKeyBytes, 32 - privateKeyBytes.length);
privateKeyBytes = zeroPadded;
}
if (privateKeyBytes.at(0) === 0 && !options.allowWeakPrivateKey) {
throw new WeakPrivateKeyError(
"The private key given is too weak; keys of length less than 32 bytes are disallowed by default. See also the Web3AccountOptions.allowWeakPrivateKey option."
);
}
const privateKey = RawPrivateKey.fromBytes(privateKeyBytes);
const address = await Address.deriveFrom(privateKey);
if (!keyObjectAddress.equals(address)) {
throw new Error(
`Failed to decrypt the key object; expected account ${keyObjectAddress} but got ${address} instead.`
);
}
return { keyId: keyObject.id, privateKey };
}
__name(decryptKeyObject, "decryptKeyObject");
async function deriveKey(kdf, passphrase) {
if (kdf.kdf === "pbkdf2") {
const { c, dklen, prf, salt } = kdf.kdfparams;
if (dklen < 16)
throw new Error(`Too short dklen: ${dklen}`);
if (prf !== "hmac-sha256")
throw new Error(`Unsupported prf: ${prf}`);
const derivedKey = await pbkdf2Async(
sha256,
passphrase,
Buffer.from(salt, "hex"),
{
c,
dkLen: dklen
}
);
if (derivedKey.length < dklen) {
throw new Error(`Key too short: ${toHex(derivedKey)}`);
}
return derivedKey;
} else if (kdf.kdf === "scrypt") {
const { dklen, n, p, r, salt } = kdf.kdfparams;
const derivedKey = await scryptAsync(passphrase, salt, { N: n, r, p, dkLen: dklen });
if (derivedKey.length < dklen) {
throw new Error(`Key too short: ${toHex(derivedKey)}`);
}
return derivedKey;
}
throw new Error(`Unsupported kdf: ${kdf["kdf"]}`);
}
__name(deriveKey, "deriveKey");
var IncorrectPassphraseError = class extends Error {
constructor(expectedMac, actualMac) {
super(`Expected: ${toHex(expectedMac)}, Actual: ${toHex(actualMac)}`);
this.expectedMac = expectedMac;
this.actualMac = actualMac;
}
name = "IncorrectPassphraseError";
};
__name(IncorrectPassphraseError, "IncorrectPassphraseError");
async function encipher({
derivedKey,
privateKey
}) {
const iv = new Uint8Array(16);
crypto.getRandomValues(iv);
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 128
},
await crypto.subtle.importKey(
"raw",
derivedKey.subarray(0, 16),
{ name: "AES-CTR" },
false,
["encrypt"]
),
privateKey
);
return {
cipher: "aes-128-ctr",
cipherparams: { iv: toHex(iv) },
ciphertext: toHex(ciphertext),
mac: toHex(calculateMac(derivedKey, ciphertext))
};
}
__name(encipher, "encipher");
async function decipher(cipher, derivedKey) {
if (cipher.cipher !== "aes-128-ctr") {
throw new Error(`Unsupported cipher: ${cipher.cipher}`);
}
const ciphertext = new Uint8Array(Buffer.from(cipher.ciphertext, "hex"));
const mac = calculateMac(derivedKey, ciphertext);
const expectedMac = new Uint8Array(Buffer.from(cipher.mac, "hex"));
if (!mac.every((v, i) => v === expectedMac[i])) {
throw new IncorrectPassphraseError(expectedMac, mac);
}
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: new Uint8Array(Buffer.from(cipher.cipherparams.iv, "hex")),
length: 128
},
await crypto.subtle.importKey(
"raw",
derivedKey.subarray(0, 16),
{ name: "AES-CTR" },
false,
["decrypt"]
),
ciphertext
);
return decrypted instanceof Uint8Array ? decrypted : new Uint8Array(decrypted);
}
__name(decipher, "decipher");
function calculateMac(derivedKey, ciphertext) {
const keySubBytes = 16;
const ciphertextBytes = ciphertext instanceof Uint8Array ? ciphertext : new Uint8Array(ciphertext);
const seal = new Uint8Array(keySubBytes + ciphertextBytes.length);
if (derivedKey.length < keySubBytes) {
throw new Error(
`Too short derived key (${derivedKey.length} < ${keySubBytes}): ${toHex(
derivedKey
)}`
);
}
seal.set(derivedKey.subarray(derivedKey.length - keySubBytes));
seal.set(ciphertextBytes, keySubBytes);
const mac = keccak_256(seal);
return mac;
}
__name(calculateMac, "calculateMac");
// src/Web3KeyStore.ts
import {
Address as Address2,
RawPrivateKey as RawPrivateKey2
} from "@planetarium/account";
// src/path/browser.ts
function join(...args) {
if (args.length === 0) {
return ".";
}
if (args.length === 1) {
return args[0];
}
const lastElement = args.at(-1);
if (args.length > 1 && lastElement !== void 0) {
return [
trimSlashEnd(args[0]),
...args.slice(1, -1).map((x) => trimSlashStart(trimSlashEnd(x))),
trimSlashStart(lastElement)
].join("/");
}
throw new Error("All cases are covered.");
}
__name(join, "join");
function trimSlashEnd(s) {
let ret = s;
while (ret.length > 0 && ret.at(-1) === "/") {
ret = ret.slice(0, -1);
}
return ret;
}
__name(trimSlashEnd, "trimSlashEnd");
function trimSlashStart(s) {
let ret = s;
while (ret.length > 0 && ret.at(0) === "/") {
ret = ret.slice(1);
}
return ret;
}
__name(trimSlashStart, "trimSlashStart");
function getDefaultWeb3KeyStorePath() {
return "/planetarium/account/web3-secret-storage/keystore";
}
__name(getDefaultWeb3KeyStorePath, "getDefaultWeb3KeyStorePath");
// src/fs/browser.ts
var PREFIX = "PLANETARIUM_EMULATED_FS_";
function prefixed(str) {
return PREFIX + str;
}
__name(prefixed, "prefixed");
async function mkdir(path, options) {
}
__name(mkdir, "mkdir");
async function readFile(path, options) {
const item = localStorage.getItem(prefixed(path));
if (item == null) {
throw new Error("Not found");
}
return item;
}
__name(readFile, "readFile");
async function removeFile(path) {
localStorage.removeItem(prefixed(path));
}
__name(removeFile, "removeFile");
async function* listFiles(directory) {
for (let i = 0; i < localStorage.length; ++i) {
const item = localStorage.key(i);
if (item == null) {
throw new Error(`Expected ${i}th item in localStorage.`);
}
if (item.startsWith(prefixed(directory))) {
let sliced = item.slice(prefixed(directory).length);
if (sliced.startsWith("/")) {
sliced = sliced.slice(1);
}
yield sliced;
}
}
}
__name(listFiles, "listFiles");
async function writeFile(path, content, encoding) {
localStorage.setItem(prefixed(path), content);
}
__name(writeFile, "writeFile");
// src/Web3KeyStore.ts
var pattern = /^(?:UTC--([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}-[0-9]{2}-[0-9]{2})Z--)?([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})(?:.json)?$$/i;
function parseKeyFilename(name) {
const match = pattern.exec(name);
if (match == null)
return void 0;
return {
keyId: match[3].toLowerCase(),
createdAt: match[1] != null && match[2] != null ? /* @__PURE__ */ new Date(`${match[1]}T${match[2].replace(/-/g, ":")}Z`) : void 0
};
}
__name(parseKeyFilename, "parseKeyFilename");
var Web3KeyStore = class {
#passphraseEntry;
#accountOptions;
path;
constructor(options) {
this.path = options.path ?? getDefaultWeb3KeyStorePath();
this.#passphraseEntry = options.passphraseEntry;
this.#accountOptions = options;
}
async *#listKeyFiles() {
for await (const name of listFiles(this.path)) {
yield name;
}
}
async #getKeyPath(keyId) {
for await (const name of this.#listKeyFiles()) {
const parsed = parseKeyFilename(name);
if (parsed != null && parsed.keyId === keyId) {
return { ...parsed, path: join(this.path, name) };
}
}
return void 0;
}
async *list() {
for await (const name of this.#listKeyFiles()) {
const parsed = parseKeyFilename(name);
if (parsed == null)
continue;
const keyPath = join(this.path, name);
let json;
try {
json = JSON.parse(await readFile(keyPath, { encoding: "utf8" }));
} catch (_) {
continue;
}
if (!isKeyObject(json))
continue;
let address;
try {
address = Address2.fromHex(json.address, true);
} catch (_) {
continue;
}
const metadata = { address };
yield { ...parsed, metadata };
}
}
async get(keyId) {
const keyPath = await this.#getKeyPath(keyId);
if (keyPath == null)
return { result: "keyNotFound", keyId };
let json;
try {
json = await readFile(keyPath.path, {
encoding: "utf8"
});
} catch (e) {
if (e != null && typeof e === "object" && "code" in e && e.code === "ENOENT") {
return { result: "keyNotFound", keyId };
}
return { result: "error", keyId, message: `${e}` };
}
const keyObject = JSON.parse(json);
if (!isKeyObject(keyObject)) {
return { result: "error", keyId, message: "Invalid key file" };
}
const account = new Web3Account(
keyObject,
this.#passphraseEntry,
this.#accountOptions
);
return {
result: "success",
account,
keyId,
metadata: { address: await account.getAddress() },
createdAt: keyPath.createdAt
};
}
async generate(metadata) {
if (metadata?.address != null) {
return {
result: "error",
message: "Address cannot be predetermined before generating key"
};
}
const privateKey = await RawPrivateKey2.generate();
const result = await this.#import(privateKey, metadata);
if (result.result === "success") {
return {
result: "success",
keyId: result.keyId,
account: new Web3Account(result.keyObject, this.#passphraseEntry)
};
}
return result;
}
async delete(keyId) {
const keyPath = await this.#getKeyPath(keyId);
if (keyPath == null)
return { result: "keyNotFound", keyId };
try {
await removeFile(keyPath.path);
} catch (e) {
return { result: "error", message: `${e}` };
}
return { result: "success", keyId };
}
async #import(privateKey, metadata) {
if (metadata?.address != null && !metadata.address.equals(await Address2.deriveFrom(privateKey))) {
return {
result: "error",
message: "Address does not match the private key (hint: you do not have to specify it manually)"
};
}
const passphrase = await this.#passphraseEntry.configurePassphrase();
const keyId = generateKeyId();
const keyObject = await encryptKeyObject(keyId, privateKey, passphrase);
try {
await mkdir(this.path, { recursive: true });
} catch (e) {
return { result: "error", message: `${e}` };
}
const createdAt = /* @__PURE__ */ new Date();
const keyPath = join(
this.path,
`UTC--${createdAt.toISOString().replace(/\.[0-9]+Z$/, "Z").replace(/:/g, "-")}--${keyId}`
);
try {
await writeFile(keyPath, JSON.stringify(keyObject), "utf8");
} catch (e) {
return { result: "error", message: `${e}` };
}
return { result: "success", keyId, keyObject };
}
async import(privateKey, metadata) {
const bytes = await privateKey.toBytes();
if (bytes.at(0) === 0 && !this.#accountOptions.allowWeakPrivateKey) {
return {
result: "error",
message: "The private key given is too weak; keys of length less than 32 bytes are disallowed by default. See also the Web3AccountOptions.allowWeakPrivateKey option."
};
}
const result = await this.#import(privateKey, metadata);
if (result.result === "success") {
return { result: "success", keyId: result.keyId };
}
return result;
}
};
__name(Web3KeyStore, "Web3KeyStore");
export {
TtyPassphraseEntry,
WeakPrivateKeyError,
Web3Account,
Web3KeyStore,
getDefaultWeb3KeyStorePath
};
/*! For license information please see index.browser.mjs.LEGAL.txt */
//# sourceMappingURL=index.browser.mjs.map