public static String formatForLogging(final CharSequence message, final Object... additionalFieldPairs) {
	final StringBuilder buf = new StringBuilder(300 + message.length());
	buf.append(tlHeader.get());
	buf.append(" message=\"").append(message).append("\"");
	for (int i = 0; i < additionalFieldPairs.length; i += 2) {
		String name = "null";
		if (additionalFieldPairs[i] != null) {
			name = additionalFieldPairs[i].toString();
		}
		final boolean hasValue = i + 1 < additionalFieldPairs.length;
		if (hasValue) {
			append(buf, name, additionalFieldPairs[i + 1]);
		} else {
			// Just log the first item on it's own
			buf.append(name);
			if (logger.isDebugEnabled()) {
				// To cut down on noise, we only log this warning if debug is enabled as it's not a critical error
				logger.warn("Programmatic loggging error, additionalFieldPairs has odd number name='" + name + "'",
						new SqrlException("log debug stack"));
			}
		}
	}
	buf.append(" ").append(tlFooter.get());
	return buf.toString();
} public static void setLoggingField(final LogField logField, final String value) {
	tlDataTable.get().put(logField, value);
} public class SqrlConfigOperations {
	private static final Logger			logger				= LoggerFactory.getLogger(SqrlConfig.class);
	/**
	 * Set automatically but only used if this class is set on {@link SqrlConfig#setSqrlPersistenceFactoryClass(String)}
	 */
	private static SqrlServiceExecutor	sqrlServiceExecutor	= null;

	private enum BackchannelSettingType {
		FULL_URL, FULL_PATH, PARTIAL_PATH
	}

	private final SqrlPersistenceFactory	sqrlPersistenceFactory;
	private final SqrlConfig				config;

	private final Key						aesKey;
	private final BackchannelSettingType	backchannelSettingType;

	private String	subsequentRequestPath;

	/**
	 * Internal use only.
	 *	
	 * @param config
	 *            the SQRL config object
	 */
	SqrlConfigOperations(final SqrlConfig config) {
		this.config = config;

		// SecureRandom
		final SecureRandom secureRandom = config.getSecureRandom();
		if (secureRandom == null) {
			throw new SqrlConfigSettingException("config.getSecureRandom() was null");
		}

		// Add extra entropy to the secureRandom - setSeed ADDs entropy, it does not replace existing entropy
		// To help ensure random numbers are unique per JVM, seed with a UUID
		secureRandom.setSeed(UUID.randomUUID().toString().getBytes());
		secureRandom.setSeed(Runtime.getRuntime().freeMemory()); // Don't use total memory since it is predictable
		for (final File aRootDir : File.listRoots()) {
			secureRandom.setSeed(aRootDir.getFreeSpace()); // Don't use total space since it is predictable
		}

		// if CPS is enabled, then <cpsCancelUri> must be set as well
		if (config.isEnableCps() && SqrlUtil.isBlank(config.getCpsCancelUri())) {
			throw new SqrlConfigSettingException("config cpsCancelUri must be set since CPS is enabled");
		}
		
		// AES key init
		byte[] aesKeyBytes = SqrlConfigHelper.getAESKeyBytes(config);
		aesKey = new SecretKeySpec(aesKeyBytes, 0, aesKeyBytes.length, "AES");

		// backchannelServletPath
		final String backchannelServletPathSetting = config.getBackchannelServletPath();
		backchannelSettingType = validateBackchannelSetting(backchannelServletPathSetting);

		// SQRL persistence factory class name
		final String factoryClassName = config.getSqrlPersistenceFactoryClass();
		if (SqrlUtil.isBlank(factoryClassName)) {
			sqrlPersistenceFactory = new SqrlJpaPersistenceFactory();
		} else {
			try {
				@SuppressWarnings("rawtypes")
				final Class clazz = Class.forName(factoryClassName);
				sqrlPersistenceFactory = (SqrlPersistenceFactory) createInstanceFromNoArgConstructor(clazz,
						"sqrlPersistenceFactory");
			} catch (final Exception e) {
				throw new IllegalArgumentException(
						"Could not create SqrlPersistenceFactory with name '" + factoryClassName + "'", e);
			}
		}
		// register the cleanup task
		final Class<? extends Runnable> cleanUpTaskClass = sqrlPersistenceFactory.getCleanupTaskClass();
		if (cleanUpTaskClass == null) {
			logger.warn("sqrlPersistenceFactory cleanup task was null, no background cleanup job scheduled");
		} else {
			final Runnable cleanupTask = (Runnable) createInstanceFromNoArgConstructor(cleanUpTaskClass,
					"SqrlPersistenceFactory.getCleanupTaskClass()");
			final int intervalMinutes = config.getCleanupTaskExecInMinutes();
			sqrlServiceExecutor.scheduleAtFixedRate(cleanupTask, intervalMinutes, intervalMinutes, TimeUnit.MINUTES);
		}
	}

	private static Object createInstanceFromNoArgConstructor(@SuppressWarnings("rawtypes") final Class clazz,
			final String description) {
		try {
			@SuppressWarnings({ "rawtypes", "unchecked" })
			final Constructor constructor = clazz.getConstructor();
			return constructor.newInstance();
		} catch (final NoSuchMethodException e) {
			throw new SqrlConfigSettingException(
					clazz.getName() + " must have a default no-arg constructor, but none was found", e);
		} catch (final Exception e) {
			throw new SqrlConfigSettingException("Error instantiating " + description + " of " + clazz.getName(), e);
		}
	}

	/**
	 * Poor mans dependency injection. Can't use CDI since we want to support lightweight JEE servers like tomcat
	 *
	 * @param sqrlServiceExecutor
	 */
	public static void setExecutor(final SqrlServiceExecutor sqrlServiceExecutor) {
		SqrlConfigOperations.sqrlServiceExecutor = sqrlServiceExecutor;
	}

	public Key getAESKey() {
		return aesKey;
	}

	/**
	 * Internal use only. Determines backchannel request URL based on the loginPageRequest
	 *
	 * @param loginPageRequest
	 *            the login page request
	 * @return the URI where SQRL client requests should be sent
	 * @throws SqrlException
	 *             if an invalid backchannelSettingType is present
	 */
	public URI buildBackchannelRequestUrl(final HttpServletRequest loginPageRequest) throws SqrlException {
		// No synchronization as worst case is we compute the value a few times
		String backchannelRequestString = null;
		final String requestUrl = loginPageRequest.getRequestURL().toString();
		if (backchannelSettingType == BackchannelSettingType.FULL_PATH) {
			// Chop off the URI, then add our path
			final String baseUrl = requestUrl.substring(0,
					requestUrl.length() - loginPageRequest.getRequestURI().length());
			backchannelRequestString = baseUrl + config.getBackchannelServletPath();
		} else if (backchannelSettingType == BackchannelSettingType.PARTIAL_PATH) {
			// Replace the last path with ours
			String workingCopy = requestUrl;
			if (workingCopy.endsWith(FORWARD_SLASH)) {
				workingCopy = workingCopy.substring(0, workingCopy.length() - 1);
			}
			final int lastIndex = workingCopy.lastIndexOf('/');
			workingCopy = workingCopy.substring(0, lastIndex + 1);
			backchannelRequestString = workingCopy + config.getBackchannelServletPath();
		} else if (backchannelSettingType == BackchannelSettingType.FULL_URL) {
			backchannelRequestString = config.getBackchannelServletPath();
		} else {
			throw new SqrlException("Don't know how to handle BackchannelSettingType: " + backchannelSettingType);
		}
		// Some SQRL clients require a dotted ip, so replace localhost with 127.0.0.1
		if (backchannelRequestString.contains("//localhost/") || backchannelRequestString.contains("//localhost:")) {
			backchannelRequestString = backchannelRequestString.replace("//localhost",
					FORWARD_SLASH_X2_127_0_0_1);
			// Some testers use a localhost.com alias, replace the .com if it's there
			// backchannelRequestString = backchannelRequestString.replace(".com", "");
			// TODO:
		}
		final URI backchannelRequestUrl = changeToSqrlScheme(backchannelRequestString);
		logger.debug("requestUrl={}, backchannelRequestString={},  backchannelRequestUrl={} ", requestUrl,
				backchannelRequestString, backchannelRequestUrl);
		return backchannelRequestUrl;
	}

	/**
	 * Verifies the URL is secure
	 *
	 * @param fullBackChannelUrl
	 * @return
	 */
	private static URI changeToSqrlScheme(final String fullBackChannelUrl) throws SqrlException {
		// Compute the proper protocol
		StringBuilder urlBuf = new StringBuilder(fullBackChannelUrl.length() + 5);
		if (fullBackChannelUrl.startsWith(SCHEME_HTTPS_COLON)) {
			urlBuf.append(fullBackChannelUrl.replace(SCHEME_HTTPS, SCHEME_SQRL));
		} else if (fullBackChannelUrl.startsWith(SCHEME_HTTP_COLON)) {
			// reverse proxy may go unencrypted between SSL termination and the JEE
			// container
			urlBuf.append(fullBackChannelUrl.replace(SCHEME_HTTP, SCHEME_SQRL));
		} else {
			throw new SqrlException(
					"Don't know how to handle protocol of config.getBackChannelUrl(): " + fullBackChannelUrl);
		}
		try {
			return new URI(urlBuf.toString());
		} catch (final URISyntaxException e) {
			throw new SqrlException(e, "Caught URISyntaxException with backchannel baseURL: ", urlBuf.toString());
		}
	}

	/**
	 * Internal use only. Computes the subsequent URI path for the SQRL client
	 *
	 * @param sqrlBackchannelRequest
	 *            the original SQRL client request
	 * @return the URI string to be sent back to the client
	 * @throws SqrlException
	 *             if a URISyntaxException occurs
	 */
	public String getSubsequentRequestPath(final HttpServletRequest sqrlBackchannelRequest) throws SqrlException {
		// No synchronization as worst case is we compute the value a few times
		if (subsequentRequestPath == null) {
			// getBackchannelRequestUrl was called already
			try {
				this.subsequentRequestPath = new URI(sqrlBackchannelRequest.getRequestURL().toString()).getPath();
			} catch (final URISyntaxException e) {
				throw new SqrlException(e, "Caught URISyntaxException with backchannel sqrlBackchannelRequest: ",
						sqrlBackchannelRequest.getRequestURL().toString());
			}
		}
		return this.subsequentRequestPath;
	}

	/**
	 * backchannelServletPath can be: 1) full URL, 2) full path ("/sqrlbc") or 3) partial path ("sqrlbc")
	 */
	private static BackchannelSettingType validateBackchannelSetting(final String backchannelServletPath) {
		BackchannelSettingType type = null;
		if (backchannelServletPath == null) {
			throw new IllegalArgumentException("SqrlConfig object must have backchannelServletPath set", null);
		} else if (backchannelServletPath.startsWith("/")) {
			type = BackchannelSettingType.FULL_PATH;
		} else if (backchannelServletPath.contains(".")) {
			// Must be a full url, validate it
			try {
				new URL(backchannelServletPath).toString(); // toString() = @SuppressWarnings(value = "squid:S1848")
				type = BackchannelSettingType.FULL_URL;
			} catch (final MalformedURLException e) {
				// Try build a specific error message depending on the circumstances
				final StringBuilder buf = new StringBuilder(100);
				buf.append("SqrlConfig backchannelServletPath contained what appeared to be a URL but it ");
				buf.append("couldn't be parsed: ").append(backchannelServletPath);
				throw new IllegalArgumentException(buf.toString(), e);
			}
		} else {
			type = BackchannelSettingType.PARTIAL_PATH;
		}
		logger.info("BackchannelSettingType is " + type + " for backchannelServletPath: " + backchannelServletPath);
		return type;
	}

	public SqrlConfig getSqrlConfig() {
		return config;
	}

	public SqrlPersistenceFactory getSqrlPersistenceFactory() {
		return sqrlPersistenceFactory;
	}
} public interface SqrlPersistence {
	/* ***************** SqrlIdentity *********************/
	/**
	 * Create a new {@link SqrlIdentity} and enable SQRL authentication
	 *
	 * @param sqrlIdk
	 *            the idk of the SQRL identity
	 */
	public void createAndEnableSqrlIdentity(String sqrlIdk);

	/**
	 * Check persistence to see if a user exists with the given sqrlIdk
	 *
	 * @param sqrlIdk
	 *            the SQRL ID to check for
	 * @return true if sqrlIdk exists, false otherwise
	 */
	public boolean doesSqrlIdentityExistByIdk(String sqrlIdk);

	/**
	 * Fetch the sqrl identity for the the given app user cross reference id
	 *
	 * @param appUserXref
	 *            the app user cross reference value to search by
	 * @return the SQRL identity for this app user
	 */
	public SqrlIdentity fetchSqrlIdentityByUserXref(final String appUserXref);

	/**
	 * The user has updated their SQRL ID but this application is still using the old one. The application must lookup
	 * the user by previousSqrlIdk, and replace that SQRL ID with newSqrlIdk
	 *
	 * @param previousSqrlIdk
	 *            the old SQRL ID, which is present in persistence
	 * @param newSqrlIdk
	 *            the new SQRL ID, which should replace previousSqrlIdk in persistence
	 */
	public void updateIdkForSqrlIdentity(String previousSqrlIdk, String newSqrlIdk);

	/**
	 * Invoked when the user chooses to remove SQRL authentication for this site
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which represents the user.
	 * @throws SqrlPersistenceException
	 *             if there was an error accessing the persistence store
	 */
	public void deleteSqrlIdentity(String sqrlIdk);

	/**
	 * Called to assign a native user cross reference to the given SQRL identity object so when SQRL authentication
	 * takes place, the application knows which user has authenticated
	 *
	 * @param sqrlIdentityId
	 *            the SQRL identity to update
	 * @param nativeUserXref
	 *            the applications native user id for this user
	 */
	public void updateNativeUserXref(final long sqrlIdentityId, final String nativeUserXref);

	/**
	 * Indicates that a user was authenticated successfully via SQRL. The webapp must use the given parameters to:
	 * <p>
	 * <ul>
	 * <li>Find the users web session by using nutToken as a correlator
	 * <li>Determine if the sqrlUserId is already associated with an account or not. If not, ask the user if the have an
	 * existing username/password. If not, enroll the user as new
	 * <li>Follow the webapp flow as if the user logged in via username and password. That is, lookup customer data and
	 * display the relevant content
	 * <p>
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which the user authenticated with.
	 * @param correlator
	 *            The correlator ID that was generated when the login page was presented and embedded in the SQRL url
	 * @param dataToStore
	 *            SQRL related data that must be persisted for this user and be retrievable via
	 *            {@link #fetchSqrlIdentityDataItem(String, String)}
	 * @return true if this the first time the user has used this sqrlUserId to visit this site, false otherwise
	 */
	public void userAuthenticatedViaSqrl(String sqrlIdk, String correlator);

	/**
	 * Invoked to determine if SQRL a given flag is set or unset for a user
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which represents the user.
	 * @return true if the flag is currently set, false if unset
	 * @throws SqrlPersistenceException
	 *             if there was an error accessing the persistence store
	 */
	public boolean fetchSqrlFlagForIdentity(String sqrlIdk, SqrlIdentityFlag flagToFetch);

	/**
	 * Invoked when the user chooses to temporarily disable SQRL authentication for this site
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which represents the user.
	 * @param state
	 *            the auth state to set for this SQRL user
	 * @throws SqrlPersistenceException
	 *             if there was an error accessing the persistence store
	 */
	public void setSqrlFlagForIdentity(String sqrlIdk, SqrlIdentityFlag flagToSet, boolean valueToSet);

	/* ***************** SQRL IDENTITY DATA *********************/
	/**
	 * Indicates that we have received user specific data from the SQRL client that needs to be stored for the user;
	 * <b>NOTE<b> this is often the first call made for a new SQRL identity, so if the identity does not currently
	 * exist, it must be created
	 * <p>
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which the user authenticated with.
	 * @param dataToStore
	 *            SQRL related data that must be persisted for this user and be retreivable via
	 *            {@link #fetchSqrlIdentityDataItem(String, String)}
	 * @return true if this the first time the user has used this sqrlUserId to visit this site, false otherwise
	 */
	public void storeSqrlDataForSqrlIdentity(String sqrlIdk, Map<String, String> dataToStore);

	/**
	 * Request to the data store to retrieve user specific SQRL data that was previously stored via
	 * {@link #userAuthenticatedViaSqrl(String, String, Map)}
	 * <p>
	 *
	 * @param sqrlIdk
	 *            the SQRL ID which the user authenticated with.
	 * @param toFetch
	 *            The name of the SQRL data to be fetched. Was the key in the {@link Map} when
	 * @return the data or null if it does not exist
	 */
	public String fetchSqrlIdentityDataItem(String sqrlIdk, String toFetch);

	/* ***************** SQRL USED TOKENS *********************/
	/**
	 * Check persistence to see if this token has already been used
	 *
	 * @param nutTokenString
	 *            the {@link SqrlNutToken0} token in base64url format as received from the client the token sent by the
	 *            SQRL client in the request
	 * @return true if the token was already used, false if not
	 */
	public boolean hasTokenBeenUsed(final String nutTokenString);

	/**
	 * Mark the given token as used in persistence. After this call, any subsequent calls to
	 * {@link #hasTokenBeenUsed(SqrlNutToken0)} must return true for this token until expiryTime. Once the expiryTime has
	 * been reached, persistence cleanup can occur and this token can be deleted from persistence
	 *
	 * @param nutTokenString
	 *            the {@link SqrlNutToken0} token in base64url format as received from the client
	 * @param expiryTime
	 *            the time at which this token can safely be deleted from persistence since it will fail timestamp
	 *            validation
	 */
	public void markTokenAsUsed(final String nutTokenString, final Date expiryTime);

	/**
	 * Fetch a short lived name/value for a given correlator and name
	 *
	 * @param correlator
	 *            correlator to which this data belongs
	 * @param name
	 *            the name of the item to be fetched
	 * @return the value for the correlator and name or null if it does not exist
	 */
	public String fetchTransientAuthData(String correlator, String transientNameServerParrot);

	/* ***************** SqrlCorrelator *********************/

	/**
	 * Create a new correlator instance in the persistence
	 *
	 * @param correlatorString
	 *            the correlator value string
	 * @param expiryTime
	 *            the time at which this correlator expires
	 * @return
	 */
	public SqrlCorrelator createCorrelator(String correlatorString, Date expiryTime);

	/**
	 * Fetch the correlator object for the given string value, or throw an exception if it does not exist
	 *
	 * @param correlator
	 *            the string value to search for
	 * @return the non-null correlator object
	 */
	public SqrlCorrelator fetchSqrlCorrelatorRequired(String correlator);

	/**
	 * Fetch the correlator object for the given string value
	 *
	 * @param correlator
	 *            the string value to search for
	 * @return the correlator object or null if it does not exist
	 */
	public SqrlCorrelator fetchSqrlCorrelator(String correlator);

	/* ***************** TRANSACTION START / STOP *********************/

	/**
	 * Commit all updates since this persistence object was created
	 */
	public void closeCommit();

	/**
	 * Ignore all updates since this persistence object was created
	 */
	public void closeRollback();

	/**
	 * @return true if this persistence object has been closed
	 */
	public boolean isClosed();

	/**
	 * Delete any expired objects in the persistence store
	 */
	public void cleanUpExpiredEntries();

	Map<String, SqrlCorrelator> fetchSqrlCorrelatorsDetached(Set<String> correlatorStringSet);

	/**
	 * Checks for correlators who's status has changed or is {@link SqrlAuthenticationStatus#AUTHENTICATED_BROWSER}. Complete
	 * state is always returned in case the client didn't get the update the first time it was sent
	 *
	 * @param correlatorToCurrentStatusTable
	 *            table of string correlators and their current state as sent by the browser
	 * @return table of correlators and their updated (or complete) states
	 */
	public Map<String, SqrlAuthenticationStatus> fetchSqrlCorrelatorStatusUpdates(
			Map<String, SqrlAuthenticationStatus> correlatorToCurrentStatusTable);

	public void deleteSqrlCorrelator(SqrlCorrelator sqrlCorrelator);

} public enum LogField {
	CHANNEL("channel"),
	COR("cor"), 
	PROCESS("process"),
	CLIENT_IP("clientip"),
	POLL_UUID("polluuid"),
	USER_AGENT("useragent"),
	SQRL_AGENT("sqrlagent"),
	CLIENT_COMMAND("command"),
	PROTOCOL_VERSION("protover"),
	POLL_TRANSPORT("polltransport"),
	POLL_BROWSER_STATE("pollstate"), 
	IDK("idk"),
	CLIENT_PARAM("clientParam"),
	SERVER_PARAM("serverParam"),
	OPT_LIST("optList"),
	;

	private String logFormat;
	private LogField(final String logFormat) {
		this.logFormat = logFormat;
	}
	public String logformat() {
		return logFormat;
	}

	// 
} public abstract class SqrlNutToken0 {
	/**
	 * @return the encrypted, base64 representation of this token
	 */
	public abstract String asEncryptedBase64();

	abstract long getIssuedTimestampMillis();

	/**
	 * @return the InetAddress of device which originated the browser request. This can return null <b>if</b>
	 *         <code>compareSqrlClientInetAddress<code> is overridden
	 */
	abstract InetAddress getBrowserIPAddress();

	/**
	 * Default implementation to compute when this SQRL "nut" token will expire
	 *
	 * @param config
	 *            the SQRL config
	 * @return the time, in millis, when the token will expire
	 */
	public long computeExpiresAt(final SqrlConfig config) {
		final long nutValidityMillis = TimeUnit.SECONDS.toMillis(config.getNutValidityInSeconds());
		return getIssuedTimestampMillis() + nutValidityMillis;
	}

	/**
	 * Default implementation to perform a comparison of the IP address between the browser and the SQRL client
	 * 
	 * @param sqrlClientIpAddress
	 *            the IP address SQRL client which sent the backchannel request
	 * @return Optional.empty if the IP addresses matched, Optional.string if they did not including detail of the
	 *         mismatch for debugging
	 */
	public Optional<String> compareSqrlClientInetAddress(final InetAddress sqrlClientIpAddress, final SqrlConfig config)
			throws SqrlException {
		final InetAddress browserIpAddress = getBrowserIPAddress();
		if (sqrlClientIpAddress.equals(browserIpAddress)) {
			return Optional.empty();
		} else if ((sqrlClientIpAddress instanceof Inet4Address && browserIpAddress instanceof Inet6Address)
				|| (sqrlClientIpAddress instanceof Inet6Address && browserIpAddress instanceof Inet4Address)) {
			// One side saw an IPv4 while the other side saw IPv6, these will never match
			if (sqrlClientIpAddress.isLoopbackAddress() && browserIpAddress.isLoopbackAddress()) {
				return Optional.empty();
			}
			return Optional.of(SqrlUtil.buildString("IP address format mismatch, browserIP=",
					browserIpAddress.getHostAddress(), " sqrlClientIP=", sqrlClientIpAddress.getHostAddress()));
		} else {
			return Optional.of(SqrlUtil.buildString("IP address mismatch, browserIP=",
					browserIpAddress.getHostAddress(), " sqrlClientIP=", sqrlClientIpAddress.getHostAddress()));
		}
	}

	/**
	 * Helper method to convert translate from a byte to an unsigned byte represented by an int
	 */
	static int buildFormatId(final byte unsignedByte) {
		return Byte.toUnsignedInt(unsignedByte);
	}
} public class SqrlNutTokenFactory {
	private static final Logger	logger		= LoggerFactory.getLogger(SqrlNutTokenFactory.class);
	private static int			formatToUse	= -1;

	private SqrlNutTokenFactory() {
		// Factory
	}

	public static SqrlNutToken0 unmarshal(final String nutTokenString, final SqrlConfigOperations configOperations)
			throws SqrlClientRequestProcessingException {
		final byte[] tokenBytes = SqrlUtil.base64UrlDecodeDataFromSqrlClient(nutTokenString);

		int formatIdOfNutToken = -1;
		if (tokenBytes.length == 16) {
			// If the decoded token is 16 bytes, it is the legacy format which does not embed any format info
			formatIdOfNutToken = 0;
		} else {
			// Newer token, extract the format ID to determine how to parse it
			formatIdOfNutToken = SqrlNutToken0.buildFormatId(tokenBytes[0]);
		}
		if (formatIdOfNutToken == -1) {
			throw new SqrlClientRequestProcessingException(SqrlTifFlag.COMMAND_FAILED, null,
					"Could not detemrine format ID of SqrlNutToken=", nutTokenString);
		} else if (formatIdOfNutToken != formatToUse) {
			logger.warn(
					"Unmarshalling SqrlNutToken with format {} which is different than preferred format of {} SqrlNutToken={}",
					formatIdOfNutToken, formatToUse, nutTokenString);
		}

		if(SqrlNutTokenLegacyFormat.FORMAT_ID == formatIdOfNutToken) {
			return new SqrlNutTokenLegacyFormat(configOperations, nutTokenString);
		}else if(SqrlNutToken1SingleBlockFormat.FORMAT_ID == formatIdOfNutToken) {
			return new SqrlNutToken1SingleBlockFormat(configOperations, nutTokenString);
		}else if(SqrlNutToken2Embedded.FORMAT_ID == formatIdOfNutToken) {
			return new SqrlNutToken2Embedded(configOperations, nutTokenString);
		} else {
			throw new SqrlClientRequestProcessingException(SqrlTifFlag.COMMAND_FAILED, null,
					"Cant create SqrlNutToken with formatid=", Integer.toString(formatIdOfNutToken));
		}
	}

	public static SqrlNutToken0 buildNut(final SqrlConfig config, final SqrlConfigOperations configOperations, final URI backchannelUri,
			final InetAddress browserIPAddress) throws SqrlException {
		if(formatToUse < 0) {
			formatToUse = config.getSqrlNutTokenFormat();
		}

		if(formatToUse == SqrlNutTokenLegacyFormat.FORMAT_ID) {
			return new SqrlNutTokenLegacyFormat(browserIPAddress, configOperations, System.currentTimeMillis());
		} else if (formatToUse == SqrlNutToken1SingleBlockFormat.FORMAT_ID) {
			return new SqrlNutToken1SingleBlockFormat(browserIPAddress, configOperations, System.currentTimeMillis());
		} else if (formatToUse == SqrlNutToken2Embedded.FORMAT_ID) {
			// TODO: will have to perform additional refactoring to make use of these fields
			// for now use dummy values so we represent the possible size of the real data (even though we will likely
			// reduce it)
			final String dummyCorrelator = "";
			final String dummyLoginUrl = "1";
			return new SqrlNutToken2Embedded(browserIPAddress, configOperations, System.currentTimeMillis(),
					dummyCorrelator, dummyLoginUrl);
		} else {
			throw new SqrlException("Unknown SqrlNutToken format ID of ", Integer.toString(formatToUse));
		}
	}
} public enum SqrlClientParam {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	// @formatter:off
	/**
	 * cor is not in the SQRL spec and is unique to this library.  Custom params is permitted by the spec
	 */
	cor,
	ver,
	cmd,
	opt,
	;
} public enum SqrlRequestCommand {
	// @formatter:off
	QUERY,
	IDENT,
	DISABLE,
	ENABLE,
	REMOVE,
	// @formatter:on
	;

	/**
	 * The SQRL spec states that opt args should only be processed during certain commands
	 *
	 * @return
	 */
	public boolean shouldProcessOpts() {
		// opt args should only be processed during non-query per the spec
		// we also add remove since it doesn't make sense to process data we are deleting
		return this != QUERY && this != REMOVE;
	}
} public enum SqrlRequestOpt {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	/**
	 * “suk” is the abbreviation for Server Unlock Key. The presence of this flag instructs the SQRL server to return
	 * the stored server unlock key (SUK) associated with whichever identity matches the identity supplied by the SQRL
	 * client. The SQRL specification requires the SQRL server to automatically return the account's matching SUK
	 * whenever it is able to anticipate that the client is likely to require it, such as when the server contains a
	 * previous identity key, or when the account is disabled. However, there are instances where the client may know
	 * that it is going to need the stored SUK from the server, such as when it wishes to remove a non-disabled account.
	 * The client could first disable the account to induce the server to return the SUK, but it's simpler for the
	 * client to request the SUK from the server whenever it wants it. It's also conceivable that future extensions of
	 * this specification will incorporate other instances where the server's stored SUK is required for RescueCode
	 * based authentication.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	suk(false, true, false),

	/**
	 * When present, this option requests the web server to set a flag on this user's account to disable any alternative
	 * non-SQRL authentication capability, such as weaker traditional username and password authentication.
	 *
	 * Users who have become confident of their use of SQRL may ask their client to include this optional request. The
	 * web server should only assume this intention if the option is present in any successful non-query transaction.
	 * Its absence from any successful non-query transaction should immediately reset the flag and the prohibition in
	 * the web server. The web server may, at its option, notice when any change has occurred and explicitly ask the
	 * user to affirm their changed intention.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	sqrlonly(true, false, true),

	/**
	 * When present, this option requests the web server to set a flag on this user's account to disable any alternative
	 * “out of band” change to this user's SQRL identity, such as traditional and weak “what as your favorite pet's
	 * name” non-SQRL identity authentication.
	 *
	 * Users who have become confident of their use of SQRL may ask their client to include this optional request. The
	 * web server should only assume this intention if the option is present in any successful non-query transaction.
	 * Its absence from any successful non-query transaction should immediately reset the flag and the prohibition in
	 * the web server. The web server may, at its option, notice when any change has occurred and explicitly ask the
	 * user to affirm their changed intention.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	hardlock(true, false, true),

	/**
	 * “cps” is the abbreviation for Client Provided Session. The presence of this flag alters the system's final
	 * authentication action. In the traditional case when this flag is NOT present, the web server arranges to refresh
	 * the web browser's page or redirect the web browser to an authenticated (typically, logged-on) page. However, for
	 * increased security the “cps” option may be specified by a SQRL client to request the server to abandon its
	 * pending authentication with the user's browser and instead provide the web URL to the client for subsequent
	 * handling. The client then might present its user with a URL link to click, or it might arrange its own private
	 * communications channel to the users web browser so that the user will be taken to the identity-authenticated web
	 * page. This higher security mode has the advantage of completely defeating any known man-in-the-middle attack
	 * where an attacking third-party might have inserted themselves into the loop. This cuts that party out of the
	 * loop, preventing them from obtaining the user's authentication.
	 *
	 * The server provides the authenticated page URL in its “url={url}” response parameter for the final non-query
	 * command.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	cps(true, false, false),
	
	/**
	 * this is needed to prevent split-authentication attacks from allowing 'query' 
	 * commands to succeed before perhaps intercepting a follow-up 'ident' command whose IPs do not match
	 * 
	 * if the server does not receive the "noiptest" option and the IP do not match, the operation 
	 * will be hard failed with the server's TIF flags having "0x40 Command failed" =SET= and 
	 * "0x04 IPs matched" =RESET=.  Since this is not a client failure, the 
	 * "0x80 Client failure" bit will not be set
	 *
	 * from https://www.grc.com/x/news.exe?cmd=article&group=grc.sqrl&item=18016&utag=
	 */
	noiptest(false, false, false),

	;

	private final boolean nonQueryOnly;
	private final boolean keyOpt;
	/**
	 * Some opts are user preferences and therefore need to be persisted in storage
	 */
	private final boolean persist;

	private SqrlRequestOpt(final boolean nonQueryOnly, final boolean keyOpt, boolean persist) {
		this.nonQueryOnly = nonQueryOnly;
		this.keyOpt = keyOpt;
		if (keyOpt) {
			this.persist = false;
		} else {
			this.persist = persist;
		}
	}

	/**
	 * Per the spec, some flags are to be processed during non-query calls only; account for that here
	 *
	 * @return
	 */
	public boolean isNonQueryOnly() {
		return nonQueryOnly;
	}

	public static Set<SqrlRequestOpt> getKeyOpts() {
		final Set<SqrlRequestOpt> keyOptSet = new HashSet<>();
		for(final SqrlRequestOpt opt : values()) {
			if (opt.keyOpt) {
				keyOptSet.add(opt);
			}
		}
		return keyOptSet;
	}

	/**
	 * Some opts are user preferences and therefore need to be persisted in storage
	 */
	public boolean isPersist() {
		return persist;
	}
} public enum SqrlServerSideKey {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	// @formatter:off
	idk(false),
	pidk(false),
	suk(true),
	vuk(true);

	// @formatter:on
	/**
	 * Whether the given key needs to be associated with a SQRL Identity and persisted by the SQRL server
	 */
	private final boolean needsBePersisted;

	SqrlServerSideKey(final boolean shouldBeStored) {
		this.needsBePersisted = shouldBeStored;
	}

	private static final Map<String, SqrlServerSideKey> LOOKUP_TABLE;
	static {
		final Map<String, SqrlServerSideKey> tempTable = new TreeMap<>();
		for (final SqrlServerSideKey aKey : values()) {
			tempTable.put(aKey.toString(), aKey);
		}
		LOOKUP_TABLE = Collections.unmodifiableMap(tempTable);
	}

	/**
	 * A version of {@code values} which is case-insensitive and returns null when not found instead of throwing an
	 * exception
	 *
	 * @return the key for this name or null
	 */
	public static SqrlServerSideKey valueOfOrNull(final String toFind) {
		return LOOKUP_TABLE.get(toFind.toLowerCase());
	}

	/**
	 * Whether the given key, if sent in a SQRL client request, must be associated with the users SQRL Identity and
	 * persisted
	 *
	 * @return if the key should be persisted
	 */
	public boolean needsToBePersisted() {
		return needsBePersisted;
	}

	/**
	 * Checks to see if a given name from the {@CLIENT} param is a key that needs to be associated with the users SQRL
	 * Identity and persisted
	 *
	 * @param dataName
	 *            name of a name/value pair passed in the {@CLIENT} param
	 * @return true if it represents a key and that key should be persisted
	 */
	public static boolean isRequestDataAKeyThatNeedsToBePersisted(final String dataName) {
		for (final SqrlServerSideKey aKey : SqrlServerSideKey.values()) {
			if (aKey.toString().equals(dataName) && SqrlServerSideKey.valueOf(dataName).needsToBePersisted()) {
				return true;
			}
		}
		return false;
	}
} public enum SqrlSignatureType {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	// @formatter:off
	ids,
	urs,
	;
	// @formatter:on

	public static final Map<SqrlSignatureType, SqrlServerSideKey> getSignatureToKeyParamTable() {
		final Map<SqrlSignatureType, SqrlServerSideKey> map = new ConcurrentHashMap<>();
		map.put(ids, SqrlServerSideKey.idk);
		// Think this is wrong... believe urs is a combo of keys? will circle back when we add urs support
		map.put(urs, SqrlServerSideKey.suk);
		return map;
	}
} public class SqrlClientRequestProcessingException extends SqrlException {
	private static final long serialVersionUID = -7986435707384269525L;
	private final SqrlTifFlag			tifFlagToAdd;

	public SqrlClientRequestProcessingException(final CharSequence... messagePartArray) {
		// The caller does not wish to set an extra tif, so just re-use COMMAND_FAILED which is always added on a
		// failure
		this(SqrlTifFlag.COMMAND_FAILED, null, messagePartArray);
	}

	/**
	 * 
	 * @param tifToAdd
	 */
	public SqrlClientRequestProcessingException(final SqrlTifFlag tifToAdd, final Throwable cause,
			final CharSequence... messagePartArray) {
		super(cause, messagePartArray);
		this.tifFlagToAdd = tifToAdd;
	}

	public SqrlTifFlag getTifToAdd() {
		return tifFlagToAdd;
	}

} public class SqrlException extends Exception {
	private static final long serialVersionUID = -693580346221526789L;

	public SqrlException(final Throwable e, final CharSequence... messagePartArray) {
		super(LoggingUtil.formatForException((Object[]) messagePartArray), e);
	}

	/**
	 * {@inheritDoc}
	 */
	public SqrlException(final CharSequence... messagePartArray) {
		this(null, messagePartArray);
	}

	// TODO: is this used?
	static String buildMessageWithHeader(final CharSequence[] originalArray, final CharSequence header) {
		final int originalArrayLength = originalArray.length;
		final CharSequence[] newArray = new CharSequence[originalArrayLength + 1];
		newArray[0] = header;
		System.arraycopy(originalArray, 0, newArray, 1, originalArrayLength);
		return SqrlUtil.buildString((Object[]) newArray);
	}
} public class SqrlInvalidDataException extends SqrlException {
	private static final long serialVersionUID = 7999271266503909263L;

	/**
	 * {@inheritDoc}
	 */
	public SqrlInvalidDataException(final Throwable e, final CharSequence... messageStringPartArray) {
		super(e, messageStringPartArray);
	}

	/**
	 * {@inheritDoc}
	 */
	public SqrlInvalidDataException(final CharSequence... messageStringPartArray) {
		this(null, messageStringPartArray);
	}
} public class SqrlInvalidRequestException extends SqrlClientRequestProcessingException {
	private static final long serialVersionUID = -1136919442400493773L;

	public SqrlInvalidRequestException(final CharSequence... messagePartArray) {
		this(null, messagePartArray);
	}

	public SqrlInvalidRequestException(final Throwable cause, final CharSequence... messagePartArray) {
		super(SqrlTifFlag.CLIENT_FAILURE, cause, messagePartArray);
	}

} public class SqrlConstants {
	public static final Integer SQRL_VERSION_1 = Integer.valueOf(1);

	/**
	 * The SQRL versions which this library supports, stored in descending order. This is an ugly one liner, but we need
	 * to ensure it is sorted descending and unmodifiable
	 */
	// @formatter:off
	public static final Set<Integer> SUPPORTED_SQRL_VERSIONS = Collections.unmodifiableSet(
			new TreeSet<>(Arrays.asList(new Integer[] {
					SqrlConstants.SQRL_VERSION_1
			})).descendingSet());
	// @formatter:off

	public static final Charset UTF8_CHARSET = StandardCharsets.UTF_8;
	public static final String	SCHEME_HTTP			= "http";
	public static final String	SCHEME_HTTP_COLON	= SCHEME_HTTP + ":";
	public static final String	SCHEME_HTTPS		= "https";
	public static final String	SCHEME_HTTPS_COLON	= SCHEME_HTTPS + ":";
	public static final String	SCHEME_SQRL			= "sqrl";
	public static final String	FORWARD_SLASH		= "/";

	public static final CharSequence	FORWARD_SLASH_X2_127_0_0_1	= "//127.0.0.1";
	public static final int				AES_KEY_LENGTH				= 16;

	// TODO: make enums
	public static final String			TRANSIENT_NAME_SERVER_PARROT		= "lastServerParam";
	public static final String			TRANSIENT_ENTRY_URL					= "entryPointUrl";
	public static final String			TRANSIENT_CPS_NONCE					= "cpsNonce";
	public static final String			ERROR								= "error";

	public static final String	REGEX_BASE64_URL	= "[a-zA-Z0-9_-]+";
	/**
	 * Token being any request param, correlator, nut token, etc
	 */
	public static final int		MAX_SQRL_TOKEN_SIZE	= 30000;

	public static final String SQRL_ATOMOSPHERE_LIB_UPDATER_CLASS = "com.github.sqrlserverjava.atmosphere.AtmosphereClientAuthStateUpdater";

	private SqrlConstants() {
		// Constants class
	}
} public class SqrlSanitize {

	/**
	 * Performs basic security checks for size and character format for all tokens used in this library: nut,
	 * correlator, http params, etc
	 *
	 * @param data
	 *            the data to be examined
	 * @throws SqrlInvalidDataException
	 *             if data validation fails
	 */
	public static void inspectIncomingData(final String data) throws SqrlInvalidDataException {
		if (SqrlUtil.isBlank(data)) {
			return;
		}
		if(data.length() > SqrlConstants.MAX_SQRL_TOKEN_SIZE) {
			throw new SqrlInvalidDataException(
					"Data size of " + data.length() + " exceeded max size of " + SqrlConstants.MAX_SQRL_TOKEN_SIZE);
		} else if (!SqrlUtil.REGEX_PATTERN_REGEX_BASE64_URL.matcher(data).matches()) {
			throw new SqrlInvalidDataException("Data failed base64url validation: '" + data + "'");
		}
	}
} public class SqrlUtil {
	private static final Logger					logger				= LoggerFactory.getLogger(SqrlUtil.class);
	private static final Map<String, String>	cookieDomainCache	= new ConcurrentHashMap<>();
	private static final List<String> LOCALHOST_IP_STRING_LIST = Collections
			.unmodifiableList(Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1"));
	static final Pattern				REGEX_PATTERN_REGEX_BASE64_URL	= Pattern
			.compile(SqrlConstants.REGEX_BASE64_URL);

	private SqrlUtil() {
		// Util class
	}

	/**
	 * Performs the SQRL required base64URL encoding (base64url without padding)
	 *
	 * @param bytes
	 *            the data to be encoded
	 * @return the encoded string
	 */
	public static String sqrlBase64UrlEncode(final byte[] bytes) {
		String encoded = new String(Base64.getUrlEncoder().encode(bytes), SqrlConstants.UTF8_CHARSET);
		while (encoded.endsWith("=")) {
			encoded = encoded.substring(0, encoded.length() - 1);
		}
		return encoded;
	}

	/**
	 * Performs the SQRL required base64URL encoding (base64url without padding)
	 *
	 * @param toEncode
	 *            the string to be encoded
	 * @return the encoded string
	 */
	public static String sqrlBase64UrlEncode(final String toEncode) { // TODO: rename to base64UrlEncode
		return sqrlBase64UrlEncode(toEncode.getBytes(SqrlConstants.UTF8_CHARSET));
	}

	/**
	 * Performs the SQRL required base64URL decoding (base64url without padding)
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded byte array
	 * @throws SqrlException
	 *             if an error occurs during the base64 decode
	 */
	public static byte[] base64UrlDecode(final String toDecodeParam) throws SqrlException {
		try {
			return Base64.getUrlDecoder().decode(toDecodeParam.getBytes());
		} catch (final IllegalArgumentException e) {
			throw new SqrlException(e, "Error base64 decoding: " + toDecodeParam);
		}
	}

	/**
	 * Convenience method which performs the SQRL required base64URL decoding but throws SqrlInvalidRequestException
	 * since the assumption is that the data originated from a SQRL client
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded byte array
	 * @throws SqrlInvalidRequestException
	 *             if an error occurs during the base64 decode
	 */
	public static byte[] base64UrlDecodeDataFromSqrlClient(final String toDecodeParam) throws SqrlInvalidRequestException {
		try {
			return base64UrlDecode(toDecodeParam);
		} catch (final SqrlException e) {
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
	}

	/**
	 * Performs the SQRL required base64URL decoding
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded data as a string using the UTF-8 character set
	 * @throws SqrlException
	 *             if UTF8 is not supported
	 */
	public static String base64UrlDecodeToString(final String toDecode) throws SqrlException {
		return new String(base64UrlDecode(toDecode), SqrlConstants.UTF8_CHARSET);
	}

	/**
	 * Convenience method which performs the SQRL required base64URL decoding but throws SqrlInvalidRequestException
	 * since the assumption is that the data originated from a SQRL client
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded data as a string using the UTF-8 character set
	 * @throws SqrlInvalidRequestException
	 *             if the data was not in base64url format
	 * @throws IllegalStateException
	 *             if UTF8 is not supported
	 */
	public static String base64UrlDecodeDataFromSqrlClientToString(final String toDecode)
			throws SqrlInvalidRequestException {
		try {
			return base64UrlDecodeToString(toDecode);
		} catch (final SqrlException e) {
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
	}



	public static String base64UrlDecodeToStringOrErrorMessage(final String toDecode) {
		try {
			return base64UrlDecodeToString(toDecode);
		} catch (final Exception e) {
			logger.error("Error during url decode, returning error string for " + toDecode, e);
			return "<error during base64url decode>";
		}
	}

	/**
	 * Internal use only. Verifies the ED25519 signature
	 *
	 * @param signatureFromMessage
	 *            the signature data
	 * @param messageBytes
	 *            the message that was signed
	 * @param publicKeyBytes
	 *            the public key to be used for verification
	 * @return true if verification was successful
	 * @throws SqrlException
	 *             if an error occurs during ED25519 operations
	 */
	public static boolean verifyED25519(final byte[] signatureFromMessage, final byte[] messageBytes,
			final byte[] publicKeyBytes) throws SqrlException {
		try {
			final Signature signature = new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
			final EdDSAParameterSpec edDsaSpec = EdDSANamedCurveTable
					.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);

			final PublicKey publicKey = new EdDSAPublicKey(new EdDSAPublicKeySpec(publicKeyBytes, edDsaSpec));
			signature.initVerify(publicKey);

			signature.update(messageBytes);
			return signature.verify(signatureFromMessage);
		} catch (final GeneralSecurityException e) {
			throw new SqrlException(e, "Got exception during EC signature verification");
		}
	}

	/**
	 * Provides the functionality of Apache commons StringUtils.isBlank() without bringing in the dependency
	 *
	 * @param string
	 *            the string to check
	 * @return true if blank, false otherwise
	 */
	public static boolean isBlank(final String string) {
		return string == null || string.trim().length() == 0;
	}

	/**
	 * Provides the functionality of Apache commons StringUtils.isNotBlank() without bringing in the dependency
	 *
	 * @param string
	 *            the string to check
	 * @return true if not blank, false otherwise
	 */
	public static boolean isNotBlank(final String string) {
		return !isBlank(string);
	}

	/**
	 * Internal use only.
	 *
	 * @param ipAddressString
	 *            the ip address to parse
	 * @return the IP address
	 * @throws SqrlException
	 *             if an {@link UnknownHostException} occurs
	 */
	public static InetAddress ipStringToInetAddresss(final String ipAddressString) throws SqrlException {
		if (SqrlUtil.isBlank(ipAddressString)) {
			throw new SqrlException("ipAddressString was null or empty");
		}
		try {
			return InetAddress.getByName(ipAddressString);
		} catch (final UnknownHostException e) {
			throw new SqrlException(e, "Got UnknownHostException for <", ipAddressString, ">");
		}
	}

	/**
	 * Internal use only. Builds a string of name value pairs from the request
	 *
	 * @param servletRequest
	 *            the request
	 * @return a string of the name value pairs that were in the request
	 */
	public static String buildRequestParamList(final HttpServletRequest servletRequest) {
		final Enumeration<String> params = servletRequest.getParameterNames();
		final StringBuilder buf = new StringBuilder(500);
		while (params.hasMoreElements()) {
			final String paramName = params.nextElement();
			buf.append(paramName).append("=").append(servletRequest.getParameter(paramName)).append("  ");
		}
		return buf.toString();
	}

	// TODO: delete - need to decide on cookies first
	public static Cookie createOrUpdateCookie(final HttpServletRequest request, final String cookieDomain,
			final String name, final String value, final int maxAgeInSeconds, final SqrlConfig config) {
		Cookie cookie = findCookie(request, name);
		if (cookie == null) {
			cookie = new Cookie(name, value);
		}
		cookie.setValue(value);
		cookie.setMaxAge(maxAgeInSeconds);
		// secure and httponly are set in applySettingsToCookie
		applySettingsToCookie(cookie, cookieDomain, request, config);
		return cookie;
	}

	private static void applySettingsToCookie(final Cookie cookie, final String cookieDomain,
			final HttpServletRequest request, final SqrlConfig config) {
		if (cookieDomain != null) {
			cookie.setDomain(cookieDomain);
		}
		cookie.setPath(config.getCookiePath());
		cookie.setHttpOnly(false);
		if (request.getScheme().equals(SqrlConstants.SCHEME_HTTPS)) {
			cookie.setSecure(true);
		}
	}

	private static Cookie findCookie(final HttpServletRequest request, final String toFind) {
		if (request.getCookies() != null) {
			for (final Cookie cookie : request.getCookies()) {
				if (toFind.equals(cookie.getName())) {
					return cookie;
				}
			}
		}
		return null;
	}

	public static String findCookieValue(final HttpServletRequest request, final String toFind) {
		final Cookie cookie = findCookie(request, toFind);
		if (cookie == null) {
			return null;
		}
		return cookie.getValue();
	}

	public static void deleteCookies(final HttpServletRequest request, final HttpServletResponse response,
			final SqrlConfig sqrlConfig, final String... cookiesToDelete) {
		final List<String> cookieToDeleteList = Arrays.asList(cookiesToDelete);
		if (request.getCookies() == null) {
			return;
		}
		for (final Cookie cookie : request.getCookies()) {
			if (cookieToDeleteList.contains(cookie.getName())) {
				cookie.setMaxAge(0);
				cookie.setValue("");
				final String cookieDomain = SqrlUtil.computeCookieDomain(request, sqrlConfig);
				applySettingsToCookie(cookie, cookieDomain, request, sqrlConfig);
				response.addCookie(cookie);
			}
		}
	}

	private static final String cookiesToString(final Cookie[] cookieArray) {
		final StringBuilder buf = new StringBuilder(300);
		buf.append("[ ");
		if (cookieArray != null) {
			for (final Cookie cookie : cookieArray) {
				buf.append(cookie.getName()).append("=").append(cookie.getValue()).append(", ");
			}
		}
		return buf.substring(0, buf.length() - 2) + " ]";
	}

	public static String computeCookieDomain(final HttpServletRequest request, final SqrlConfig config) {
		String domain = config.getCookieDomain();
		if (domain == null) {
			final String requestUrl = request.getRequestURL().toString();
			domain = cookieDomainCache.get(requestUrl);
			if (domain == null) {
				// compute the value and store in the cache
				domain = requestUrl.substring(requestUrl.indexOf("//") + 2);
				final int index = domain.indexOf('/');
				if (index > 0) {
					domain = domain.substring(0, index);
				}
				final int portIndex = domain.indexOf(':');
				if (portIndex > -1) {
					domain = domain.substring(0, portIndex);
				}
				cookieDomainCache.put(requestUrl, domain);
			}
			if ("localhost".equals(domain)) {
				return null;
			}
		}
		return domain;
	}

	public static String logEnterServlet(final HttpServletRequest request) {
		if (!logger.isInfoEnabled()) {
			return "";
		}
		final StringBuilder buf = new StringBuilder(300);
		buf.append("In ");
		buf.append(request.getRequestURI()).append(" with params: ");
		buf.append(parameterMapToString(request.getParameterMap())).append(" and cookies: ");
		buf.append(cookiesToString(request.getCookies()));
		return buf.toString();
	}

	public static String parameterMapToString(final Map<String, String[]> parameterMap) {
		final StringBuilder buf = new StringBuilder(400);
		buf.append("<");
		// {c=12850, 38.6=386540,
		for (final Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
			buf.append(entry.getKey()).append(":");
			for (final String aValue : entry.getValue()) {
				buf.append(aValue).append(",");
			}
			buf.append(" ");
		}
		// remove the trailing comma
		if (buf.toString().endsWith(", ")) {
			buf.delete(buf.length() - 2, buf.length());
		}
		buf.append(">");
		return buf.toString();
	}

	/**
	 * Convenience method which concatenates the given objects into a String in an efficient manner using a
	 * {@link StringBuilder} or similar object
	 * 
	 */
	public static String buildString(final Object... objectArray) {
		if (objectArray.length == 0) {
			return "";
		}
		int length = 0;
		for (final Object aObject : objectArray) {
			if (objectArray != null) {
				length += aObject.toString().length();
			} else {
				length += 4;
			}
		}
		final StringBuilder buf = new StringBuilder(length + 1);
		for (final Object aObject : objectArray) {
			if (aObject != null) {
				buf.append(aObject.toString());
			} else {
				buf.append("null");
			}
		}
		return buf.toString();
	}

	public static boolean isClassOnClasspath(final String classToFind) {
		try {
			Class.forName(classToFind);
			return true;
		} catch (final ClassNotFoundException e) {
			return false;
		}
	}

	public static void debugHeaders(final HttpServletRequest servletRequest) {
		if (!logger.isDebugEnabled()) {
			return;
		}
		final StringBuilder buf = new StringBuilder(200);
		final Enumeration<String> iter = servletRequest.getHeaderNames();
		while (iter.hasMoreElements()) {
			final String headerName = iter.nextElement();
			buf.append(headerName).append("=");
			final Enumeration<String> valueIter = servletRequest.getHeaders(headerName);
			boolean another = false;
			while (valueIter.hasMoreElements()) {
				if (another) {
					buf.append(",");
				}
				buf.append(valueIter.nextElement());
				another = true;
			}
			buf.append(" ");
		}
		logger.debug(LoggingUtil.formatForLogging("requestHeaders=[{}]"), buf.toString().trim());
	}

	public static InetAddress findClientIpAddress(final HttpServletRequest servletRequest, final SqrlConfig config)
			throws SqrlException {
		final String ipToParse = findBrowserIpAddressString(servletRequest, config);
		try {
			final InetAddress inetAddress = InetAddress.getByName(ipToParse);
			return inetAddress;
		} catch (final UnknownHostException e) {
			throw new SqrlException(e, "Caught exception trying to determine clients IP address");
		}
	}

	/**
	 * Determines the web browsers IP address by looking at headers (as set in config) or pulling from the
	 * HttpServletRequest
	 */
	public static String findBrowserIpAddressString(final HttpServletRequest servletRequest, final SqrlConfig config) {
		final List<String> headersToCheckList = config.getIpForwardedForHeaderList();
		String ipString = null;
		SqrlUtil.debugHeaders(servletRequest);
		for (final String headerToFind : headersToCheckList) {
			ipString = servletRequest.getHeader(headerToFind);
			if (SqrlUtil.isNotBlank(ipString)) {
				break;
			}
		}
		if (SqrlUtil.isBlank(ipString)) {
			ipString = servletRequest.getRemoteAddr();
		}
		return ipString;
	}

	public static void exceptionIfNull(final String stringData, final String errorDescription) throws SqrlException {
		if (SqrlUtil.isBlank(stringData)) {
			throw new SqrlException(errorDescription);
		}
	}

	public static void exceptionIfNull(final Object data, final String errorDescription) throws SqrlException {
		if (data == null) {
			throw new SqrlException(errorDescription);
		}
	}

	public static boolean isLocalhost(final InetAddress inetAddress) {
		return LOCALHOST_IP_STRING_LIST.contains(inetAddress.toString());
	}
} public class SqrlVersionUtil {
	private static final Logger logger = LoggerFactory.getLogger(SqrlVersionUtil.class);

	private SqrlVersionUtil() {
		// util
	}

	public static Collection<Integer> parseClientVersionString(final String versionStringToParse) {
		// Order of the client set doesn't matter, just parse as the order they were passed
		final List<Integer> clientVersionList = new ArrayList<>();
		// Client can pass version string as 1[,n],[n-m]
		for(String aVersionString : versionStringToParse.split(",")) {
			aVersionString = aVersionString.trim();
			if(aVersionString.contains("-")) {
				// We have a range, extrapolate it
				clientVersionList.addAll(parseVersionRange(aVersionString));
			} else { // Parse as a single version #
				parseToInteger(aVersionString).ifPresent((i) -> clientVersionList.add(i));
			}
		}
		return clientVersionList;
	}

	private static Optional<Integer> parseToInteger(final String intString) {
		try {
			return Optional.of(Integer.parseInt(intString));
		} catch (final NumberFormatException  e) {
			logger.error(formatForLogging("Version parsing failed for '", intString, "'"));
			return Optional.empty();
		}
	}

	/**
	 * Parses a version range string such as "1-3" to a {@code Collection} of {1, 2, 3}
	 *
	 * @param versionRangeString
	 *            a suspected version range string
	 * @return all values that make up the range or an empty {@code Collection} if a parsing error occurred
	 */
	private static Collection<Integer> parseVersionRange(final String versionRangeString) {
		final List<Integer> dataList = new ArrayList<>();
		final String[] partArray = versionRangeString.split("-");
		if (partArray.length != 2) {
			logger.error(formatForLogging("Version range parsing failed for '{}'", versionRangeString));
		} else {
			final Optional<Integer> min = parseToInteger(partArray[0]);
			final Optional<Integer> max = parseToInteger(partArray[1]);
			if (!min.isPresent() || !max.isPresent()) {
				logger.error(formatForLogging("Version range parsing failed for '", versionRangeString, "'"));
				return dataList; // dataList is empty
			}
			final int compared = min.get().compareTo(max.get());
			if (compared == 0) {
				dataList.add(min.get());
			} else if (compared > 0) {
				logger.error(formatForLogging("Version range parsing failed for '", versionRangeString, "'"));
			} else {
				for (int i = min.get(); i < max.get() + 1; i++) {
					dataList.add(i);
				}
			}
		}
		return dataList;
	}

	/**
	 * Compares 2 sets of descending order sorted {@code Integer} sets and finds the highest common version number
	 *
	 * @param serverVersionSet
	 *            a descending order set of SQRL versions that the server supports
	 * @param clientVersionSet
	 *            a collection (ordered or unordered) of SQRL versions that the client supports
	 * @return the highest version in common, or null if none are in common
	 */
	public static Integer findHighestCommonVersion(final Set<Integer> serverVersionSet,
			final Collection<Integer> clientVersionSet) {
		for (final Integer aServerVersion : serverVersionSet) {
			for (final Integer aClientVersion : clientVersionSet) {
				if (aServerVersion.equals(aClientVersion)) {
					return aServerVersion;
				}
			}
		}
		return null;
	}
}
import static com.github.sqrlserverjava.backchannel.LoggingUtil.formatForLogging;
import static com.github.sqrlserverjava.backchannel.LoggingUtil.setLoggingField;
import java.io.BufferedReader;
import java.io.StringReader;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.sqrlserverjava.SqrlConfigOperations;
import com.github.sqrlserverjava.SqrlPersistence;
import com.github.sqrlserverjava.backchannel.LoggingUtil.LogField;
import com.github.sqrlserverjava.backchannel.nut.SqrlNutToken0;
import com.github.sqrlserverjava.backchannel.nut.SqrlNutTokenFactory;
import com.github.sqrlserverjava.enums.SqrlClientParam;
import com.github.sqrlserverjava.enums.SqrlRequestCommand;
import com.github.sqrlserverjava.enums.SqrlRequestOpt;
import com.github.sqrlserverjava.enums.SqrlServerSideKey;
import com.github.sqrlserverjava.enums.SqrlSignatureType;
import com.github.sqrlserverjava.exception.SqrlClientRequestProcessingException;
import com.github.sqrlserverjava.exception.SqrlException;
import com.github.sqrlserverjava.exception.SqrlInvalidDataException;
import com.github.sqrlserverjava.exception.SqrlInvalidRequestException;
import com.github.sqrlserverjava.util.SqrlConstants;
import com.github.sqrlserverjava.util.SqrlSanitize;
import com.github.sqrlserverjava.util.SqrlUtil;
import com.github.sqrlserverjava.util.SqrlVersionUtil;
package com.github.sqrlserverjava.backchannel;






/**
 * Parses a SQRL client request and validates all signatures
 *
 * @author Dave Badia
 *
 */
public class SqrlClientRequest {
	private static final Logger logger = LoggerFactory.getLogger(SqrlClientRequest.class);

	private static final String NUT_EQUALS = "nut=";

	private final Integer				negotiatedSqrlProtocolVersion;
	private final SqrlNutToken0						nut;
	private final SqrlRequestCommand	clientCommand;
	private final Map<SqrlServerSideKey, byte[]>	requestKeyTableRaw			= new ConcurrentHashMap<>();
	private final Map<SqrlServerSideKey, String>	requestKeyTableBase64	= new ConcurrentHashMap<>();
	private final Set<SqrlRequestOpt> optList = new HashSet<>();

	private final HttpServletRequest	servletRequest;
	private final String				clientParam;
	private final String				serverParam;
	private final String				correlator;

	public SqrlClientRequest(final HttpServletRequest servletRequest, final SqrlPersistence persistence,
			final SqrlConfigOperations configOps) throws SqrlClientRequestProcessingException {
		this.servletRequest = servletRequest;
		this.clientParam = getRequiredParameter(servletRequest, "client");
		setLoggingField(LogField.CLIENT_PARAM, clientParam);
		this.serverParam = getRequiredParameter(servletRequest, "server");
		setLoggingField(LogField.SERVER_PARAM, serverParam);
		this.nut = SqrlNutTokenFactory.unmarshal(extractFromSqrlCsvString(serverParam, NUT_EQUALS), configOps);
		final String decoded = SqrlUtil.base64UrlDecodeDataFromSqrlClientToString(clientParam);
		// parse server - not a name value pair, just the query string we gave
		this.correlator = extractFromSqrlCsvString(serverParam, SqrlClientParam.cor.toString());

		// parse client
		final Map<String, String> clientNameValuePairTable = parseLinesToNameValueMap(decoded);
		// clientVersionString format is 1[,n],[n-m]
		final String clientVersionString = clientNameValuePairTable.get(SqrlClientParam.ver.toString());
		final Collection<Integer> clientVersionsSupported = SqrlVersionUtil
				.parseClientVersionString(clientVersionString);
		final Integer commonProtocolVersion = SqrlVersionUtil
				.findHighestCommonVersion(SqrlConstants.SUPPORTED_SQRL_VERSIONS, clientVersionsSupported);
		if (commonProtocolVersion == null) {
			throw new SqrlClientRequestProcessingException("No common SQRL protocol version found");
		}
		// Sanity check; we probably need to take different actions for different versions so account for that here
		if (!SqrlConstants.SQRL_VERSION_1.equals(commonProtocolVersion)) {
			throw new SqrlClientRequestProcessingException(
					"Unable to process SQRL protcol version " + commonProtocolVersion);
		}
		this.negotiatedSqrlProtocolVersion = commonProtocolVersion;

		// parse opt
		final String optListString = clientNameValuePairTable.get(SqrlClientParam.opt.toString());
		if (SqrlUtil.isNotBlank(optListString)) {
			for (final String optString : optListString.split("~")) {
				try {
					final SqrlRequestOpt clientOpt = SqrlRequestOpt.valueOf(optString);
					if(!optList.add(clientOpt)) {
						logger.debug(formatForLogging("Client sent opt ", clientOpt, " more than once in clientParam",
								clientOpt));
					}
				} catch (final IllegalArgumentException e) {
					throw new SqrlInvalidRequestException(e, "Unknown SQRL client option '", optString, "'");
				}
			}
			setLoggingField(LogField.OPT_LIST, optList.toString());
		}

		// parse keys
		for (final Map.Entry<String, String> entry : parseLinesToNameValueMap(decoded).entrySet()) {
			final SqrlServerSideKey keyType = SqrlServerSideKey.valueOfOrNull(entry.getKey());
			if (keyType == null) {
				continue;
			}
			final String keyBase64 = entry.getValue();
			// Sanity check for a client sending the same key more than once
			if (requestKeyTableRaw.containsKey(keyType)) {
				// Uh oh, is the value different?
				if (requestKeyTableBase64.get(keyType).equals(entry.getValue())) {
					logger.info("Client sent the key " + keyType
							+ " multiple times but with the same value; this should be reported as a minor bug to the client author.  clientParam="
							+ clientParam);
				} else {
					throw new SqrlInvalidRequestException("Client sent the key " + keyType
							+ " multiple times in the same request, each with different values: " + clientParam);
				}
			} else {
				// Store the keys in our tables
				final byte[] keyBytes = SqrlUtil.base64UrlDecodeDataFromSqrlClient(entry.getValue());
				requestKeyTableRaw.put(keyType, keyBytes);
				requestKeyTableBase64.put(keyType, keyBase64);
			}
		}
		logger.debug(formatForLogging("keys found in request: {}", requestKeyTableBase64.keySet()));

		// Per the SQRL spec, since the server response is not signed, we must check the value that comes
		// back to ensure it wasn't tampered with
		final String expectedServerValue = persistence.fetchTransientAuthData(correlator,
				SqrlConstants.TRANSIENT_NAME_SERVER_PARROT);
		if (SqrlUtil.isBlank(expectedServerValue)) {
			throw new SqrlInvalidRequestException("Server parrot was not found in persistence");
		}
		if (!expectedServerValue.equals(serverParam)) {
			logger.warn(formatForLogging("Server parrot mismatch: Expected={}, Received={}", expectedServerValue,
					serverParam));
			throw new SqrlInvalidRequestException("Server parrot mismatch, possible tampering");
		}

		// Validate the signatures
		boolean idsFound = false;
		for (final SqrlSignatureType aSignatureType : SqrlSignatureType.values()) {
			final String signatureParamValue = servletRequest.getParameter(aSignatureType.toString());
			if (SqrlUtil.isNotBlank(signatureParamValue)) {
				// Validate the signature
				validateSignature(SqrlSignatureType.getSignatureToKeyParamTable().get(aSignatureType),
						signatureParamValue);
				if (aSignatureType == SqrlSignatureType.ids) {
					idsFound = true;
				}
			}
		}

		// All requests must have the ids signature
		if (!idsFound) {
			throw new SqrlInvalidRequestException(
					"ids was missing in SQRL client request: ", clientNameValuePairTable.toString());
		}

		final String clientCommandString = clientNameValuePairTable.get(SqrlClientParam.cmd.toString());
		try {
			this.clientCommand = SqrlRequestCommand.valueOf(clientCommandString.toUpperCase());
		} catch (final IllegalArgumentException e) {
			// We handle all SQRL v1 verbs, so don't set FUNCTIONS_NOT_SUPPORTED, treat it as an invalid
			// request instead
			throw new SqrlInvalidRequestException("Recevied invalid SQRL command from client: '" ,clientCommandString ,"'");
		}
	}

	/**
	 * The correlator is our only key to determining which user this is, so it's critical we parse this out first
	 */
	public static String parseCorrelatorOnly(final HttpServletRequest servletRequest) throws SqrlException {
		final String serverParam = getRequiredParameter(servletRequest, "server");
		// parse server - not a name value pair, just the query string we gave
		return extractFromSqrlCsvString(serverParam, SqrlClientParam.cor.toString());
	}

	private static String getRequiredParameter(final HttpServletRequest servletRequest, final String requiredParamName)
			throws SqrlInvalidRequestException {
		final String value = servletRequest.getParameter(requiredParamName);
		if (value == null || value.trim().length() == 0) {
			throw new SqrlInvalidRequestException("Missing required parameter " + requiredParamName
					+ ".  Request contained: " + SqrlUtil.buildRequestParamList(servletRequest));
		}
		try {
			SqrlSanitize.inspectIncomingData(value);
		} catch (final SqrlInvalidDataException e) {