@stellar/stellar-sdk
Version:
A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.
320 lines (316 loc) • 10.9 kB
JavaScript
;
var buffer = require('buffer');
var errors = require('./errors.js');
var utils$1 = require('./utils.js');
var utils = require('../utils.js');
var uint8arrayExtras = require('uint8array-extras');
var account = require('../base/account.js');
var transaction_builder = require('../base/transaction_builder.js');
var operation = require('../base/operation.js');
var memo = require('../base/memo.js');
var transaction = require('../base/transaction.js');
var fee_bump_transaction = require('../base/fee_bump_transaction.js');
var keypair = require('../base/keypair.js');
function buildChallengeTx(serverKeypair, clientAccountID, homeDomain, timeout = 300, networkPassphrase, webAuthDomain, memo$1 = null, clientDomain = null, clientSigningKey = null) {
if (clientAccountID.startsWith("M") && memo$1) {
throw Error("memo cannot be used if clientAccountID is a muxed account");
}
const account$1 = new account.Account(serverKeypair.publicKey(), "-1");
const now = Math.floor(Date.now() / 1e3);
const value = uint8arrayExtras.uint8ArrayToBase64(crypto.getRandomValues(new Uint8Array(48)));
const builder = new transaction_builder.TransactionBuilder(account$1, {
fee: transaction_builder.BASE_FEE,
networkPassphrase,
timebounds: {
minTime: now,
maxTime: now + timeout
}
}).addOperation(
operation.Operation.manageData({
name: `${homeDomain} auth`,
value,
source: clientAccountID
})
).addOperation(
operation.Operation.manageData({
name: "web_auth_domain",
value: webAuthDomain,
source: account$1.accountId()
})
);
if (clientDomain) {
if (!clientSigningKey) {
throw Error("clientSigningKey is required if clientDomain is provided");
}
builder.addOperation(
operation.Operation.manageData({
name: `client_domain`,
value: clientDomain,
source: clientSigningKey
})
);
}
if (memo$1) {
builder.addMemo(memo.Memo.id(memo$1));
}
const transaction = builder.build();
transaction.sign(serverKeypair);
return transaction.toEnvelope().toXDR("base64").toString();
}
function readChallengeTx(challengeTx, serverAccountID, networkPassphrase, homeDomains, webAuthDomain) {
if (serverAccountID.startsWith("M")) {
throw Error(
"Invalid serverAccountID: multiplexed accounts are not supported."
);
}
let transaction$1;
try {
transaction$1 = new transaction.Transaction(challengeTx, networkPassphrase);
} catch {
try {
transaction$1 = new fee_bump_transaction.FeeBumpTransaction(challengeTx, networkPassphrase);
} catch {
throw new errors.InvalidChallengeError(
"Invalid challenge: unable to deserialize challengeTx transaction string"
);
}
throw new errors.InvalidChallengeError(
"Invalid challenge: expected a Transaction but received a FeeBumpTransaction"
);
}
const sequence = Number.parseInt(transaction$1.sequence, 10);
if (sequence !== 0) {
throw new errors.InvalidChallengeError(
"The transaction sequence number should be zero"
);
}
if (transaction$1.source !== serverAccountID) {
throw new errors.InvalidChallengeError(
"The transaction source account is not equal to the server's account"
);
}
if (transaction$1.operations.length < 1) {
throw new errors.InvalidChallengeError(
"The transaction should contain at least one operation"
);
}
const [operation, ...subsequentOperations] = transaction$1.operations;
if (!operation.source) {
throw new errors.InvalidChallengeError(
"The transaction's operation should contain a source account"
);
}
const clientAccountID = operation.source;
let memo$1 = null;
if (transaction$1.memo.type !== memo.MemoNone) {
if (clientAccountID.startsWith("M")) {
throw new errors.InvalidChallengeError(
"The transaction has a memo but the client account ID is a muxed account"
);
}
if (transaction$1.memo.type !== memo.MemoID) {
throw new errors.InvalidChallengeError(
"The transaction's memo must be of type `id`"
);
}
memo$1 = transaction$1.memo.value;
}
if (operation.type !== "manageData") {
throw new errors.InvalidChallengeError(
"The transaction's operation type should be 'manageData'"
);
}
if (transaction$1.timeBounds && Number.parseInt(transaction$1.timeBounds?.maxTime, 10) === transaction_builder.TimeoutInfinite) {
throw new errors.InvalidChallengeError(
"The transaction requires non-infinite timebounds"
);
}
if (!utils.Utils.validateTimebounds(transaction$1, 60 * 5)) {
throw new errors.InvalidChallengeError("The transaction has expired");
}
if (operation.value === void 0) {
throw new errors.InvalidChallengeError(
"The transaction's operation values should not be null"
);
}
if (!operation.value) {
throw new errors.InvalidChallengeError(
"The transaction's operation value should not be null"
);
}
if (buffer.Buffer.from(operation.value.toString(), "base64").length !== 48) {
throw new errors.InvalidChallengeError(
"The transaction's operation value should be a 64 bytes base64 random string"
);
}
if (!homeDomains) {
throw new errors.InvalidChallengeError(
"Invalid homeDomains: a home domain must be provided for verification"
);
}
let matchedHomeDomain;
if (typeof homeDomains === "string") {
if (`${homeDomains} auth` === operation.name) {
matchedHomeDomain = homeDomains;
}
} else if (Array.isArray(homeDomains)) {
matchedHomeDomain = homeDomains.find(
(domain) => `${domain} auth` === operation.name
);
} else {
throw new errors.InvalidChallengeError(
`Invalid homeDomains: homeDomains type is ${typeof homeDomains} but should be a string or an array`
);
}
if (!matchedHomeDomain) {
throw new errors.InvalidChallengeError(
"Invalid homeDomains: the transaction's operation key name does not match the expected home domain"
);
}
for (const op of subsequentOperations) {
if (op.type !== "manageData") {
throw new errors.InvalidChallengeError(
"The transaction has operations that are not of type 'manageData'"
);
}
if (op.source !== serverAccountID && op.name !== "client_domain") {
throw new errors.InvalidChallengeError(
"The transaction has operations that are unrecognized"
);
}
if (op.name === "web_auth_domain") {
if (op.value === void 0) {
throw new errors.InvalidChallengeError(
"'web_auth_domain' operation value should not be null"
);
}
if (op.value.compare(buffer.Buffer.from(webAuthDomain))) {
throw new errors.InvalidChallengeError(
`'web_auth_domain' operation value does not match ${webAuthDomain}`
);
}
}
}
if (!utils$1.verifyTxSignedBy(transaction$1, serverAccountID)) {
throw new errors.InvalidChallengeError(
`Transaction not signed by server: '${serverAccountID}'`
);
}
return { tx: transaction$1, clientAccountID, matchedHomeDomain, memo: memo$1 };
}
function verifyChallengeTxSigners(challengeTx, serverAccountID, networkPassphrase, signers, homeDomains, webAuthDomain) {
const { tx } = readChallengeTx(
challengeTx,
serverAccountID,
networkPassphrase,
homeDomains,
webAuthDomain
);
let serverKP;
try {
serverKP = keypair.Keypair.fromPublicKey(serverAccountID);
} catch (err) {
throw new Error(
`Couldn't infer keypair from the provided 'serverAccountID': ${err.message}`
);
}
const clientSigners = /* @__PURE__ */ new Set();
for (const signer of signers) {
if (signer === serverKP.publicKey()) {
continue;
}
if (signer.charAt(0) !== "G") {
continue;
}
clientSigners.add(signer);
}
if (clientSigners.size === 0) {
throw new errors.InvalidChallengeError(
"No verifiable client signers provided, at least one G... address must be provided"
);
}
let clientSigningKey;
for (const op of tx.operations) {
if (op.type === "manageData" && op.name === "client_domain") {
if (clientSigningKey) {
throw new errors.InvalidChallengeError(
"Found more than one client_domain operation"
);
}
clientSigningKey = op.source;
}
}
const allSigners = [
serverKP.publicKey(),
...Array.from(clientSigners)
];
if (clientSigningKey) {
allSigners.push(clientSigningKey);
}
const signersFound = utils$1.gatherTxSigners(tx, allSigners);
let serverSignatureFound = false;
let clientSigningKeySignatureFound = false;
for (const signer of signersFound) {
if (signer === serverKP.publicKey()) {
serverSignatureFound = true;
}
if (signer === clientSigningKey) {
clientSigningKeySignatureFound = true;
}
}
if (!serverSignatureFound) {
throw new errors.InvalidChallengeError(
`Transaction not signed by server: '${serverKP.publicKey()}'`
);
}
if (clientSigningKey && !clientSigningKeySignatureFound) {
throw new errors.InvalidChallengeError(
"Transaction not signed by the source account of the 'client_domain' ManageData operation"
);
}
if (signersFound.length === 1) {
throw new errors.InvalidChallengeError(
"None of the given signers match the transaction signatures"
);
}
if (signersFound.length !== tx.signatures.length) {
throw new errors.InvalidChallengeError("Transaction has unrecognized signatures");
}
signersFound.splice(signersFound.indexOf(serverKP.publicKey()), 1);
if (clientSigningKey) {
signersFound.splice(signersFound.indexOf(clientSigningKey), 1);
}
if (signersFound.length === 0) {
throw new errors.InvalidChallengeError(
"None of the given signers match the transaction signatures"
);
}
return signersFound;
}
function verifyChallengeTxThreshold(challengeTx, serverAccountID, networkPassphrase, threshold, signerSummary, homeDomains, webAuthDomain) {
const signers = signerSummary.map((signer) => signer.key);
const signersFound = verifyChallengeTxSigners(
challengeTx,
serverAccountID,
networkPassphrase,
signers,
homeDomains,
webAuthDomain
);
let weight = 0;
for (const signer of signersFound) {
const sigWeight = signerSummary.find((s) => s.key === signer)?.weight || 0;
weight += sigWeight;
}
if (weight < threshold) {
throw new errors.InvalidChallengeError(
`signers with weight ${weight} do not meet threshold ${threshold}"`
);
}
return signersFound;
}
exports.buildChallengeTx = buildChallengeTx;
exports.readChallengeTx = readChallengeTx;
exports.verifyChallengeTxSigners = verifyChallengeTxSigners;
exports.verifyChallengeTxThreshold = verifyChallengeTxThreshold;
//# sourceMappingURL=challenge_transaction.js.map