@canvas-js/chain-cosmos
Advanced tools
Comparing version 0.8.26 to 0.9.0-next.1
@@ -1,2 +0,3 @@ | ||
import type { Signature, SessionSigner, Action, Message, Session } from "@canvas-js/interfaces"; | ||
import type { Session } from "@canvas-js/interfaces"; | ||
import { AbstractSessionData, AbstractSessionSigner, Ed25519DelegateSigner } from "@canvas-js/signatures"; | ||
import { CosmosSessionData, ExternalCosmosSigner } from "./types.js"; | ||
@@ -8,16 +9,11 @@ export interface CosmosSignerInit { | ||
} | ||
export declare class CosmosSigner implements SessionSigner { | ||
export declare class CosmosSigner extends AbstractSessionSigner<CosmosSessionData> { | ||
#private; | ||
readonly key: string; | ||
readonly sessionDuration: number | null; | ||
private readonly log; | ||
readonly codecs: ("dag-cbor" | "dag-json")[]; | ||
constructor({ signer, sessionDuration, bech32Prefix }?: CosmosSignerInit); | ||
readonly match: (address: string) => boolean; | ||
readonly verify: typeof Ed25519DelegateSigner.verify; | ||
verifySession(topic: string, session: Session): Promise<void>; | ||
getSession(topic: string, options?: { | ||
timestamp?: number; | ||
fromCache?: boolean; | ||
}): Promise<Session<CosmosSessionData>>; | ||
sign(message: Message<Action | Session>): Signature; | ||
clear(topic: string): Promise<void>; | ||
protected getAddress(): Promise<string>; | ||
protected newSession(data: AbstractSessionData): Promise<Session<CosmosSessionData>>; | ||
} |
@@ -12,17 +12,17 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
}; | ||
var _CosmosSigner_signer, _CosmosSigner_store; | ||
import { logger } from "@libp2p/logger"; | ||
import { Secp256k1Signer, didKeyPattern } from "@canvas-js/signed-cid"; | ||
import { assert, signalInvalidType, validateSessionData, addressPattern, parseAddress } from "./utils.js"; | ||
var _CosmosSigner_signer; | ||
import { AbstractSessionSigner, Ed25519DelegateSigner } from "@canvas-js/signatures"; | ||
import { addressPattern, parseAddress } from "./utils.js"; | ||
import { createDefaultSigner } from "./external_signers/default.js"; | ||
import { createEthereumSigner, verifyEthereum } from "./external_signers/ethereum.js"; | ||
import { createAminoSigner, verifyAmino } from "./external_signers/amino.js"; | ||
import { createBytesSigner, verifyBytes } from "./external_signers/bytes.js"; | ||
import target from "#target"; | ||
export class CosmosSigner { | ||
import { createEthereumSigner, validateEthereumSignedSessionData, verifyEthereum } from "./external_signers/ethereum.js"; | ||
import { createAminoSigner, validateAminoSignedSessionData, verifyAmino } from "./external_signers/amino.js"; | ||
import { createBytesSigner, validateBytesSignedSessionData, verifyBytes } from "./external_signers/bytes.js"; | ||
import { createArbitrarySigner, validateArbitrarySignedSessionData, verifyArbitrary, } from "./external_signers/arbitrary.js"; | ||
export class CosmosSigner extends AbstractSessionSigner { | ||
constructor({ signer, sessionDuration, bech32Prefix } = {}) { | ||
this.log = logger("canvas:chain-cosmos"); | ||
super("chain-cosmos", { createSigner: (init) => new Ed25519DelegateSigner(init), defaultDuration: sessionDuration }); | ||
this.codecs = [Ed25519DelegateSigner.cborCodec, Ed25519DelegateSigner.jsonCodec]; | ||
_CosmosSigner_signer.set(this, void 0); | ||
_CosmosSigner_store.set(this, target.getSessionStore()); | ||
this.match = (address) => addressPattern.test(address); | ||
this.verify = Ed25519DelegateSigner.verify; | ||
const bech32Prefix_ = bech32Prefix == undefined ? "cosmos" : bech32Prefix; | ||
@@ -41,12 +41,11 @@ if (signer == undefined) { | ||
} | ||
else if (signer.type == "arbitrary") { | ||
__classPrivateFieldSet(this, _CosmosSigner_signer, createArbitrarySigner(signer), "f"); | ||
} | ||
else { | ||
throw new Error("invalid signer"); | ||
} | ||
this.sessionDuration = sessionDuration ?? null; | ||
this.key = `CosmosSigner-${signer ? "signer-" + signer.type : "burner"}`; | ||
} | ||
async verifySession(topic, session) { | ||
const { publicKey, address, authorizationData: data, timestamp, duration } = session; | ||
assert(didKeyPattern.test(publicKey), "invalid signing key"); | ||
assert(validateSessionData(data), "invalid session"); | ||
const [chainId, walletAddress] = parseAddress(address); | ||
@@ -57,3 +56,3 @@ const message = { | ||
chainId, | ||
uri: publicKey, | ||
publicKey: publicKey, | ||
issuedAt: new Date(timestamp).toISOString(), | ||
@@ -64,38 +63,37 @@ expirationTime: duration === null ? null : new Date(timestamp + duration).toISOString(), | ||
if (data.signatureType == "ethereum") { | ||
if (!validateEthereumSignedSessionData(data)) { | ||
throw new Error("invalid ethereum session data"); | ||
} | ||
verifyEthereum(message, data); | ||
} | ||
else if (data.signatureType == "amino") { | ||
if (!validateAminoSignedSessionData(data)) { | ||
throw new Error("invalid amino session data"); | ||
} | ||
await verifyAmino(message, data); | ||
} | ||
else if (data.signatureType == "bytes") { | ||
if (!validateBytesSignedSessionData(data)) { | ||
throw new Error("invalid bytes session data"); | ||
} | ||
verifyBytes(message, data); | ||
} | ||
else if (data.signatureType == "arbitrary") { | ||
if (!validateArbitrarySignedSessionData(data)) { | ||
throw new Error("invalid arbitrary session data"); | ||
} | ||
await verifyArbitrary(message, data); | ||
} | ||
else { | ||
signalInvalidType(data.signatureType); | ||
throw new Error("invalid signature type"); | ||
} | ||
} | ||
async getSession(topic, options = {}) { | ||
async getAddress() { | ||
const chainId = await __classPrivateFieldGet(this, _CosmosSigner_signer, "f").getChainId(); | ||
const walletAddress = await __classPrivateFieldGet(this, _CosmosSigner_signer, "f").getAddress(chainId); | ||
const address = `cosmos:${chainId}:${walletAddress}`; | ||
this.log("getting session for %s", address); | ||
{ | ||
const { signer, session } = (await __classPrivateFieldGet(this, _CosmosSigner_store, "f").get(topic, address)) ?? {}; | ||
if (session !== undefined && signer !== undefined) { | ||
const { timestamp, duration } = session; | ||
const t = options.timestamp ?? timestamp; | ||
if (timestamp <= t && t <= timestamp + (duration ?? Infinity)) { | ||
this.log("found session for %s in store: %o", address, session); | ||
return session; | ||
} | ||
else { | ||
this.log("stored session for %s has expired", address); | ||
} | ||
} | ||
} | ||
if (options.fromCache) | ||
return Promise.reject(); | ||
this.log("creating new session for %s", address); | ||
const signer = new Secp256k1Signer(); | ||
const timestamp = options.timestamp ?? Date.now(); | ||
return `cosmos:${chainId}:${walletAddress}`; | ||
} | ||
async newSession(data) { | ||
const { topic, address, timestamp, publicKey, duration } = data; | ||
const [chainId, walletAddress] = parseAddress(address); | ||
const issuedAt = new Date(timestamp); | ||
@@ -106,49 +104,21 @@ const message = { | ||
chainId, | ||
uri: signer.uri, | ||
publicKey: publicKey, | ||
issuedAt: issuedAt.toISOString(), | ||
expirationTime: null, | ||
}; | ||
if (this.sessionDuration !== null) { | ||
message.expirationTime = new Date(timestamp + this.sessionDuration).toISOString(); | ||
if (duration !== null) { | ||
message.expirationTime = new Date(timestamp + duration).toISOString(); | ||
} | ||
const signResult = await __classPrivateFieldGet(this, _CosmosSigner_signer, "f").sign(message, walletAddress, chainId); | ||
const session = { | ||
return { | ||
type: "session", | ||
address: address, | ||
publicKey: signer.uri, | ||
publicKey: publicKey, | ||
authorizationData: signResult, | ||
blockhash: null, | ||
timestamp, | ||
duration: this.sessionDuration, | ||
timestamp: timestamp, | ||
duration: duration, | ||
}; | ||
// save the session and private key in the cache and the store | ||
__classPrivateFieldGet(this, _CosmosSigner_store, "f").set(topic, address, session, signer); | ||
this.log("created new session for %s: %o", address, session); | ||
return session; | ||
} | ||
sign(message) { | ||
if (message.payload.type === "action") { | ||
const { address, timestamp } = message.payload; | ||
const { signer, session } = __classPrivateFieldGet(this, _CosmosSigner_store, "f").get(message.topic, address) ?? {}; | ||
assert(signer !== undefined && session !== undefined); | ||
assert(address === session.address); | ||
assert(timestamp >= session.timestamp); | ||
assert(timestamp <= session.timestamp + (session.duration ?? Infinity)); | ||
return signer.sign(message); | ||
} | ||
else if (message.payload.type === "session") { | ||
const { signer, session } = __classPrivateFieldGet(this, _CosmosSigner_store, "f").get(message.topic, message.payload.address) ?? {}; | ||
assert(signer !== undefined && session !== undefined); | ||
// only sign our own current sessions | ||
assert(message.payload === session); | ||
return signer.sign(message); | ||
} | ||
else { | ||
signalInvalidType(message.payload); | ||
} | ||
} | ||
async clear(topic) { | ||
__classPrivateFieldGet(this, _CosmosSigner_store, "f").clear(topic); | ||
} | ||
} | ||
_CosmosSigner_signer = new WeakMap(), _CosmosSigner_store = new WeakMap(); | ||
_CosmosSigner_signer = new WeakMap(); |
@@ -13,13 +13,8 @@ import { AminoSignResponse, StdSignDoc } from "@keplr-wallet/types"; | ||
sign: (cosmosMessage: CosmosMessage, address: string, chainId: string) => Promise<{ | ||
signature: { | ||
signature: Uint8Array; | ||
pub_key: { | ||
type: "tendermint/PubKeySecp256k1"; | ||
value: Uint8Array; | ||
}; | ||
}; | ||
signature: Uint8Array; | ||
signatureType: "amino"; | ||
}>; | ||
}; | ||
export declare const verifyAmino: (message: CosmosMessage, sessionData: AminoSignedSessionData) => Promise<void>; | ||
export declare const verifyAmino: (message: CosmosMessage, { signature }: AminoSignedSessionData) => Promise<void>; | ||
export type AminoSignedSessionData = Awaited<ReturnType<ReturnType<typeof createAminoSigner>["sign"]>>; | ||
export declare function validateAminoSignedSessionData(data: any): data is AminoSignedSessionData; |
import * as cbor from "@ipld/dag-cbor"; | ||
import { base64 } from "multiformats/bases/base64"; | ||
import { pubkeyType, rawSecp256k1PubkeyToRawAddress, serializeSignDoc } from "@cosmjs/amino"; | ||
import { rawSecp256k1PubkeyToRawAddress, serializeSignDoc } from "@cosmjs/amino"; | ||
import { getSessionSignatureData } from "../signatureData.js"; | ||
@@ -13,13 +13,7 @@ import { fromBech32, toBech32 } from "@cosmjs/encoding"; | ||
const msg = cbor.encode(cosmosMessage); | ||
const signDoc = await getSessionSignatureData(msg, address); | ||
const signDoc = getSessionSignatureData(msg, address); | ||
const signRes = await signer.signAmino(chainId, address, signDoc); | ||
const stdSig = signRes.signature; | ||
return { | ||
signature: { | ||
signature: base64.baseDecode(stdSig.signature), | ||
pub_key: { | ||
type: pubkeyType.secp256k1, | ||
value: base64.baseDecode(stdSig.pub_key.value), | ||
}, | ||
}, | ||
signature: base64.baseDecode(stdSig.signature), | ||
signatureType: "amino", | ||
@@ -29,20 +23,23 @@ }; | ||
}); | ||
export const verifyAmino = async (message, sessionData) => { | ||
const { pub_key: { value: pub_key }, signature, } = sessionData.signature; | ||
export const verifyAmino = async (message, { signature }) => { | ||
const walletAddress = message.address; | ||
const { prefix } = fromBech32(walletAddress); | ||
if (walletAddress !== toBech32(prefix, rawSecp256k1PubkeyToRawAddress(pub_key))) { | ||
throw new Error("Session signed with a pubkey that doesn't match the session address"); | ||
} | ||
// the payload can either be signed directly, or encapsulated in a SignDoc | ||
const encodedMessage = cbor.encode(message); | ||
const signDocPayload = await getSessionSignatureData(encodedMessage, walletAddress); | ||
const signDocPayload = getSessionSignatureData(encodedMessage, walletAddress); | ||
const signDocDigest = sha256(serializeSignDoc(signDocPayload)); | ||
const digest = sha256(encodedMessage); | ||
// compare the signature against the directly signed and signdoc digests | ||
let isValid = false; | ||
isValid || (isValid = secp256k1.verify(signature, signDocDigest, pub_key)); | ||
isValid || (isValid = secp256k1.verify(signature, digest, pub_key)); | ||
if (!isValid) | ||
throw Error("invalid signature"); | ||
// try with both values of the recovery bit | ||
for (const recoveryBit of [0, 1]) { | ||
const signatureWithRecoveryBit = secp256k1.Signature.fromCompact(signature).addRecoveryBit(recoveryBit); | ||
// get the public key from the signature and digest | ||
const pub_key = signatureWithRecoveryBit.recoverPublicKey(signDocDigest).toRawBytes(); | ||
// get the address from the public key | ||
const address = toBech32(prefix, rawSecp256k1PubkeyToRawAddress(pub_key)); | ||
if (address == walletAddress) | ||
return; | ||
} | ||
throw Error("invalid signature"); | ||
}; | ||
export function validateAminoSignedSessionData(data) { | ||
return data.signatureType == "amino" && data.signature instanceof Uint8Array; | ||
} |
@@ -27,1 +27,2 @@ import { CosmosMessage } from "../types.js"; | ||
export type BytesSignedSessionData = Awaited<ReturnType<ReturnType<typeof createBytesSigner>["sign"]>>; | ||
export declare function validateBytesSignedSessionData(data: any): data is BytesSignedSessionData; |
@@ -36,1 +36,9 @@ import * as cbor from "@ipld/dag-cbor"; | ||
}; | ||
export function validateBytesSignedSessionData(data) { | ||
return (data.signatureType == "bytes" && | ||
data.signature instanceof Uint8Array && | ||
data.signature.pub_key instanceof Object && | ||
data.signature.pub_key.value instanceof Uint8Array && | ||
typeof data.signature.pub_key.type === "string" && | ||
data.signature.pub_key.type == pubkeyType.secp256k1); | ||
} |
@@ -19,1 +19,2 @@ import { CosmosMessage } from "../types.js"; | ||
export type EthereumSignedSessionData = Awaited<ReturnType<ReturnType<typeof createEthereumSigner>["sign"]>>; | ||
export declare function validateEthereumSignedSessionData(data: any): data is EthereumSignedSessionData; |
import { fromBech32, toBech32 } from "@cosmjs/encoding"; | ||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; | ||
import { verifyMessage } from "ethers"; | ||
import { verifyMessage, getBytes, hexlify } from "ethers"; | ||
export function encodeReadableEthereumMessage(message) { | ||
@@ -11,3 +10,3 @@ return ` | ||
issuedAt: ${message.issuedAt} | ||
uri: ${message.uri} | ||
uri: ${message.publicKey} | ||
`; | ||
@@ -17,4 +16,5 @@ } | ||
getAddress: async (chainId) => { | ||
const address = (await signer.getAddress(chainId)).substring(2); | ||
return toBech32(bech32Prefix, hexToBytes(address)); | ||
const address = await signer.getAddress(chainId); | ||
// this assumes that `address` is an ethereum hex address prefixed by 0x | ||
return toBech32(bech32Prefix, getBytes(address)); | ||
}, | ||
@@ -24,6 +24,7 @@ getChainId: signer.getChainId, | ||
const encodedMessage = encodeReadableEthereumMessage(cosmosMessage); | ||
const ethAddress = `0x${bytesToHex(fromBech32(signerAddress).data)}`; | ||
const hexSignature = await signer.signEthereum(chainId, ethAddress, encodedMessage); | ||
const ethAddress = hexlify(fromBech32(signerAddress).data); | ||
const signature = await signer.signEthereum(chainId, ethAddress, encodedMessage); | ||
// signature is a hex string prefixed by 0x | ||
return { | ||
signature: hexToBytes(hexSignature.substring(2)), | ||
signature: getBytes(signature), | ||
signatureType: "ethereum", | ||
@@ -37,7 +38,10 @@ }; | ||
// validate ethereum signature | ||
const recoveredAddress = verifyMessage(encodedReadableMessage, `0x${bytesToHex(sessionData.signature)}`); | ||
const recoveredAddress = verifyMessage(encodedReadableMessage, hexlify(sessionData.signature)); | ||
const { prefix } = fromBech32(walletAddress); | ||
if (toBech32(prefix, hexToBytes(recoveredAddress.substring(2))) !== walletAddress) { | ||
if (toBech32(prefix, getBytes(recoveredAddress)) !== walletAddress) { | ||
throw Error("invalid signature"); | ||
} | ||
}; | ||
export function validateEthereumSignedSessionData(data) { | ||
return data.signatureType == "ethereum" && data.signature instanceof Uint8Array; | ||
} |
@@ -1,2 +0,1 @@ | ||
import type { StdSignDoc } from "@cosmjs/amino"; | ||
export declare const getSessionSignatureData: (sessionPayload: Uint8Array, address: string, chain_id?: string) => Promise<StdSignDoc>; | ||
export declare const getSessionSignatureData: (sessionPayload: Uint8Array, address: string, chain_id?: string) => import("@cosmjs/amino").StdSignDoc; |
import { makeSignDoc } from "@cosmjs/amino"; | ||
import { toBase64 } from "@cosmjs/encoding"; | ||
export const getSessionSignatureData = async (sessionPayload, address, chain_id) => { | ||
export const getSessionSignatureData = (sessionPayload, address, chain_id) => { | ||
const accountNumber = 0; | ||
@@ -5,0 +5,0 @@ const sequence = 0; |
import type { EthereumSignedSessionData, EthereumSigner } from "./external_signers/ethereum.js"; | ||
import type { BytesSignedSessionData, BytesSigner } from "./external_signers/bytes.js"; | ||
import type { AminoSignedSessionData, AminoSigner } from "./external_signers/amino.js"; | ||
import { ArbitrarySignedSessionData, ArbitrarySigner } from "./external_signers/arbitrary.js"; | ||
export type CosmosMessage = { | ||
@@ -8,7 +9,7 @@ topic: string; | ||
chainId: string; | ||
uri: string; | ||
publicKey: string; | ||
issuedAt: string; | ||
expirationTime: string | null; | ||
}; | ||
export type CosmosSessionData = EthereumSignedSessionData | BytesSignedSessionData | AminoSignedSessionData; | ||
export type ExternalCosmosSigner = EthereumSigner | AminoSigner | BytesSigner; | ||
export type CosmosSessionData = EthereumSignedSessionData | BytesSignedSessionData | AminoSignedSessionData | ArbitrarySignedSessionData; | ||
export type ExternalCosmosSigner = EthereumSigner | AminoSigner | BytesSigner | ArbitrarySigner; |
@@ -1,6 +0,2 @@ | ||
import type { CosmosSessionData } from "./types.js"; | ||
export declare function assert(condition: boolean, message?: string): asserts condition; | ||
export declare function signalInvalidType(type: never): never; | ||
export declare const addressPattern: RegExp; | ||
export declare function parseAddress(address: string): [chain: string, walletAddress: string]; | ||
export declare function validateSessionData(data: unknown): data is CosmosSessionData; |
@@ -1,10 +0,1 @@ | ||
export function assert(condition, message) { | ||
if (!condition) { | ||
throw new Error(message ?? "assertion failed"); | ||
} | ||
} | ||
export function signalInvalidType(type) { | ||
console.error(type); | ||
throw new TypeError("internal error: invalid type"); | ||
} | ||
export const addressPattern = /^cosmos:([0-9a-z\-_]+):([a-zA-Fa-f0-9]+)$/; | ||
@@ -19,29 +10,9 @@ export function parseAddress(address) { | ||
} | ||
export function validateSessionData(data) { | ||
try { | ||
extractSessionData(data); | ||
} | ||
catch (error) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
function extractSessionData(data) { | ||
if (data.signatureType == "amino") { | ||
const signature = data.signature.signature; | ||
const pub_key_value = data.signature.pub_key.value; | ||
const pub_key_type = data.signature.pub_key.type; | ||
if (signature instanceof Uint8Array && | ||
pub_key_value instanceof Uint8Array && | ||
typeof pub_key_type === "string" && | ||
pub_key_type == "tendermint/PubKeySecp256k1") { | ||
const signature = data.signature; | ||
if (signature instanceof Uint8Array) { | ||
return { | ||
signatureType: "amino", | ||
signature: { | ||
signature: signature, | ||
pub_key: { | ||
type: pub_key_type, | ||
value: pub_key_value, | ||
}, | ||
}, | ||
signature, | ||
}; | ||
@@ -79,3 +50,18 @@ } | ||
} | ||
else if (data.signatureType == "arbitrary") { | ||
const signature = data.signature; | ||
if (signature.pub_key instanceof Object && signature.signature instanceof Uint8Array) { | ||
return { | ||
signatureType: "arbitrary", | ||
signature: { | ||
pub_key: { | ||
type: signature.pub_key.type, | ||
value: signature.pub_key.value, | ||
}, | ||
signature: signature.signature, | ||
}, | ||
}; | ||
} | ||
} | ||
throw Error(`invalid session data`); | ||
} |
{ | ||
"name": "@canvas-js/chain-cosmos", | ||
"version": "0.8.26", | ||
"version": "0.9.0-next.1", | ||
"type": "module", | ||
@@ -11,12 +11,6 @@ "author": "Canvas Technologies, Inc. (https://canvas.xyz)", | ||
], | ||
"imports": { | ||
"#target": { | ||
"node": "./lib/targets/node/index.js", | ||
"browser": "./lib/targets/browser/index.js", | ||
"default": "./lib/targets/default/index.js" | ||
} | ||
}, | ||
"dependencies": { | ||
"@canvas-js/interfaces": "0.8.26", | ||
"@canvas-js/signed-cid": "0.8.26", | ||
"@canvas-js/interfaces": "0.9.0-next.1", | ||
"@canvas-js/signatures": "0.9.0-next.1", | ||
"@canvas-js/utils": "0.9.0-next.1", | ||
"@cosmjs/amino": "^0.30.1", | ||
@@ -31,4 +25,5 @@ "@cosmjs/crypto": "^0.30.1", | ||
"@noble/hashes": "^1.3.2", | ||
"ethers": "^6.9.0", | ||
"multiformats": "^13.0.1" | ||
} | ||
} |
26255
14
22
542
+ Addedethers@^6.9.0
+ Added@adraffy/ens-normalize@1.10.1(transitive)
+ Added@canvas-js/interfaces@0.9.0-next.1(transitive)
+ Added@canvas-js/signatures@0.9.0-next.1(transitive)
+ Added@canvas-js/utils@0.9.0-next.1(transitive)
+ Added@noble/curves@1.2.0(transitive)
+ Added@noble/hashes@1.3.2(transitive)
+ Added@types/node@22.7.5(transitive)
+ Addedaes-js@4.0.0-beta.5(transitive)
+ Addedethers@6.13.5(transitive)
+ Addedtslib@2.7.0(transitive)
+ Addedundici-types@6.19.8(transitive)
+ Addedws@8.17.1(transitive)
- Removed@canvas-js/signed-cid@0.8.26
- Removed@canvas-js/interfaces@0.8.26(transitive)
- Removed@canvas-js/signed-cid@0.8.26(transitive)