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