1 #!/usr/bin/env python 2 # 3 # Copyright (C) 2017 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 """Updates a JSON data file of supported algorithms. 18 19 Takes input on stdin a list of provided algorithms as produced by 20 ListProviders.java along with a JSON file of the previous set of algorithm 21 support and what the current API level is, and produces an updated JSON 22 record of algorithm support. 23 """ 24 25 import argparse 26 import collections 27 import datetime 28 import json 29 import re 30 import sys 31 32 import crypto_docs 33 34 SUPPORTED_CATEGORIES = [ 35 'AlgorithmParameterGenerator', 36 'AlgorithmParameters', 37 'CertificateFactory', 38 'CertPathBuilder', 39 'CertPathValidator', 40 'CertStore', 41 'Cipher', 42 'KeyAgreement', 43 'KeyFactory', 44 'KeyGenerator', 45 'KeyManagerFactory', 46 'KeyPairGenerator', 47 'KeyStore', 48 'Mac', 49 'MessageDigest', 50 'SecretKeyFactory', 51 'SecureRandom', 52 'Signature', 53 'SSLContext', 54 'SSLEngine.Enabled', 55 'SSLEngine.Supported', 56 'SSLSocket.Enabled', 57 'SSLSocket.Supported', 58 'TrustManagerFactory', 59 ] 60 61 # For these categories, we really want to maintain the casing that was in the 62 # original data, so avoid changing it. 63 CASE_SENSITIVE_CATEGORIES = [ 64 'SSLEngine.Enabled', 65 'SSLEngine.Supported', 66 'SSLSocket.Enabled', 67 'SSLSocket.Supported', 68 ] 69 70 71 find_by_name = crypto_docs.find_by_name 72 73 74 def find_by_normalized_name(seq, name): 75 """Returns the first element in seq with the given normalized name.""" 76 for item in seq: 77 if normalize_name(item['name']) == name: 78 return item 79 return None 80 81 82 def sort_by_name(seq): 83 """Returns a copy of the input sequence sorted by name.""" 84 return sorted(seq, key=lambda x: x['name']) 85 86 87 def normalize_name(name): 88 """Returns a normalized version of the given algorithm name.""" 89 name = name.upper() 90 # BouncyCastle uses X.509 with an alias of X509, Conscrypt does the 91 # reverse. X.509 is the official name of the standard, so use that. 92 if name == "X509": 93 name = "X.509" 94 # PKCS5PADDING and PKCS7PADDING are the same thing (more accurately, PKCS#5 95 # is a special case of PKCS#7), but providers are inconsistent in their 96 # naming. Use PKCS5PADDING because that's what our docs have used 97 # historically. 98 if name.endswith("/PKCS7PADDING"): 99 name = name[:-1 * len("/PKCS7PADDING")] + "/PKCS5PADDING" 100 return name 101 102 103 def fix_name_caps_for_output(name): 104 """Returns a version of the given algorithm name with capitalization fixed.""" 105 # It's important that this must only change the capitalization of the 106 # name, not any of its text, otherwise future runs won't be able to 107 # match this name with the name coming from the device. 108 109 # We current make the following capitalization fixes 110 # DESede (not DESEDE) 111 # FOOwithBAR (not FOOWITHBAR or FOOWithBAR) 112 # Hmac (not HMAC) 113 name = re.sub('WITH', 'with', name, flags=re.I) 114 name = re.sub('DESEDE', 'DESede', name, flags=re.I) 115 name = re.sub('HMAC', 'Hmac', name, flags=re.I) 116 return name 117 118 119 def get_current_data(f): 120 """Returns a map of the algorithms in the given input. 121 122 The input file-like object must supply a "BEGIN ALGORITHM LIST" line 123 followed by any number of lines of an algorithm category and algorithm name 124 separated by whitespace followed by a "END ALGORITHM LIST" line. The 125 input can supply arbitrary values outside of the BEGIN and END lines, it 126 will be ignored. 127 128 The returned algorithms will have their names normalized. 129 130 Returns: 131 A dict of categories to lists of normalized algorithm names and a 132 dict of normalized algorithm names to original algorithm names. 133 134 Raises: 135 EOFError: If either the BEGIN or END sentinel lines are not present. 136 ValueError: If a line between the BEGIN and END sentinel lines is not 137 made up of two identifiers separated by whitespace. 138 """ 139 current_data = collections.defaultdict(list) 140 name_dict = {} 141 142 saw_begin = False 143 saw_end = False 144 for line in f.readlines(): 145 line = line.strip() 146 if not saw_begin: 147 if line.strip() == 'BEGIN ALGORITHM LIST': 148 saw_begin = True 149 continue 150 if line == 'END ALGORITHM LIST': 151 saw_end = True 152 break 153 category, algorithm = line.split() 154 if category not in SUPPORTED_CATEGORIES: 155 continue 156 normalized_name = normalize_name(algorithm) 157 current_data[category].append(normalized_name) 158 name_dict[normalized_name] = algorithm 159 160 if not saw_begin: 161 raise EOFError( 162 'Reached the end of input without encountering the begin sentinel') 163 if not saw_end: 164 raise EOFError( 165 'Reached the end of input without encountering the end sentinel') 166 return dict(current_data), name_dict 167 168 169 def update_data(prev_data, current_data, name_dict, api_level, date): 170 """Returns a copy of prev_data, modified to take into account current_data. 171 172 Updates the algorithm support metadata structure by starting with the 173 information in prev_data and updating it to take into account the algorithms 174 listed in current_data. Algorithms not present in current_data will still 175 be present in the return value, but their supported_api_levels may be 176 modified to indicate that they are no longer supported. 177 178 Args: 179 prev_data: The data on algorithm support from the previous API level. 180 current_data: The algorithms supported in the current API level, as a map 181 from algorithm category to list of algorithm names. 182 api_level: An integer representing the current API level. 183 date: A datetime object containing the time of update. 184 """ 185 new_data = {'categories': []} 186 187 for category in SUPPORTED_CATEGORIES: 188 prev_category = find_by_name(prev_data['categories'], category) 189 if prev_category is None: 190 prev_category = {'name': category, 'algorithms': []} 191 current_category = ( 192 current_data[category] if category in current_data else []) 193 new_category = {'name': category, 'algorithms': []} 194 prev_algorithms = [normalize_name(x['name']) for x in prev_category['algorithms']] 195 alg_union = set(prev_algorithms) | set(current_category) 196 for alg in alg_union: 197 prev_alg = find_by_normalized_name(prev_category['algorithms'], alg) 198 if alg in name_dict: 199 new_algorithm = {'name': name_dict[alg]} 200 elif prev_alg is not None: 201 new_algorithm = {'name': prev_alg['name']} 202 else: 203 new_algorithm = {'name': alg} 204 if category not in CASE_SENSITIVE_CATEGORIES: 205 new_algorithm['name'] = fix_name_caps_for_output(new_algorithm['name']) 206 new_level = None 207 if alg in current_category and alg in prev_algorithms: 208 # Both old and new have it, just ensure the API level is right 209 if prev_alg['supported_api_levels'].endswith('+'): 210 new_level = prev_alg['supported_api_levels'] 211 else: 212 new_level = (prev_alg['supported_api_levels'] 213 + ',%d+' % api_level) 214 elif alg in prev_algorithms: 215 # Only in the old set, so ensure the API level is marked 216 # as ending 217 if prev_alg['supported_api_levels'].endswith('+'): 218 # The algorithm is newly missing, so modify the support 219 # to end at the previous level 220 new_level = prev_alg['supported_api_levels'][:-1] 221 if not new_level.endswith(str(api_level - 1)): 222 new_level += '-%d' % (api_level - 1) 223 else: 224 new_level = prev_alg['supported_api_levels'] 225 new_algorithm['deprecated'] = 'true' 226 else: 227 # Only in the new set, so add it 228 new_level = '%d+' % api_level 229 new_algorithm['supported_api_levels'] = new_level 230 new_category['algorithms'].append(new_algorithm) 231 if new_category['algorithms']: 232 new_category['algorithms'] = sort_by_name( 233 new_category['algorithms']) 234 new_data['categories'].append(new_category) 235 new_data['categories'] = sort_by_name(new_data['categories']) 236 new_data['api_level'] = str(api_level) 237 new_data['last_updated'] = date.strftime('%Y-%m-%d %H:%M:%S UTC') 238 239 return new_data 240 241 242 def main(): 243 parser = argparse.ArgumentParser(description='Update JSON support file') 244 parser.add_argument('--api_level', 245 required=True, 246 type=int, 247 help='The current API level') 248 parser.add_argument('--rewrite_file', 249 action='store_true', 250 help='If specified, rewrite the' 251 ' input file with the result') 252 parser.add_argument('file', 253 help='The JSON file to update') 254 args = parser.parse_args() 255 256 prev_data = crypto_docs.load_json(args.file) 257 258 current_data, name_dict = get_current_data(sys.stdin) 259 260 new_data = update_data(prev_data, 261 current_data, 262 name_dict, 263 args.api_level, 264 datetime.datetime.utcnow()) 265 266 if args.rewrite_file: 267 f = open(args.file, 'w') 268 f.write('# This file is autogenerated.' 269 ' See libcore/tools/docs/crypto/README for details.\n') 270 json.dump( 271 new_data, f, indent=2, sort_keys=True, separators=(',', ': ')) 272 f.close() 273 else: 274 print json.dumps( 275 new_data, indent=2, sort_keys=True, separators=(',', ': ')) 276 277 278 if __name__ == '__main__': 279 main() 280