UNPKG

lit-siwe

Version:
265 lines (264 loc) 11.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateNonce = exports.checkContractWalletSignature = exports.SiweMessage = exports.SignatureType = exports.ErrorTypes = void 0; const random_1 = require("@stablelib/random"); // TODO: Figure out how to get types from this lib: // import { Contract, ethers, utils } from 'ethers'; const contracts_1 = require("@ethersproject/contracts"); const wallet_1 = require("@ethersproject/wallet"); const hash_1 = require("@ethersproject/hash"); const siwe_parser_1 = require("@spruceid/siwe-parser"); /** * Possible message error types. */ var ErrorTypes; (function (ErrorTypes) { /**Thrown when the `validate()` function can verify the message. */ ErrorTypes["INVALID_SIGNATURE"] = "Invalid signature."; /**Thrown when the `expirationTime` is present and in the past. */ ErrorTypes["EXPIRED_MESSAGE"] = "Expired message."; /**Thrown when some required field is missing. */ ErrorTypes["MALFORMED_SESSION"] = "Malformed session."; })(ErrorTypes = exports.ErrorTypes || (exports.ErrorTypes = {})); /**@deprecated * Possible signature types that this library supports. * * This enum will be removed in future releases. And signature type will be * inferred from version. */ var SignatureType; (function (SignatureType) { /**EIP-191 signature scheme */ SignatureType["PERSONAL_SIGNATURE"] = "Personal signature"; })(SignatureType = exports.SignatureType || (exports.SignatureType = {})); class SiweMessage { /** * Creates a parsed Sign-In with Ethereum Message (EIP-4361) object from a * string or an object. If a string is used an ABNF parser is called to * validate the parameter, otherwise the fields are attributed. * @param param {string | SiweMessage} Sign message as a string or an object. */ constructor(param) { if (typeof param === "string") { const parsedMessage = new siwe_parser_1.ParsedMessage(param); this.domain = parsedMessage.domain; this.address = parsedMessage.address; this.statement = parsedMessage.statement; this.uri = parsedMessage.uri; this.version = parsedMessage.version; this.nonce = parsedMessage.nonce; this.issuedAt = parsedMessage.issuedAt; this.expirationTime = parsedMessage.expirationTime; this.notBefore = parsedMessage.notBefore; this.requestId = parsedMessage.requestId; this.chainId = parsedMessage.chainId; this.resources = parsedMessage.resources; } else { Object.assign(this, param); if (typeof this.chainId === "string") { this.chainId = parseInt(this.chainId); } } } /** * Given a sign message (EIP-4361) returns the correct matching groups. * @param message {string} * @returns {RegExpExecArray} The matching groups for the message */ regexFromMessage(message) { const parsedMessage = new siwe_parser_1.ParsedMessageRegExp(message); return parsedMessage.match; } /** * This function can be used to retrieve an EIP-4361 formated message for * signature, although you can call it directly it's advised to use * [signMessage()] instead which will resolve to the correct method based * on the [type] attribute of this object, in case of other formats being * implemented. * @returns {string} EIP-4361 formated message, ready for EIP-191 signing. */ toMessage() { const header = `${this.domain} wants you to sign in with your Ethereum account:`; const uriField = `URI: ${this.uri}`; let prefix = [header, this.address].join("\n"); const versionField = `Version: ${this.version}`; if (!this.nonce) { this.nonce = (0, exports.generateNonce)(); } const chainField = `Chain ID: ` + this.chainId || "1"; const nonceField = `Nonce: ${this.nonce}`; const suffixArray = [uriField, versionField, chainField, nonceField]; if (this.issuedAt) { Date.parse(this.issuedAt); } this.issuedAt = this.issuedAt ? this.issuedAt : new Date().toISOString(); suffixArray.push(`Issued At: ${this.issuedAt}`); if (this.expirationTime) { const expiryField = `Expiration Time: ${this.expirationTime}`; suffixArray.push(expiryField); } if (this.notBefore) { suffixArray.push(`Not Before: ${this.notBefore}`); } if (this.requestId) { suffixArray.push(`Request ID: ${this.requestId}`); } if (this.resources) { suffixArray.push([`Resources:`, ...this.resources.map((x) => `- ${x}`)].join("\n")); } let suffix = suffixArray.join("\n"); prefix = [prefix, this.statement].join("\n\n"); if (this.statement) { prefix += "\n"; } return [prefix, suffix].join("\n"); } /** @deprecated * signMessage method is deprecated, use prepareMessage instead. * * This method parses all the fields in the object and creates a sign * message according with the type defined. * @returns {string} Returns a message ready to be signed according with the * type defined in the object. */ signMessage() { console && console.warn && console.warn("signMessage method is deprecated, use prepareMessage instead."); return this.prepareMessage(); } /** * This method parses all the fields in the object and creates a sign * message according with the type defined. * @returns {string} Returns a message ready to be signed according with the * type defined in the object. */ prepareMessage() { let message; switch (this.version) { case "1": { message = this.toMessage(); break; } default: { message = this.toMessage(); break; } } return message; } /** * Validates the integrity of the fields of this objects by matching it's * signature. * @param provider A Web3 provider able to perform a contract check, this is * required if support for Smart Contract Wallets that implement EIP-1271 is * needed. * @returns {Promise<SiweMessage>} This object if valid. */ validate(signature = this.signature, provider) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { const message = this.prepareMessage(); try { let missing = []; if (!message) { missing.push("`message`"); } if (!signature) { missing.push("`signature`"); } if (!this.address) { missing.push("`address`"); } if (missing.length > 0) { throw new Error(`${ErrorTypes.MALFORMED_SESSION} missing: ${missing.join(", ")}.`); } let addr; try { addr = (0, wallet_1.verifyMessage)(message, signature); } catch (_) { } finally { if (addr !== this.address) { try { //EIP-1271 const isValidSignature = yield (0, exports.checkContractWalletSignature)(this, signature, provider); if (!isValidSignature) { throw new Error(`${ErrorTypes.INVALID_SIGNATURE}: ${addr} !== ${this.address}`); } } catch (e) { throw e; } } } const parsedMessage = new SiweMessage(message); if (parsedMessage.expirationTime) { const exp = new Date(parsedMessage.expirationTime).getTime(); if (isNaN(exp)) { throw new Error(`${ErrorTypes.MALFORMED_SESSION} invalid expiration date.`); } if (new Date().getTime() >= exp) { throw new Error(ErrorTypes.EXPIRED_MESSAGE); } } resolve(parsedMessage); } catch (e) { reject(e); } })); }); } } exports.SiweMessage = SiweMessage; /** * This method calls the EIP-1271 method for Smart Contract wallets * @param message The EIP-4361 parsed message * @param provider Web3 provider able to perform a contract check (Web3/EthersJS). * @returns {Promise<boolean>} Checks for the smart contract (if it exists) if * the signature is valid for given address. */ const checkContractWalletSignature = (message, signature, provider) => __awaiter(void 0, void 0, void 0, function* () { if (!provider) { return false; } const abi = [ "function isValidSignature(bytes32 _message, bytes _signature) public view returns (bool)", ]; try { const walletContract = new contracts_1.Contract(message.address, abi, provider); const hashMessage = (0, hash_1.hashMessage)(message.signMessage()); return yield walletContract.isValidSignature(hashMessage, signature); } catch (e) { throw e; } }); exports.checkContractWalletSignature = checkContractWalletSignature; /** * This method leverages a native CSPRNG with support for both browser and Node.js * environments in order generate a cryptographically secure nonce for use in the * SiweMessage in order to prevent replay attacks. * * 96 bits has been chosen as a number to sufficiently balance size and security considerations * relative to the lifespan of it's usage. * * @returns cryptographically generated random nonce with 96 bits of entropy encoded with * an alphanumeric character set. */ const generateNonce = () => { return (0, random_1.randomStringForEntropy)(96); }; exports.generateNonce = generateNonce;