1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.phone.callcomposer; 18 19 import android.text.TextUtils; 20 import android.util.Log; 21 22 import com.google.common.io.BaseEncoding; 23 24 import gov.nist.javax.sip.address.GenericURI; 25 import gov.nist.javax.sip.header.Authorization; 26 import gov.nist.javax.sip.header.WWWAuthenticate; 27 import gov.nist.javax.sip.parser.WWWAuthenticateParser; 28 29 import java.security.MessageDigest; 30 import java.security.NoSuchAlgorithmException; 31 import java.security.SecureRandom; 32 import java.text.ParseException; 33 import java.util.Locale; 34 35 public class DigestAuthUtils { 36 private static final String TAG = DigestAuthUtils.class.getSimpleName(); 37 38 public static final String WWW_AUTHENTICATE = "www-authenticate"; 39 private static final String MD5_ALGORITHM = "md5"; 40 private static final int CNONCE_LENGTH_BYTES = 16; 41 private static final String AUTH_QOP = "auth"; 42 parseAuthenticateHeader(String header)43 public static WWWAuthenticate parseAuthenticateHeader(String header) { 44 String reconstitutedHeader = WWW_AUTHENTICATE + ": " + header; 45 WWWAuthenticate parsedHeader; 46 try { 47 return (WWWAuthenticate) (new WWWAuthenticateParser(reconstitutedHeader).parse()); 48 } catch (ParseException e) { 49 Log.e(TAG, "Error parsing received auth header: " + e); 50 return null; 51 } 52 } 53 54 // Generates the Authorization header for use in future requests to the call composer server. generateAuthorizationHeader(WWWAuthenticate parsedHeader, GbaCredentials credentials, String method, String uri)55 public static String generateAuthorizationHeader(WWWAuthenticate parsedHeader, 56 GbaCredentials credentials, String method, String uri) { 57 if (!TextUtils.isEmpty(parsedHeader.getAlgorithm()) 58 && !MD5_ALGORITHM.equals(parsedHeader.getAlgorithm().toLowerCase(Locale.ROOT))) { 59 Log.e(TAG, "This client only supports MD5 auth"); 60 return ""; 61 } 62 if (!TextUtils.isEmpty(parsedHeader.getQop()) 63 && !AUTH_QOP.equals(parsedHeader.getQop().toLowerCase(Locale.ROOT))) { 64 Log.e(TAG, "This client only supports the auth qop"); 65 return ""; 66 } 67 68 String clientNonce = makeClientNonce(); 69 70 String response = computeResponse(parsedHeader.getNonce(), clientNonce, AUTH_QOP, 71 credentials.getTransactionId(), parsedHeader.getRealm(), credentials.getKey(), 72 method, uri); 73 74 Authorization replyHeader = new Authorization(); 75 try { 76 replyHeader.setScheme(parsedHeader.getScheme()); 77 replyHeader.setUsername(credentials.getTransactionId()); 78 replyHeader.setURI(new WorkaroundURI(uri)); 79 replyHeader.setRealm(parsedHeader.getRealm()); 80 replyHeader.setQop(AUTH_QOP); 81 replyHeader.setNonce(parsedHeader.getNonce()); 82 replyHeader.setCNonce(clientNonce); 83 replyHeader.setNonceCount(1); 84 replyHeader.setResponse(response); 85 replyHeader.setOpaque(parsedHeader.getOpaque()); 86 replyHeader.setAlgorithm(parsedHeader.getAlgorithm()); 87 88 } catch (ParseException e) { 89 Log.e(TAG, "Error parsing while constructing reply header: " + e); 90 return null; 91 } 92 93 return replyHeader.encodeBody(); 94 } 95 computeResponse(String serverNonce, String clientNonce, String qop, String username, String realm, byte[] password, String method, String uri)96 public static String computeResponse(String serverNonce, String clientNonce, String qop, 97 String username, String realm, byte[] password, String method, String uri) { 98 String a1Hash = generateA1Hash(username, realm, password); 99 String a2Hash = generateA2Hash(method, uri); 100 101 // this is the nonce-count; since we don't reuse, it's always 1 102 String nonceCount = "00000001"; 103 MessageDigest md5Digest = getMd5Digest(); 104 105 String hashInput = String.join(":", 106 a1Hash, 107 serverNonce, 108 nonceCount, 109 clientNonce, 110 qop, 111 a2Hash); 112 md5Digest.update(hashInput.getBytes()); 113 return base16(md5Digest.digest()); 114 } 115 makeClientNonce()116 private static String makeClientNonce() { 117 SecureRandom rand = new SecureRandom(); 118 byte[] clientNonceBytes = new byte[CNONCE_LENGTH_BYTES]; 119 rand.nextBytes(clientNonceBytes); 120 return base16(clientNonceBytes); 121 } 122 generateA1Hash( String bootstrapTransactionId, String realm, byte[] gbaKey)123 private static String generateA1Hash( 124 String bootstrapTransactionId, String realm, byte[] gbaKey) { 125 MessageDigest md5Digest = getMd5Digest(); 126 127 String gbaKeyBase64 = BaseEncoding.base64().encode(gbaKey); 128 String hashInput = String.join(":", bootstrapTransactionId, realm, gbaKeyBase64); 129 md5Digest.update(hashInput.getBytes()); 130 131 return base16(md5Digest.digest()); 132 } 133 generateA2Hash(String method, String requestUri)134 private static String generateA2Hash(String method, String requestUri) { 135 MessageDigest md5Digest = getMd5Digest(); 136 md5Digest.update(String.join(":", method, requestUri).getBytes()); 137 return base16(md5Digest.digest()); 138 } 139 base16(byte[] input)140 private static String base16(byte[] input) { 141 return BaseEncoding.base16().encode(input).toLowerCase(Locale.ROOT); 142 } 143 getMd5Digest()144 private static MessageDigest getMd5Digest() { 145 try { 146 return MessageDigest.getInstance("MD5"); 147 } catch (NoSuchAlgorithmException e) { 148 throw new RuntimeException("Couldn't find MD5 algorithm: " + e); 149 } 150 } 151 152 private static class WorkaroundURI extends GenericURI { WorkaroundURI(String uriString)153 public WorkaroundURI(String uriString) { 154 this.uriString = uriString; 155 this.scheme = ""; 156 } 157 } 158 } 159