protected byte[] evaluateMessage(final int state, final byte[] response) throws SaslException {
    switch (state) {
      case ST_CHALLENGE:
        {
          final CodePointIterator cpi = CodePointIterator.ofUtf8Bytes(response);
          final CodePointIterator di = cpi.delimitedBy(0);

          authorizationID = di.hasNext() ? di.drainToString() : null;
          cpi.next(); // Skip delimiter
          userName = di.drainToString();
          validateUserName(userName);
          if ((authorizationID == null) || (authorizationID.isEmpty())) {
            authorizationID = userName;
          }
          validateAuthorizationId(authorizationID);

          // Construct an OTP extended challenge, where:
          // OTP extended challenge = <standard OTP challenge> ext[,<extension set id>[, ...]]
          // standard OTP challenge = otp-<algorithm identifier> <sequence integer> <seed>
          nameCallback = new NameCallback("Remote authentication name", userName);
          final CredentialCallback credentialCallback =
              CredentialCallback.builder()
                  .addSupportedCredentialType(
                      PasswordCredential.class,
                      OneTimePassword.ALGORITHM_OTP_SHA1,
                      OneTimePassword.ALGORITHM_OTP_MD5)
                  .build();

          final TimeoutCallback timeoutCallback = new TimeoutCallback();
          handleCallbacks(nameCallback, credentialCallback, timeoutCallback);
          final PasswordCredential credential =
              (PasswordCredential) credentialCallback.getCredential();
          final OneTimePassword previousPassword = (OneTimePassword) credential.getPassword();
          if (previousPassword == null) {
            throw log.mechUnableToRetrievePassword(getMechanismName(), userName).toSaslException();
          }
          previousAlgorithm = previousPassword.getAlgorithm();
          validateAlgorithm(previousAlgorithm);
          previousSeed = new String(previousPassword.getSeed(), StandardCharsets.US_ASCII);
          validateSeed(previousSeed);
          previousSequenceNumber = previousPassword.getSequenceNumber();
          validateSequenceNumber(previousSequenceNumber);
          previousHash = previousPassword.getHash();

          // Prevent a user from starting multiple simultaneous authentication sessions using the
          // timeout approach described in https://tools.ietf.org/html/rfc2289#section-9.0
          long timeout = timeoutCallback.getTimeout();
          time = Instant.now().getEpochSecond();
          if (time < timeout) {
            // An authentication attempt is already in progress for this user
            throw log.mechMultipleSimultaneousOTPAuthenticationsNotAllowed().toSaslException();
          } else {
            updateTimeout(time + LOCK_TIMEOUT);
            locked = true;
          }

          final ByteStringBuilder challenge = new ByteStringBuilder();
          challenge.append(previousAlgorithm);
          challenge.append(' ');
          challenge.appendNumber(previousSequenceNumber - 1);
          challenge.append(' ');
          challenge.append(previousSeed);
          challenge.append(' ');
          challenge.append(EXT);
          setNegotiationState(ST_PROCESS_RESPONSE);
          return challenge.toArray();
        }
      case ST_PROCESS_RESPONSE:
        {
          if (Instant.now().getEpochSecond() > (time + LOCK_TIMEOUT)) {
            throw log.mechServerTimedOut(getMechanismName()).toSaslException();
          }
          final CodePointIterator cpi = CodePointIterator.ofUtf8Bytes(response);
          final CodePointIterator di = cpi.delimitedBy(':');
          final String responseType = di.drainToString().toLowerCase(Locale.ENGLISH);
          final byte[] currentHash;
          OneTimePasswordSpec passwordSpec;
          String algorithm;
          skipDelims(di, cpi, ':');
          switch (responseType) {
            case HEX_RESPONSE:
            case WORD_RESPONSE:
              {
                if (responseType.equals(HEX_RESPONSE)) {
                  currentHash = convertFromHex(di.drainToString());
                } else {
                  currentHash = convertFromWords(di.drainToString(), previousAlgorithm);
                }
                passwordSpec =
                    new OneTimePasswordSpec(
                        currentHash,
                        previousSeed.getBytes(StandardCharsets.US_ASCII),
                        previousSequenceNumber - 1);
                algorithm = previousAlgorithm;
                break;
              }
            case INIT_HEX_RESPONSE:
            case INIT_WORD_RESPONSE:
              {
                if (responseType.equals(INIT_HEX_RESPONSE)) {
                  currentHash = convertFromHex(di.drainToString());
                } else {
                  currentHash = convertFromWords(di.drainToString(), previousAlgorithm);
                }
                try {
                  // Attempt to parse the new params and new OTP
                  skipDelims(di, cpi, ':');
                  final CodePointIterator si = di.delimitedBy(' ');
                  String newAlgorithm = OTP_PREFIX + si.drainToString();
                  validateAlgorithm(newAlgorithm);
                  skipDelims(si, di, ' ');
                  int newSequenceNumber = Integer.parseInt(si.drainToString());
                  validateSequenceNumber(newSequenceNumber);
                  skipDelims(si, di, ' ');
                  String newSeed = si.drainToString();
                  validateSeed(newSeed);
                  skipDelims(di, cpi, ':');
                  final byte[] newHash;
                  if (responseType.equals(INIT_HEX_RESPONSE)) {
                    newHash = convertFromHex(di.drainToString());
                  } else {
                    newHash = convertFromWords(di.drainToString(), newAlgorithm);
                  }
                  passwordSpec =
                      new OneTimePasswordSpec(
                          newHash, newSeed.getBytes(StandardCharsets.US_ASCII), newSequenceNumber);
                  algorithm = newAlgorithm;
                } catch (SaslException e) {
                  // If the new params or new OTP could not be processed for any reason, the
                  // sequence
                  // number should be decremented if a valid current OTP is provided
                  passwordSpec =
                      new OneTimePasswordSpec(
                          currentHash,
                          previousSeed.getBytes(StandardCharsets.US_ASCII),
                          previousSequenceNumber - 1);
                  algorithm = previousAlgorithm;
                  verifyAndUpdateCredential(currentHash, algorithm, passwordSpec);
                  throw log.mechOTPReinitializationFailed(e).toSaslException();
                }
                break;
              }
            default:
              throw log.mechInvalidOTPResponseType().toSaslException();
          }
          if (cpi.hasNext()) {
            throw log.mechInvalidMessageReceived(getMechanismName()).toSaslException();
          }
          verifyAndUpdateCredential(currentHash, algorithm, passwordSpec);

          // Check the authorization id
          if (authorizationID == null) {
            authorizationID = userName;
          }
          final AuthorizeCallback authorizeCallback =
              new AuthorizeCallback(userName, authorizationID);
          handleCallbacks(authorizeCallback);
          if (!authorizeCallback.isAuthorized()) {
            throw log.mechAuthorizationFailed(getMechanismName(), userName, authorizationID)
                .toSaslException();
          }
          negotiationComplete();
          return null;
        }
      case COMPLETE_STATE:
        {
          if (response != null && response.length != 0) {
            throw log.mechMessageAfterComplete(getMechanismName()).toSaslException();
          }
          return null;
        }
      default:
        throw Assert.impossibleSwitchCase(state);
    }
  }