• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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