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}