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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"commander": "^14.0.0",
"conf": "^14.0.0",
"decimal.js": "^10.6.0",
"eciesjs": "^0.4.15",
"inquirer": "^12.7.0",
"js-sha3": "^0.9.3",
"ky": "^1.8.1",
Expand Down
39 changes: 39 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions src/commands/blind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { password } from "@inquirer/prompts";
import { type } from "arktype";
import { decrypt, ECIES_CONFIG, encrypt, PrivateKey } from "eciesjs";
import { mayFail } from "ts-handling";
import { decode } from "../encoding.js";
import { exit, program } from "./cli.js";

ECIES_CONFIG.ellipticCurve = "x25519";

const isHex = (value: string) =>
value.length % 2 === 0 && /^[0-9a-f]+$/.test(value);

const Data = type("string > 0").pipe(
(v) => new Uint8Array(Buffer.from(v, isHex(v) ? "hex" : "utf8")),
);
const Key = type(/^[a-f0-9]{64}$/).pipe((v) => Buffer.from(v, "hex"));

const Hex = type(/^[a-f0-9]+$/)
.narrow((v, ctx) => v.length % 2 === 0 || ctx.mustBe("even length"))
.pipe((v) => Buffer.from(v, "hex"));

const blind = program.command("blind").description("Blind and unblind data");

blind
.command("key")
.description("Creates a new blind key pair")
.action(() => {
const sk = new PrivateKey(undefined, "x25519");
console.log("Public:", sk.publicKey.toHex());
console.log("Private:", sk.toHex());
});

blind
.command("encode")
.description("Blinds data")
.argument("data", "The data to blind")
.argument("blinding", "The x25519 public key to use to blind the data")
.action(($data: string, $blinding: string) => {
const data = Data($data);
if (data instanceof type.errors) return exit(`data ${data.summary}`);

const blinding = Key($blinding);
if (blinding instanceof type.errors)
return exit(`blinding ${blinding.summary}`);

console.log(encrypt(blinding, data).toString("hex"));
});

blind
.command("decode")
.description("Unblinds data")
.argument("encoded", "The encoded data to unblind")
.action(async ($encoded: string) => {
const encoded = Hex($encoded);
if (encoded instanceof type.errors)
return exit(`encoded ${encoded.summary}`);

const $privateKey = await password({ message: "Enter your private key" });
if (!$privateKey) return exit("No private key provided");
const privateKey = Key($privateKey);
if (privateKey instanceof type.errors)
return exit(`private key ${privateKey.summary}`);

const decrypted = mayFail(() => decrypt(privateKey, encoded));
if (!decrypted.ok) return exit("wrong private key");

console.log(decode(decrypted.data));
});
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { program } from "./cli.js";
import "./blind.js";
import "./config.js";
import "./payload.js";
import "./wallet.js";
Expand Down
49 changes: 49 additions & 0 deletions src/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { TextDecoder } from "util";

type Encoding = "hex" | "utf8";

const looksLikeHex = (data: Buffer) => {
if (data.length === 0 || data.length % 2 !== 0) return false;

for (const b of data)
if (
!(
(b >= 0x30 && b <= 0x39) || // '0'–'9'
(b >= 0x41 && b <= 0x46) || // 'A'–'F'
(b >= 0x61 && b <= 0x66) // 'a'–'f'
)
)
return false;

return true;
};

const isValidUtf8 = (data: Buffer) => {
let decoded: string;

try {
decoded = new TextDecoder("utf-8", { fatal: true }).decode(data);
} catch {
return false;
}

// eslint-disable-next-line no-control-regex
return !/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(decoded);
};

const detectEncoding = (data: Buffer): Encoding => {
if (looksLikeHex(data)) {
try {
const decoded = Buffer.from(data.toString("ascii"), "hex");
if (isValidUtf8(decoded)) return "hex";
} catch {}
}

if (isValidUtf8(data)) return "utf8";

return "hex";
};

const decode = (data: Buffer): string => data.toString(detectEncoding(data));

export { decode };