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 34 public class DigestAuthUtils { 35 private static final String TAG = DigestAuthUtils.class.getSimpleName(); 36 37 public static final String WWW_AUTHENTICATE = "www-authenticate"; 38 private static final String MD5_ALGORITHM = "md5"; 39 private static final int CNONCE_LENGTH_BYTES = 16; 40 private static final String AUTH_QOP = "auth"; 41 parseAuthenticateHeader(String header)42 public static WWWAuthenticate parseAuthenticateHeader(String header) { 43 String reconstitutedHeader = WWW_AUTHENTICATE + ": " + header; 44 WWWAuthenticate parsedHeader; 45 try { 46 return (WWWAuthenticate) (new WWWAuthenticateParser(reconstitutedHeader).parse()); 47 } catch (ParseException e) { 48 Log.e(TAG, "Error parsing received auth header: " + e); 49 return null; 50 } 51 } 52 53 // Generates the Authorization header for use in future requests to the call composer server. generateAuthorizationHeader(WWWAuthenticate parsedHeader, GbaCredentials credentials, String method, String uri)54 public static String generateAuthorizationHeader(WWWAuthenticate parsedHeader, 55 GbaCredentials credentials, String method, String uri) { 56 if (!TextUtils.isEmpty(parsedHeader.getAlgorithm()) 57 && !MD5_ALGORITHM.equals(parsedHeader.getAlgorithm().toLowerCase())) { 58 Log.e(TAG, "This client only supports MD5 auth"); 59 return ""; 60 } 61 if (!TextUtils.isEmpty(parsedHeader.getQop()) 62 && !AUTH_QOP.equals(parsedHeader.getQop().toLowerCase())) { 63 Log.e(TAG, "This client only supports the auth qop"); 64 return ""; 65 } 66 67 String clientNonce = makeClientNonce(); 68 69 String response = computeResponse(parsedHeader.getNonce(), clientNonce, AUTH_QOP, 70 credentials.getTransactionId(), parsedHeader.getRealm(), credentials.getKey(), 71 method, uri); 72 73 Authorization replyHeader = new Authorization(); 74 try { 75 replyHeader.setScheme(parsedHeader.getScheme()); 76 replyHeader.setUsername(credentials.getTransactionId()); 77 replyHeader.setURI(new WorkaroundURI(uri)); 78 replyHeader.setRealm(parsedHeader.getRealm()); 79 replyHeader.setQop(AUTH_QOP); 80 replyHeader.setNonce(parsedHeader.getNonce()); 81 replyHeader.setCNonce(clientNonce); 82 replyHeader.setNonceCount(1); 83 replyHeader.setResponse(response); 84 replyHeader.setOpaque(parsedHeader.getOpaque()); 85 replyHeader.setAlgorithm(parsedHeader.getAlgorithm()); 86 87 } catch (ParseException e) { 88 Log.e(TAG, "Error parsing while constructing reply header: " + e); 89 return null; 90 } 91 92 return replyHeader.encodeBody(); 93 } 94 computeResponse(String serverNonce, String clientNonce, String qop, String username, String realm, byte[] password, String method, String uri)95 public static String computeResponse(String serverNonce, String clientNonce, String qop, 96 String username, String realm, byte[] password, String method, String uri) { 97 String a1Hash = generateA1Hash(username, realm, password); 98 String a2Hash = generateA2Hash(method, uri); 99 100 // this is the nonce-count; since we don't reuse, it's always 1 101 String nonceCount = "00000001"; 102 MessageDigest md5Digest = getMd5Digest(); 103 104 String hashInput = String.join(":", 105 a1Hash, 106 serverNonce, 107 nonceCount, 108 clientNonce, 109 qop, 110 a2Hash); 111 md5Digest.update(hashInput.getBytes()); 112 return base16(md5Digest.digest()); 113 } 114 makeClientNonce()115 private static String makeClientNonce() { 116 SecureRandom rand = new SecureRandom(); 117 byte[] clientNonceBytes = new byte[CNONCE_LENGTH_BYTES]; 118 rand.nextBytes(clientNonceBytes); 119 return base16(clientNonceBytes); 120 } 121 generateA1Hash( String bootstrapTransactionId, String realm, byte[] gbaKey)122 private static String generateA1Hash( 123 String bootstrapTransactionId, String realm, byte[] gbaKey) { 124 MessageDigest md5Digest = getMd5Digest(); 125 126 String gbaKeyBase64 = BaseEncoding.base64().encode(gbaKey); 127 String hashInput = String.join(":", bootstrapTransactionId, realm, gbaKeyBase64); 128 md5Digest.update(hashInput.getBytes()); 129 130 return base16(md5Digest.digest()); 131 } 132 generateA2Hash(String method, String requestUri)133 private static String generateA2Hash(String method, String requestUri) { 134 MessageDigest md5Digest = getMd5Digest(); 135 md5Digest.update(String.join(":", method, requestUri).getBytes()); 136 return base16(md5Digest.digest()); 137 } 138 base16(byte[] input)139 private static String base16(byte[] input) { 140 return BaseEncoding.base16().encode(input).toLowerCase(); 141 } 142 getMd5Digest()143 private static MessageDigest getMd5Digest() { 144 try { 145 return MessageDigest.getInstance("MD5"); 146 } catch (NoSuchAlgorithmException e) { 147 throw new RuntimeException("Couldn't find MD5 algorithm: " + e); 148 } 149 } 150 151 private static class WorkaroundURI extends GenericURI { WorkaroundURI(String uriString)152 public WorkaroundURI(String uriString) { 153 this.uriString = uriString; 154 this.scheme = ""; 155 } 156 } 157 } 158