diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 2b01f294..8ac4d6f7 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -4,4 +4,5 @@ export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress } export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { types }; diff --git a/packages/sdk/src/pen.spec.ts b/packages/sdk/src/pen.spec.ts new file mode 100644 index 00000000..8a7fbadb --- /dev/null +++ b/packages/sdk/src/pen.spec.ts @@ -0,0 +1,49 @@ +import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto"; +import { Encoding } from "@iov/encoding"; + +import { Secp256k1Pen } from "./pen"; + +const { fromHex } = Encoding; + +describe("Sec256k1Pen", () => { + it("can be constructed", () => { + const pen = new Secp256k1Pen( + "zebra slush diet army arrest purpose hawk source west glimpse custom record", + ); + expect(pen).toBeTruthy(); + }); + + describe("getPubkey", () => { + it("returns compressed pubkey", async () => { + // special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling + // m/44'/118'/0'/0/0 + // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 + const pen = new Secp256k1Pen( + "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling", + ); + expect(await pen.getPubkey()).toEqual( + fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"), + ); + }); + }); + + describe("createSignature", () => { + it("creates correct signatures", async () => { + // special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling + // m/44'/118'/0'/0/0 + // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 + const pen = new Secp256k1Pen( + "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling", + ); + const data = Encoding.toAscii("foo bar"); + const signature = await pen.createSignature(data); + + const valid = await Secp256k1.verifySignature( + new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)), + new Sha256(data).digest(), + fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"), + ); + expect(valid).toEqual(true); + }); + }); +}); diff --git a/packages/sdk/src/pen.ts b/packages/sdk/src/pen.ts new file mode 100644 index 00000000..59e04108 --- /dev/null +++ b/packages/sdk/src/pen.ts @@ -0,0 +1,87 @@ +import { + Bip39, + EnglishMnemonic, + Secp256k1, + Sha256, + Sha512, + Slip10, + Slip10Curve, + Slip10RawIndex, +} from "@iov/crypto"; + +export type PrehashType = "sha256" | "sha512" | null; + +/** + * A pen is the most basic tool you can think of for signing. It works + * everywhere and can be used intuitively by everyone. However, it does not + * come with a great amount of features. End of semi suitable metaphor. + * + * This wraps a single keypair and allows for signing. + * + * Non-goals of this types are: multi account support, persistency, data migrations, + * obfuscation of sensitive data. + */ +export interface Pen { + readonly getPubkey: () => Promise; + readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; +} + +function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array { + switch (type) { + case null: + return new Uint8Array([...bytes]); + case "sha256": + return new Sha256(bytes).digest(); + case "sha512": + return new Sha512(bytes).digest(); + default: + throw new Error("Unknown prehash type"); + } +} + +/** + * The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a` + * with 0-based account index `a`. + */ +export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] { + return [ + Slip10RawIndex.hardened(44), + Slip10RawIndex.hardened(118), + Slip10RawIndex.hardened(0), + Slip10RawIndex.normal(0), + Slip10RawIndex.normal(a), + ]; +} + +export class Secp256k1Pen implements Pen { + private readonly mnemonic: EnglishMnemonic; + private readonly hdPath: readonly Slip10RawIndex[]; + + public constructor(mnemonic: string, hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0)) { + this.mnemonic = new EnglishMnemonic(mnemonic); + this.hdPath = hdPath; + } + + public async getPubkey(): Promise { + const privkey = await this.getPrivkey(); + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return Secp256k1.compressPubkey(uncompressed); + } + + /** + * Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes). + */ + public async createSignature( + signBytes: Uint8Array, + prehashType: PrehashType = "sha256", + ): Promise { + const message = prehash(signBytes, prehashType); + const signature = await Secp256k1.createSignature(message, await this.getPrivkey()); + return new Uint8Array([...signature.r(32), ...signature.s(32)]); + } + + private async getPrivkey(): Promise { + const seed = await Bip39.mnemonicToSeed(this.mnemonic); + return Slip10.derivePath(Slip10Curve.Secp256k1, seed, this.hdPath).privkey; + } +} diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 18324a0a..a341b70c 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -3,4 +3,5 @@ export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress } export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { types }; diff --git a/packages/sdk/types/pen.d.ts b/packages/sdk/types/pen.d.ts new file mode 100644 index 00000000..728b933a --- /dev/null +++ b/packages/sdk/types/pen.d.ts @@ -0,0 +1,32 @@ +import { Slip10RawIndex } from "@iov/crypto"; +export declare type PrehashType = "sha256" | "sha512" | null; +/** + * A pen is the most basic tool you can think of for signing. It works + * everywhere and can be used intuitively by everyone. However, it does not + * come with a great amount of features. End of semi suitable metaphor. + * + * This wraps a single keypair and allows for signing. + * + * Non-goals of this types are: multi account support, persistency, data migrations, + * obfuscation of sensitive data. + */ +export interface Pen { + readonly getPubkey: () => Promise; + readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; +} +/** + * The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a` + * with 0-based account index `a`. + */ +export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[]; +export declare class Secp256k1Pen implements Pen { + private readonly mnemonic; + private readonly hdPath; + constructor(mnemonic: string, hdPath?: readonly Slip10RawIndex[]); + getPubkey(): Promise; + /** + * Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes). + */ + createSignature(signBytes: Uint8Array, prehashType?: PrehashType): Promise; + private getPrivkey; +}