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); } }