@keplr-ewallet/ewallet-sdk-cosmos
Version:
186 lines (162 loc) • 4.83 kB
text/typescript
import { Buffer } from "buffer";
import type { StdSignDoc } from "@cosmjs/amino";
import { serializeSignDoc } from "@cosmjs/amino";
import { sha256 } from "@noble/hashes/sha2";
import { keccak_256 } from "@noble/hashes/sha3";
import { secp256k1 } from "@noble/curves/secp256k1";
import { getBech32Address, getCosmosAddress, getEthAddress } from "./address";
export function makeADR36AminoSignDoc(
signer: string,
data: string | Uint8Array,
): StdSignDoc {
const base64Data =
typeof data === "string"
? Buffer.from(data).toString("base64")
: Buffer.from(data).toString("base64");
return {
chain_id: "",
account_number: "0",
sequence: "0",
fee: {
gas: "0",
amount: [],
},
msgs: [
{
type: "sign/MsgSignData",
value: {
signer,
data: base64Data,
},
},
],
memo: "",
};
}
/**
* Check the sign doc is for ADR-36.
* If the sign doc is expected to be ADR-36, validate the sign doc and throw an error if the sign doc is valid ADR-36.
* @param signDoc
* @param bech32PrefixAccAddr If this argument is provided, validate the signer in the `MsgSignData` with this prefix.
*/
export function checkAndValidateADR36AminoSignDoc(
signDoc: StdSignDoc,
bech32PrefixAccAddr?: string,
): boolean {
const hasOnlyMsgSignData = (() => {
if (
signDoc &&
signDoc.msgs &&
Array.isArray(signDoc.msgs) &&
signDoc.msgs.length === 1
) {
const msg = signDoc.msgs[0];
return msg.type === "sign/MsgSignData";
} else {
return false;
}
})();
if (!hasOnlyMsgSignData) {
return false;
}
if (signDoc.chain_id !== "") {
throw new Error("Chain id should be empty string for ADR-36 signing");
}
if (signDoc.memo !== "") {
throw new Error("Memo should be empty string for ADR-36 signing");
}
if (signDoc.account_number !== "0") {
throw new Error('Account number should be "0" for ADR-36 signing');
}
if (signDoc.sequence !== "0") {
throw new Error('Sequence should be "0" for ADR-36 signing');
}
if (signDoc.fee.gas !== "0") {
throw new Error('Gas should be "0" for ADR-36 signing');
}
if (signDoc.fee.amount.length !== 0) {
throw new Error("Fee amount should be empty array for ADR-36 signing");
}
const msg = signDoc.msgs[0];
if (msg.type !== "sign/MsgSignData") {
throw new Error(`Invalid type of ADR-36 sign msg: ${msg.type}`);
}
if (!msg.value) {
throw new Error("Empty value in the msg");
}
const signer = msg.value.signer;
if (!signer) {
throw new Error("Empty signer in the ADR-36 msg");
}
if (bech32PrefixAccAddr) {
// Basic validation - check if signer starts with the expected prefix
if (!signer.startsWith(bech32PrefixAccAddr)) {
throw new Error(`Invalid signer prefix: expected ${bech32PrefixAccAddr}`);
}
}
const data = msg.value.data;
if (!data) {
throw new Error("Empty data in the ADR-36 msg");
}
const rawData = Buffer.from(data, "base64");
// Validate the data is encoded as base64.
if (rawData.toString("base64") !== data) {
throw new Error("Data is not encoded by base64");
}
if (rawData.length === 0) {
throw new Error("Empty data in the ADR-36 msg");
}
return true;
}
export function verifyADR36AminoSignDoc(
bech32PrefixAccAddr: string,
signDoc: StdSignDoc,
pubKey: Uint8Array,
signature: Uint8Array,
algo: "secp256k1" | "ethsecp256k1" = "secp256k1",
): boolean {
if (!checkAndValidateADR36AminoSignDoc(signDoc, bech32PrefixAccAddr)) {
throw new Error("Invalid sign doc for ADR-36");
}
const expectedSigner = (() => {
if (algo === "ethsecp256k1") {
return getBech32Address(getEthAddress(pubKey), bech32PrefixAccAddr);
}
return getBech32Address(getCosmosAddress(pubKey), bech32PrefixAccAddr);
})();
const signer = signDoc.msgs[0].value.signer;
if (expectedSigner !== signer) {
throw new Error("Unmatched signer");
}
const msg = serializeSignDoc(signDoc);
const messageHash = (() => {
if (algo === "ethsecp256k1") {
return keccak_256(msg);
}
return sha256(msg);
})();
// Signature should be 64 bytes (32 bytes r + 32 bytes s)
if (signature.length !== 64) {
throw new Error(`Invalid length of signature: ${signature.length}`);
}
return secp256k1.verify(signature, messageHash, pubKey, {
prehash: false,
});
}
export function verifyADR36Amino(
bech32PrefixAccAddr: string,
signer: string,
data: string | Uint8Array,
pubKey: Uint8Array,
signature: Uint8Array,
algo: "secp256k1" | "ethsecp256k1" = "secp256k1",
): boolean {
const signDoc = makeADR36AminoSignDoc(signer, data);
return verifyADR36AminoSignDoc(
bech32PrefixAccAddr,
signDoc,
pubKey,
signature,
algo,
);
}