/* * JBoss, Home of Professional Open Source. * Copyright 2016 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.wildfly.security.http.impl; import static org.wildfly.security._private.ElytronMessages.log; import java.nio.ByteBuffer; import java.security.DigestException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.wildfly.security.http.HttpConstants; import org.wildfly.security.mechanism.AuthenticationMechanismException; import org.wildfly.security.util.ByteIterator; import org.wildfly.security.util.CodePointIterator; import org.wildfly.security.util._private.Arrays2; /** * A utility responsible for managing nonces. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ class NonceManager { private static final int PREFIX_LENGTH = Integer.BYTES + Long.BYTES; private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1); private final AtomicInteger nonceCounter = new AtomicInteger(); private final Set<String> usedNonces = new HashSet<>(); private final byte[] privateKey; private final long validityPeriodNano; private final boolean singleUse; private final String algorithm; /** * @param validityPeriod the time in ms that nonces are valid for in ms. * @param singleUse are nonces single use? * @param keySize the number of bytes to use in the private key of this node. * @param algorithm the message digest algorithm to use when creating the digest portion of the nonce. */ NonceManager(long validityPeriod, boolean singleUse, int keySize, String algorithm) { this.validityPeriodNano = validityPeriod * 1000000; this.singleUse = singleUse; this.algorithm = algorithm; this.privateKey = new byte[keySize]; new SecureRandom().nextBytes(privateKey); } /** * Generate a new encoded nonce to send to the client. * * @return a new encoded nonce to send to the client. */ String generateNonce() { return generateNonce(null); } /** * Generate a new encoded nonce to send to the client. * * @param salt additional data to use when creating the overall signature for the nonce. * @return a new encoded nonce to send to the client. */ String generateNonce(byte[] salt) { try { MessageDigest messageDigest = MessageDigest.getInstance(algorithm); ByteBuffer byteBuffer = ByteBuffer.allocate(PREFIX_LENGTH + messageDigest.getDigestLength()); byteBuffer.putInt(nonceCounter.incrementAndGet()); byteBuffer.putLong(System.nanoTime()); byteBuffer.put(digest(byteBuffer.array(), 0, PREFIX_LENGTH, salt, messageDigest)); String nonce = ByteIterator.ofBytes(byteBuffer.array()).base64Encode().drainToString(); if (log.isTraceEnabled()) { String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); log.tracef("New nonce generated %s, using seed %s", nonce, saltString); } return nonce; } catch (GeneralSecurityException e) { throw new IllegalStateException(e); } } private byte[] digest(byte[] prefix, int prefixOffset, int prefixLength, byte[] salt, MessageDigest messageDigest) throws DigestException { messageDigest.update(prefix, prefixOffset, prefixLength); if (salt != null) { messageDigest.update(salt); } return messageDigest.digest(privateKey); } /** * Attempt to use the supplied nonce. * * A nonce might not be usable for a couple of different reasons: - * * <ul> * <li>It was created too far in the past. * <li>Validation of the signature fails. * <li>The nonce has been used previously and re-use is disabled. * </ul> * * @param nonce the nonce supplied by the client. * @return {@code true} if the nonce can be used, {@code false} otherwise. * @throws AuthenticationMechanismException */ boolean useNonce(String nonce) throws AuthenticationMechanismException { return useNonce(nonce, null); } /** * Attempt to use the supplied nonce. * * A nonce might not be usable for a couple of different reasons: - * * <ul> * <li>It was created too far in the past. * <li>Validation of the signature fails. * <li>The nonce has been used previously and re-use is disabled. * </ul> * * @param nonce the nonce supplied by the client. * @param salt additional data to use when creating the overall signature for the nonce. * @return {@code true} if the nonce can be used, {@code false} otherwise. * @throws AuthenticationMechanismException */ boolean useNonce(final String nonce, byte[] salt) throws AuthenticationMechanismException { try { MessageDigest messageDigest = MessageDigest.getInstance(algorithm); ByteIterator byteIterator = CodePointIterator.ofChars(nonce.toCharArray()).base64Decode(); byte[] nonceBytes = byteIterator.drain(); if (nonceBytes.length != PREFIX_LENGTH + messageDigest.getDigestLength()) { throw log.invalidNonceLength(HttpConstants.DIGEST_NAME); } long age = System.nanoTime() - ByteBuffer.wrap(nonceBytes, Integer.BYTES, Long.BYTES).getLong(); if (age < 0 || age > validityPeriodNano) { log.tracef("Nonce %s rejected due to age %d (ns) being less than 0 or greater than the validity period %d (ns)", nonce, age, validityPeriodNano); return false; } if (Arrays2.equals(nonceBytes, PREFIX_LENGTH, digest(nonceBytes, 0, PREFIX_LENGTH, salt, messageDigest)) == false) { if (log.isTraceEnabled()) { String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); log.tracef("Nonce %s rejected due to failed comparison using secret key with seed %s.", nonce, saltString); } return false; } if (singleUse) { synchronized(usedNonces) { boolean used = usedNonces.add(nonce); if (used) { executor.schedule(() -> { synchronized(usedNonces) { usedNonces.remove(nonce); } }, validityPeriodNano - age, TimeUnit.MILLISECONDS); } else { log.tracef("Nonce %s rejected as previously used.", nonce); } return used; } } return true; } catch (GeneralSecurityException e) { throw new IllegalStateException(e); } } }