validateChallenge method

void validateChallenge(
  1. String challengeTransaction,
  2. String userAccountId,
  3. String? clientDomainAccountId, [
  4. int? timeBoundsGracePeriod,
  5. int? memo,
])

Validates the challenge transaction received from the web auth server.

Implementation

void validateChallenge(String challengeTransaction, String userAccountId,
    String? clientDomainAccountId,
    [int? timeBoundsGracePeriod, int? memo]) {
  XdrTransactionEnvelope envelopeXdr =
      XdrTransactionEnvelope.fromEnvelopeXdrString(challengeTransaction);

  if (envelopeXdr.discriminant != XdrEnvelopeType.ENVELOPE_TYPE_TX) {
    throw ChallengeValidationError(
        "Invalid transaction type received in challenge");
  }

  final transaction = envelopeXdr.v1!.tx;

  if (transaction.seqNum.sequenceNumber.int64 != 0) {
    throw ChallengeValidationErrorInvalidSeqNr(
        "Invalid transaction, sequence number not 0");
  }

  if (transaction.memo.discriminant != XdrMemoType.MEMO_NONE) {
    if (userAccountId.startsWith("M")) {
      throw ChallengeValidationErrorMemoAndMuxedAccount(
          "Memo and muxed account (M...) found");
    } else if (transaction.memo.discriminant != XdrMemoType.MEMO_ID) {
      throw ChallengeValidationErrorInvalidMemoType("invalid memo type");
    } else if (memo != null && transaction.memo.id!.uint64 != memo) {
      throw ChallengeValidationErrorInvalidMemoValue("invalid memo value");
    }
  } else if (memo != null) {
    throw ChallengeValidationErrorInvalidMemoValue("missing memo");
  }

  if (transaction.operations.length == 0) {
    throw ChallengeValidationError("invalid number of operations (0)");
  }

  for (int i = 0; i < transaction.operations.length; i++) {
    final op = transaction.operations[i];
    if (op.sourceAccount == null) {
      throw ChallengeValidationErrorInvalidSourceAccount(
          "invalid source account (is null) in operation[$i]");
    }

    final opSourceAccountId =
        MuxedAccount.fromXdr(op.sourceAccount!).accountId;
    if (i == 0 && opSourceAccountId != userAccountId) {
      throw ChallengeValidationErrorInvalidSourceAccount(
          "invalid source account in operation[$i]");
    }

    // all operations must be manage data operations
    if (op.body.discriminant != XdrOperationType.MANAGE_DATA ||
        op.body.manageDataOp == null) {
      throw ChallengeValidationErrorInvalidOperationType(
          "invalid type of operation $i");
    }

    final dataName = op.body.manageDataOp!.dataName.string64;
    if (i > 0) {
      if (dataName == "client_domain") {
        if (opSourceAccountId != clientDomainAccountId) {
          throw ChallengeValidationErrorInvalidSourceAccount(
              "invalid source account in operation[$i]");
        }
      } else if (opSourceAccountId != _serverSigningKey) {
        throw ChallengeValidationErrorInvalidSourceAccount(
            "invalid source account in operation[$i]");
      }
    }

    if (i == 0 && dataName != _serverHomeDomain + " auth") {
      throw ChallengeValidationErrorInvalidHomeDomain(
          "invalid home domain in operation $i");
    }
    final dataValue = op.body.manageDataOp!.dataValue!.dataValue;
    if (i > 0 && dataName == "web_auth_domain") {
      final uri = Uri.parse(_authEndpoint);
      if (uri.host != String.fromCharCodes(dataValue)) {
        throw ChallengeValidationErrorInvalidWebAuthDomain(
            "invalid web auth domain in operation $i");
      }
    }
  }

  // check timebounds
  final timeBounds = transaction.preconditions.timeBounds;
  if (timeBounds != null) {
    int grace = 0;
    if (timeBoundsGracePeriod != null) {
      grace = timeBoundsGracePeriod;
    }
    final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
    if (currentTime < timeBounds.minTime.uint64 - grace ||
        currentTime > timeBounds.maxTime.uint64 + grace) {
      throw ChallengeValidationErrorInvalidTimeBounds(
          "Invalid transaction, invalid time bounds");
    }
  }

  // the envelope must have one signature and it must be valid: transaction signed by the server
  final signatures = envelopeXdr.v1!.signatures;
  if (signatures.length != 1) {
    throw ChallengeValidationErrorInvalidSignature(
        "Invalid transaction envelope, invalid number of signatures");
  }
  final firstSignature = envelopeXdr.v1!.signatures[0];
  // validate signature
  final serverKeyPair = KeyPair.fromAccountId(_serverSigningKey);
  final transactionHash =
      AbstractTransaction.fromEnvelopeXdr(envelopeXdr).hash(_network);
  final valid = serverKeyPair.verify(
      transactionHash, firstSignature.signature!.signature!);
  if (!valid) {
    throw ChallengeValidationErrorInvalidSignature(
        "Invalid transaction envelope, invalid signature");
  }
}