001/*
002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth;
016
017import static net.openid.appauth.Preconditions.checkArgument;
018import static net.openid.appauth.Preconditions.checkNotNull;
019
020import android.util.Base64;
021
022import net.openid.appauth.internal.Logger;
023
024import java.io.UnsupportedEncodingException;
025import java.security.MessageDigest;
026import java.security.NoSuchAlgorithmException;
027import java.security.SecureRandom;
028import java.util.regex.Pattern;
029
030
031/**
032 * Generates code verifiers and challenges for PKCE exchange.
033 *
034 * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636)
035 * <https://tools.ietf.org/html/rfc7636>"
036 */
037public final class CodeVerifierUtil {
038
039    /**
040     * The minimum permitted length for a code verifier.
041     *
042     * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4.1
043     * <https://tools.ietf.org/html/rfc7636#section-4.1>"
044     */
045    public static final int MIN_CODE_VERIFIER_LENGTH = 43;
046
047    /**
048     * The maximum permitted length for a code verifier.
049     *
050     * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4.1
051     * <https://tools.ietf.org/html/rfc7636#section-4.1>"
052     */
053    public static final int MAX_CODE_VERIFIER_LENGTH = 128;
054
055    /**
056     * The default entropy (in bytes) used for the code verifier.
057     */
058    public static final int DEFAULT_CODE_VERIFIER_ENTROPY = 64;
059
060    /**
061     * The minimum permitted entropy (in bytes) for use with
062     * {@link #generateRandomCodeVerifier(SecureRandom,int)}.
063     */
064    public static final int MIN_CODE_VERIFIER_ENTROPY = 32;
065
066    /**
067     * The maximum permitted entropy (in bytes) for use with
068     * {@link #generateRandomCodeVerifier(SecureRandom,int)}.
069     */
070    public static final int MAX_CODE_VERIFIER_ENTROPY = 96;
071
072    /**
073     * Base64 encoding settings used for generated code verifiers.
074     */
075    private static final int PKCE_BASE64_ENCODE_SETTINGS =
076            Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE;
077
078    /**
079     * Regex for legal code verifier strings, as defined in the spec.
080     *
081     * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4.1
082     * <https://tools.ietf.org/html/rfc7636#section-4.1>"
083     */
084    private static final Pattern REGEX_CODE_VERIFIER =
085            Pattern.compile("^[0-9a-zA-Z\\-\\.\\_\\~]{43,128}$");
086
087
088    private CodeVerifierUtil() {
089        throw new IllegalStateException("This type is not intended to be instantiated");
090    }
091
092    /**
093     * Throws an IllegalArgumentException if the provided code verifier is invalid.
094     *
095     * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4.1
096     * <https://tools.ietf.org/html/rfc7636#section-4.1>"
097     */
098    public static void checkCodeVerifier(String codeVerifier) {
099        checkArgument(MIN_CODE_VERIFIER_LENGTH <= codeVerifier.length(),
100                "codeVerifier length is shorter than allowed by the PKCE specification");
101        checkArgument(codeVerifier.length() <= MAX_CODE_VERIFIER_LENGTH,
102                "codeVerifier length is longer than allowed by the PKCE specification");
103        checkArgument(REGEX_CODE_VERIFIER.matcher(codeVerifier).matches(),
104                "codeVerifier string contains illegal characters");
105    }
106
107    /**
108     * Generates a random code verifier string using {@link SecureRandom} as the source of
109     * entropy, with the default entropy quantity as defined by
110     * {@link #DEFAULT_CODE_VERIFIER_ENTROPY}.
111     */
112    public static String generateRandomCodeVerifier() {
113        return generateRandomCodeVerifier(new SecureRandom(), DEFAULT_CODE_VERIFIER_ENTROPY);
114    }
115
116    /**
117     * Generates a random code verifier string using the provided entropy source and the specified
118     * number of bytes of entropy.
119     */
120    public static String generateRandomCodeVerifier(SecureRandom entropySource, int entropyBytes) {
121        checkNotNull(entropySource, "entropySource cannot be null");
122        checkArgument(MIN_CODE_VERIFIER_ENTROPY <= entropyBytes,
123                "entropyBytes is less than the minimum permitted");
124        checkArgument(entropyBytes <= MAX_CODE_VERIFIER_ENTROPY,
125                "entropyBytes is greater than the maximum permitted");
126        byte[] randomBytes = new byte[entropyBytes];
127        entropySource.nextBytes(randomBytes);
128        return Base64.encodeToString(randomBytes, PKCE_BASE64_ENCODE_SETTINGS);
129    }
130
131    /**
132     * Produces a challenge from a code verifier, using SHA-256 as the challenge method if the
133     * system supports it (all Android devices _should_ support SHA-256), and falls back
134     * to the {@link AuthorizationRequest#CODE_CHALLENGE_METHOD_PLAIN "plain" challenge type} if
135     * unavailable.
136     */
137    public static String deriveCodeVerifierChallenge(String codeVerifier) {
138        try {
139            MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
140            sha256Digester.update(codeVerifier.getBytes("ISO_8859_1"));
141            byte[] digestBytes = sha256Digester.digest();
142            return Base64.encodeToString(digestBytes, PKCE_BASE64_ENCODE_SETTINGS);
143        } catch (NoSuchAlgorithmException e) {
144            Logger.warn("SHA-256 is not supported on this device! Using plain challenge", e);
145            return codeVerifier;
146        } catch (UnsupportedEncodingException e) {
147            Logger.error("ISO-8859-1 encoding not supported on this device!", e);
148            throw new IllegalStateException("ISO-8859-1 encoding not supported", e);
149        }
150    }
151
152    /**
153     * Returns the challenge method utilized on this system: typically
154     * {@link AuthorizationRequest#CODE_CHALLENGE_METHOD_S256 SHA-256} if supported by
155     * the system, {@link AuthorizationRequest#CODE_CHALLENGE_METHOD_PLAIN plain} otherwise.
156     */
157    public static String getCodeVerifierChallengeMethod() {
158        try {
159            MessageDigest.getInstance("SHA-256");
160            // no exception, so SHA-256 is supported
161            return AuthorizationRequest.CODE_CHALLENGE_METHOD_S256;
162        } catch (NoSuchAlgorithmException e) {
163            return AuthorizationRequest.CODE_CHALLENGE_METHOD_PLAIN;
164        }
165    }
166}