Skip to content

Commit

Permalink
Merge pull request #2561 from getAlby/fix/mnemonic-lnurl-auth
Browse files Browse the repository at this point in the history
fix: mnemonic auth to match LUD05 spec
  • Loading branch information
rolznz authored Jul 13, 2023
2 parents a51681d + df0ec88 commit b63d3f9
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 48 deletions.
102 changes: 78 additions & 24 deletions src/extension/background-script/actions/lnurl/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,93 @@
import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings";
import Mnemonic from "~/extension/background-script/mnemonic";
import state from "~/extension/background-script/state";
import { btcFixture } from "~/fixtures/btc";
import type { LNURLDetails } from "~/types";

import { authFunction } from "../auth";
import { authFunction, getPathSuffix } from "../auth";

let fetchedUrl: string;
jest.mock("~/extension/background-script/state");
jest.mock("axios", () => ({
get: (requestUrl: string) => {
fetchedUrl = requestUrl;
return { data: { status: "OK" } };
},
}));

const mockState = {
settings: mockSettings,
getConnector: () => ({
signMessage: () =>
Promise.resolve({
data: {
signature:
"rnu5pnhanjs3bfxz33fuyf9ywzrmkm1ns6jxdraxff1irq3hpxcbkce6zk34ee9bh7bamgd891tfy4gq1y119w53qg1ap5zodwi4u51n",
},
const passwordMock = jest.fn;

describe("auth with mnemonic", () => {
test("getPathSuffix matches test vector", () => {
expect(
getPathSuffix(
"site.com",
"7d417a6a5e9a6a4a879aeaba11a11838764c8fa2b959c242d43dea682b3e409b"
)
).toStrictEqual([1588488367, 2659270754, 38110259, 4136336762]);
});

test("matches LUD05 test vector", async () => {
const lnurlDetails: LNURLDetails = {
domain: "site.com",
k1: "dea6a5e410ae8db8872b30ed715d9c10bbaca1dda653396511a40bb353529572",
tag: "login",
url: "https://site.com/lnurl-login",
};

const mockState = {
password: passwordMock,
currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97",
getAccount: () => ({
mnemonic: btcFixture.mnemnoic,
bitcoinNetwork: "regtest",
useMnemonicForLnurlAuth: true,
}),
}),
};

const lnurlDetails: LNURLDetails = {
domain: "lnurl.fiatjaf.com",
k1: "dea6a5e410ae8db8872b30ed715d9c10bbaca1dda653396511a40bb353529572",
tag: "login",
url: "https://lnurl.fiatjaf.com/lnurl-login",
};

// skip till this is solved:
// https://github.com/axios/axios/pull/5146
// test works if we do not use:
// https://github.com/getAlby/lightning-browser-extension/blob/refactor/manifest-v3-support/src/extension/background-script/actions/lnurl/auth.ts#L93
getMnemonic: () => new Mnemonic(btcFixture.mnemnoic),
getConnector: jest.fn(),
};

state.getState = jest.fn().mockReturnValue(mockState);

expect(await authFunction({ lnurlDetails })).toStrictEqual({
success: true,
status: "OK",
reason: undefined,
authResponseData: { status: "OK" },
});

// ensure the public key is constant for the given mnemonic
expect(new URL(fetchedUrl).searchParams.get("key")).toBe(
"027da5d64331f61260eb8e2b356403446555f525bc7dc35b991ec1447e4f58991f"
);
});
});

// FIXME: this test is doing a real API call, and does not
// test if the user logs in to the correct account or not
describe.skip("auth", () => {
const mockState = {
settings: mockSettings,
getConnector: () => ({
signMessage: () =>
Promise.resolve({
data: {
signature:
"rnu5pnhanjs3bfxz33fuyf9ywzrmkm1ns6jxdraxff1irq3hpxcbkce6zk34ee9bh7bamgd891tfy4gq1y119w53qg1ap5zodwi4u51n",
},
}),
}),
};
test("returns success response", async () => {
state.getState = jest.fn().mockReturnValue(mockState);

const lnurlDetails: LNURLDetails = {
domain: "lnurl.fiatjaf.com",
k1: "dea6a5e410ae8db8872b30ed715d9c10bbaca1dda653396511a40bb353529572",
tag: "login",
url: "https://lnurl.fiatjaf.com/lnurl-login",
};

expect(await authFunction({ lnurlDetails })).toStrictEqual({
success: true,
status: "OK",
Expand Down
85 changes: 61 additions & 24 deletions src/extension/background-script/actions/lnurl/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as secp256k1 from "@noble/secp256k1";
import fetchAdapter from "@vespaiach/axios-fetch-adapter";
import axios from "axios";
import { Buffer } from "buffer";
import Hex from "crypto-js/enc-hex";
import Utf8 from "crypto-js/enc-utf8";
import hmacSHA256 from "crypto-js/hmac-sha256";
import sha256 from "crypto-js/sha256";
import PubSub from "pubsub-js";
Expand Down Expand Up @@ -40,14 +43,35 @@ export async function authFunction({
throw new Error("LNURL-AUTH FAIL: no account selected");
}

let keyMaterialForSignature: string;
const url = new URL(lnurlDetails.url);
if (!url.host) {
throw new Error("Invalid input");
}

let linkingKeyPriv: string;

// use mnemonic for LNURL auth
if (account.mnemonic && account.useMnemonicForLnurlAuth) {
const mnemonic = await state.getState().getMnemonic();

keyMaterialForSignature = await mnemonic.signMessage(
LNURLAUTH_CANONICAL_PHRASE
// See https://github.com/lnurl/luds/blob/luds/05.md
const hashingKey = mnemonic.deriveKey("m/138'/0");
const hashingPrivateKey = hashingKey.privateKey as Uint8Array;

const pathSuffix = getPathSuffix(
url.host,
secp256k1.utils.bytesToHex(hashingPrivateKey)
);

// Derive key manually (rather than using mnemonic.deriveKey with full path) due to
// https://github.com/paulmillr/scure-bip32/issues/8
let linkingKey = mnemonic.deriveKey("m/138'");
for (const index of pathSuffix) {
linkingKey = linkingKey.deriveChild(index);
}

linkingKeyPriv = secp256k1.utils.bytesToHex(
linkingKey.privateKey as Uint8Array
);
} else {
const connector = await state.getState().getConnector();
Expand All @@ -61,31 +85,28 @@ export async function authFunction({
},
});

keyMaterialForSignature = signResponse.data.signature;
}
const keyMaterialForSignature = signResponse.data.signature;

// make sure we got a signature
if (!keyMaterialForSignature) {
throw new Error("Invalid Signature");
}
// make sure we got a signature
if (!keyMaterialForSignature) {
throw new Error("Invalid Signature");
}

const hashingKey = sha256(keyMaterialForSignature).toString(Hex);
const url = new URL(lnurlDetails.url);
if (!url.host || !hashingKey) {
throw new Error("Invalid input");
}
const hashingKey = sha256(keyMaterialForSignature).toString(Hex);

let linkingKeyPriv;
const { settings } = state.getState();
if (settings.isUsingLegacyLnurlAuthKey) {
linkingKeyPriv = hmacSHA256(url.host, hashingKey).toString(Hex);
} else {
linkingKeyPriv = hmacSHA256(url.host, Hex.parse(hashingKey)).toString(Hex);
const { settings } = state.getState();
if (settings.isUsingLegacyLnurlAuthKey) {
linkingKeyPriv = hmacSHA256(url.host, hashingKey).toString(Hex);
} else {
linkingKeyPriv = hmacSHA256(url.host, Hex.parse(hashingKey)).toString(
Hex
);
}
}

// make sure we got a hashingKey and a linkingkey (just to be sure for whatever reason)
if (!hashingKey || !linkingKeyPriv) {
throw new Error("Invalid hashingKey/linkingKey");
// make sure we got a linkingkey (just to be sure for whatever reason)
if (!linkingKeyPriv) {
throw new Error("Invalid linkingKey");
}

const signer = new HashKeySigner(linkingKeyPriv);
Expand Down Expand Up @@ -151,11 +172,27 @@ export async function authFunction({
origin,
});

throw new Error(e.message);
throw e;
}
}
}

// see https://github.com/lnurl/luds/blob/luds/05.md
export function getPathSuffix(domain: string, privateKeyHex: string) {
const derivationMaterial = hmacSHA256(
Utf8.parse(domain),
Hex.parse(privateKeyHex)
).toString(Hex);

const buf = Buffer.from(derivationMaterial, "hex");

const pathSuffix = [];
for (let i = 0; i < 4; i++) {
pathSuffix.push(buf.readUint32BE(i * 4));
}
return pathSuffix;
}

const auth = async (message: MessageLnurlAuth) => {
const { lnurlDetails, origin } = message.args;
const response = await authFunction({ lnurlDetails, origin });
Expand Down

0 comments on commit b63d3f9

Please sign in to comment.