lit-siwe
Version:
Sign-In with Ethereum
265 lines (264 loc) • 11.3 kB
JavaScript
;
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;