Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Ledger wallet integration #53

Open
wants to merge 12 commits into
base: development
Choose a base branch
from
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
},
"dependencies": {
"@geist-ui/react": "^2.2.0",
"@ledgerhq/hw-transport-webusb": "^6.11.2",
"@limestonefi/api": "^0.1.3",
"@primer/octicons-react": "^16.0.0",
"@verto/js": "^0.0.0-alpha.19",
"@verto/lib": "^0.10.4",
"@zondax/ledger-arweave": "^1.0.0",
"ar-gql": "^0.0.6",
"arverify": "^0.0.11",
"arweave": "^1.10.18",
Expand Down
34 changes: 24 additions & 10 deletions src/background/api/address.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { JWKInterface } from "arweave/node/lib/wallet";
import { getStoreData } from "../../utils/background";
import { MessageFormat } from "../../utils/messenger";
import * as ledger from "../../utils/ledger";

/**
* APIs for getting the user's address / addresses
Expand Down Expand Up @@ -29,25 +30,38 @@ export const activeAddress = () =>
export const publicKey = () =>
new Promise<Partial<MessageFormat>>(async (resolve, _) => {
try {
const address = (await getStoreData())["profile"];
const wallets = (await getStoreData())?.["wallets"];
const store = await getStoreData();
const address = store["profile"];
const wallets = store["wallets"];

if (wallets) {
const keyfileToDecrypt = wallets.find(
(wallet) => wallet.address === address
)?.keyfile;
const wallet = wallets.find((wallet) => wallet.address === address);

if (keyfileToDecrypt) {
const keyfile: JWKInterface = JSON.parse(atob(keyfileToDecrypt));
if (!wallet) {
resolve({
res: false,
message: "No wallets added"
});
return;
}

if (wallet.type === "local") {
const keyfile: JWKInterface = JSON.parse(atob(wallet.keyfile!));
resolve({
res: true,
publicKey: keyfile.n
});
} else {
} else if (wallet.type === "ledger") {
const { address: ledgerAddress, owner } =
await ledger.getWalletInfo();

if (ledgerAddress !== address) {
throw new Error("Ledger address mismatch");
}

resolve({
res: false,
message: "No wallets added"
res: true,
publicKey: owner
});
}
} else {
Expand Down
218 changes: 85 additions & 133 deletions src/background/api/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,94 @@
import { MessageFormat } from "../../utils/messenger";
import {
authenticateUser,
getArweaveConfig,
getStoreData
} from "../../utils/background";
import { authenticateUser, getStoreData } from "../../utils/background";
import { JWKInterface } from "arweave/node/lib/wallet";
import Arweave from "arweave";
import { Wallet } from "../../stores/reducers/wallets";

// encrypt data using the user's keyfile
export const encrypt = (message: MessageFormat, tabURL: string) =>
new Promise<Partial<MessageFormat>>(async (resolve, _) => {
if (!message.data)
return resolve({
res: false,
message: "No data submitted"
});

if (!message.options)
return resolve({
res: false,
message: "No options submitted"
});

try {
await authenticateUser(message.type, tabURL);

resolve({
res: true,
data: await doEncrypt(message),
message: "Success"
});
} catch {
resolve({
res: false,
message: "Error encrypting data"
});
}
export async function encrypt(message: MessageFormat, tabURL: string) {
return handleMessage(message, tabURL, async (message, wallet) => {
const keyfile: JWKInterface = JSON.parse(atob(wallet.keyfile!));
return {
res: true,
data: await doEncrypt(message, keyfile),
message: "Success"
};
});
}

export const decrypt = (message: MessageFormat, tabURL: string) =>
new Promise<Partial<MessageFormat>>(async (resolve, _) => {
if (!message.data)
return resolve({
res: false,
message: "No data submitted"
});

if (!message.options)
return resolve({
res: false,
message: "No options submitted"
});

try {
await authenticateUser(message.type, tabURL);

resolve({
res: true,
data: await doDecrypt(message),
message: "Success"
});
} catch {
resolve({
res: false,
message: "Error decrypting data"
});
}
export async function decrypt(message: MessageFormat, tabURL: string) {
return handleMessage(message, tabURL, async (message, wallet) => {
const keyfile: JWKInterface = JSON.parse(atob(wallet.keyfile!));
return {
res: true,
data: await doDecrypt(message, keyfile),
message: "Success"
};
});
}

export const signature = (message: MessageFormat, tabURL: string) =>
new Promise<Partial<MessageFormat>>(async (resolve, _) => {
if (!message.data)
return resolve({
res: false,
message: "No data submitted"
});
export async function signature(message: MessageFormat, tabURL: string) {
return handleMessage(message, tabURL, async (message, wallet) => {
const keyfile: JWKInterface = JSON.parse(atob(wallet.keyfile!));
return {
res: true,
data: await doSignature(message, keyfile),
message: "Success"
};
});
}

if (!message.options)
return resolve({
async function handleMessage(
message: MessageFormat,
tabURL: string,
handler: (
message: MessageFormat,
wallet: Wallet
) => Promise<Partial<MessageFormat>>
): Promise<Partial<MessageFormat>> {
if (!message.data)
return {
res: false,
message: "No data submitted"
};

if (!message.options)
return {
res: false,
message: "No options submitted"
};

try {
await authenticateUser(message.type, tabURL);

const storeData = await getStoreData(),
wallets = storeData.wallets,
storedAddress = storeData.profile,
wallet = wallets?.find((item) => item.address === storedAddress);

if (!wallets || !storedAddress || !wallet)
throw new Error("No wallets added");

if (wallet.type === "local") {
return handler(message, wallet);
} else if (wallet.type === "ledger") {
return {
res: false,
message: "No options submitted"
});

try {
await authenticateUser(message.type, tabURL);

resolve({
res: true,
data: await doSignature(message),
message: "Success"
});
} catch (e: any) {
resolve({
res: false,
message: e.message || "Error signing data"
});
message: "Action not supported"
};
} else {
throw new Error("Unknown wallet type");
}
});
} catch (e: any) {
return {
res: false,
message: e.message || "Error signing data"
};
}
}

async function doEncrypt(message: MessageFormat) {
const arweave = new Arweave(await getArweaveConfig()),
storeData = await getStoreData(),
storedKeyfiles = storeData?.["wallets"],
storedAddress = storeData?.["profile"],
keyfileToDecrypt = storedKeyfiles?.find(
(item) => item.address === storedAddress
)?.keyfile;

// this should not happen, we already check for wallets in background.ts
if (!storedKeyfiles || !storedAddress || !keyfileToDecrypt)
throw new Error("No wallets added");

const keyfile: JWKInterface = JSON.parse(atob(keyfileToDecrypt)),
obj = {
async function doEncrypt(message: MessageFormat, keyfile: JWKInterface) {
const obj = {
kty: "RSA",
e: "AQAB",
n: keyfile.n,
Expand Down Expand Up @@ -141,30 +117,18 @@ async function doEncrypt(message: MessageFormat) {

const keyBuf = array;

const encryptedData = await arweave.crypto.encrypt(dataBuf, keyBuf),
const encryptedData = await Arweave.crypto.encrypt(dataBuf, keyBuf),
encryptedKey = await crypto.subtle.encrypt(
{ name: message.options.algorithm },
key,
keyBuf
);

return arweave.utils.concatBuffers([encryptedKey, encryptedData]);
return Arweave.utils.concatBuffers([encryptedKey, encryptedData]);
}

async function doDecrypt(message: MessageFormat) {
const arweave = new Arweave(await getArweaveConfig()),
storeData = await getStoreData(),
storedKeyfiles = storeData?.["wallets"],
storedAddress = storeData?.["profile"],
keyfileToDecrypt = storedKeyfiles?.find(
(item) => item.address === storedAddress
)?.keyfile;

if (!storedKeyfiles || !storedAddress || !keyfileToDecrypt)
throw new Error("No wallets added");

const keyfile: JWKInterface = JSON.parse(atob(keyfileToDecrypt)),
obj = {
async function doDecrypt(message: MessageFormat, keyfile: JWKInterface) {
const obj = {
...keyfile,
alg: "RSA-OAEP-256",
ext: true
Expand Down Expand Up @@ -195,27 +159,15 @@ async function doDecrypt(message: MessageFormat) {
encryptedKey
);

const res = await arweave.crypto.decrypt(
const res = await Arweave.crypto.decrypt(
encryptedData,
new Uint8Array(symmetricKey)
);

return arweave.utils.bufferToString(res).split(message.options.salt)[0];
return Arweave.utils.bufferToString(res).split(message.options.salt)[0];
}

async function doSignature(message: MessageFormat) {
const storeData = await getStoreData(),
storedKeyfiles = storeData?.["wallets"],
storedAddress = storeData?.["profile"],
keyfileToDecrypt = storedKeyfiles?.find(
(item) => item.address === storedAddress
)?.keyfile;

if (!storedKeyfiles || !storedAddress || !keyfileToDecrypt)
throw new Error("No wallets added");

const keyfile: JWKInterface = JSON.parse(atob(keyfileToDecrypt));

async function doSignature(message: MessageFormat, keyfile: JWKInterface) {
const cryptoKey = await crypto.subtle.importKey(
"jwk",
keyfile,
Expand Down
Loading