| #!/usr/bin/env python |
| # |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Updates a JSON data file of supported algorithms. |
| |
| Takes input on stdin a list of provided algorithms as produced by |
| ListProviders.java along with a JSON file of the previous set of algorithm |
| support and what the current API level is, and produces an updated JSON |
| record of algorithm support. |
| """ |
| |
| import argparse |
| import collections |
| import datetime |
| import json |
| import re |
| import sys |
| |
| import crypto_docs |
| |
| SUPPORTED_CATEGORIES = [ |
| 'AlgorithmParameterGenerator', |
| 'AlgorithmParameters', |
| 'CertificateFactory', |
| 'CertPathBuilder', |
| 'CertPathValidator', |
| 'CertStore', |
| 'Cipher', |
| 'KeyAgreement', |
| 'KeyFactory', |
| 'KeyGenerator', |
| 'KeyManagerFactory', |
| 'KeyPairGenerator', |
| 'KeyStore', |
| 'Mac', |
| 'MessageDigest', |
| 'SecretKeyFactory', |
| 'SecureRandom', |
| 'Signature', |
| 'SSLContext', |
| 'SSLEngine.Enabled', |
| 'SSLEngine.Supported', |
| 'SSLSocket.Enabled', |
| 'SSLSocket.Supported', |
| 'TrustManagerFactory', |
| ] |
| |
| # For these categories, we really want to maintain the casing that was in the |
| # original data, so avoid changing it. |
| CASE_SENSITIVE_CATEGORIES = [ |
| 'SSLEngine.Enabled', |
| 'SSLEngine.Supported', |
| 'SSLSocket.Enabled', |
| 'SSLSocket.Supported', |
| ] |
| |
| |
| find_by_name = crypto_docs.find_by_name |
| |
| |
| def find_by_normalized_name(seq, name): |
| """Returns the first element in seq with the given normalized name.""" |
| for item in seq: |
| if normalize_name(item['name']) == name: |
| return item |
| return None |
| |
| |
| def sort_by_name(seq): |
| """Returns a copy of the input sequence sorted by name.""" |
| return sorted(seq, key=lambda x: x['name']) |
| |
| |
| def normalize_name(name): |
| """Returns a normalized version of the given algorithm name.""" |
| name = name.upper() |
| # BouncyCastle uses X.509 with an alias of X509, Conscrypt does the |
| # reverse. X.509 is the official name of the standard, so use that. |
| if name == "X509": |
| name = "X.509" |
| # PKCS5PADDING and PKCS7PADDING are the same thing (more accurately, PKCS#5 |
| # is a special case of PKCS#7), but providers are inconsistent in their |
| # naming. Use PKCS5PADDING because that's what our docs have used |
| # historically. |
| if name.endswith("/PKCS7PADDING"): |
| name = name[:-1 * len("/PKCS7PADDING")] + "/PKCS5PADDING" |
| return name |
| |
| |
| def fix_name_caps_for_output(name): |
| """Returns a version of the given algorithm name with capitalization fixed.""" |
| # It's important that this must only change the capitalization of the |
| # name, not any of its text, otherwise future runs won't be able to |
| # match this name with the name coming from the device. |
| |
| # We current make the following capitalization fixes |
| # DESede (not DESEDE) |
| # FOOwithBAR (not FOOWITHBAR or FOOWithBAR) |
| # Hmac (not HMAC) |
| name = re.sub('WITH', 'with', name, flags=re.I) |
| name = re.sub('DESEDE', 'DESede', name, flags=re.I) |
| name = re.sub('HMAC', 'Hmac', name, flags=re.I) |
| return name |
| |
| |
| def get_current_data(f): |
| """Returns a map of the algorithms in the given input. |
| |
| The input file-like object must supply a "BEGIN ALGORITHM LIST" line |
| followed by any number of lines of an algorithm category and algorithm name |
| separated by whitespace followed by a "END ALGORITHM LIST" line. The |
| input can supply arbitrary values outside of the BEGIN and END lines, it |
| will be ignored. |
| |
| The returned algorithms will have their names normalized. |
| |
| Returns: |
| A dict of categories to lists of normalized algorithm names and a |
| dict of normalized algorithm names to original algorithm names. |
| |
| Raises: |
| EOFError: If either the BEGIN or END sentinel lines are not present. |
| ValueError: If a line between the BEGIN and END sentinel lines is not |
| made up of two identifiers separated by whitespace. |
| """ |
| current_data = collections.defaultdict(list) |
| name_dict = {} |
| |
| saw_begin = False |
| saw_end = False |
| for line in f.readlines(): |
| line = line.strip() |
| if not saw_begin: |
| if line.strip() == 'BEGIN ALGORITHM LIST': |
| saw_begin = True |
| continue |
| if line == 'END ALGORITHM LIST': |
| saw_end = True |
| break |
| category, algorithm = line.split() |
| if category not in SUPPORTED_CATEGORIES: |
| continue |
| normalized_name = normalize_name(algorithm) |
| current_data[category].append(normalized_name) |
| name_dict[normalized_name] = algorithm |
| |
| if not saw_begin: |
| raise EOFError( |
| 'Reached the end of input without encountering the begin sentinel') |
| if not saw_end: |
| raise EOFError( |
| 'Reached the end of input without encountering the end sentinel') |
| return dict(current_data), name_dict |
| |
| |
| def update_data(prev_data, current_data, name_dict, api_level, date): |
| """Returns a copy of prev_data, modified to take into account current_data. |
| |
| Updates the algorithm support metadata structure by starting with the |
| information in prev_data and updating it to take into account the algorithms |
| listed in current_data. Algorithms not present in current_data will still |
| be present in the return value, but their supported_api_levels may be |
| modified to indicate that they are no longer supported. |
| |
| Args: |
| prev_data: The data on algorithm support from the previous API level. |
| current_data: The algorithms supported in the current API level, as a map |
| from algorithm category to list of algorithm names. |
| api_level: An integer representing the current API level. |
| date: A datetime object containing the time of update. |
| """ |
| new_data = {'categories': []} |
| |
| for category in SUPPORTED_CATEGORIES: |
| prev_category = find_by_name(prev_data['categories'], category) |
| if prev_category is None: |
| prev_category = {'name': category, 'algorithms': []} |
| current_category = ( |
| current_data[category] if category in current_data else []) |
| new_category = {'name': category, 'algorithms': []} |
| prev_algorithms = [normalize_name(x['name']) for x in prev_category['algorithms']] |
| alg_union = set(prev_algorithms) | set(current_category) |
| for alg in alg_union: |
| prev_alg = find_by_normalized_name(prev_category['algorithms'], alg) |
| if prev_alg is not None: |
| new_algorithm = {'name': prev_alg['name']} |
| elif alg in name_dict: |
| new_algorithm = {'name': name_dict[alg]} |
| else: |
| new_algorithm = {'name': alg} |
| if category not in CASE_SENSITIVE_CATEGORIES: |
| new_algorithm['name'] = fix_name_caps_for_output(new_algorithm['name']) |
| new_level = None |
| if alg in current_category and alg in prev_algorithms: |
| # Both old and new have it, just ensure the API level is right |
| if prev_alg['supported_api_levels'].endswith('+'): |
| new_level = prev_alg['supported_api_levels'] |
| else: |
| new_level = (prev_alg['supported_api_levels'] |
| + ',%d+' % api_level) |
| elif alg in prev_algorithms: |
| # Only in the old set, so ensure the API level is marked |
| # as ending |
| if prev_alg['supported_api_levels'].endswith('+'): |
| # The algorithm is newly missing, so modify the support |
| # to end at the previous level |
| new_level = prev_alg['supported_api_levels'][:-1] |
| if not new_level.endswith(str(api_level - 1)): |
| new_level += '-%d' % (api_level - 1) |
| else: |
| new_level = prev_alg['supported_api_levels'] |
| new_algorithm['deprecated'] = 'true' |
| else: |
| # Only in the new set, so add it |
| new_level = '%d+' % api_level |
| if alg in prev_algorithms and 'note' in prev_alg: |
| new_algorithm['note'] = prev_alg['note'] |
| new_algorithm['supported_api_levels'] = new_level |
| new_category['algorithms'].append(new_algorithm) |
| if new_category['algorithms']: |
| new_category['algorithms'] = sort_by_name( |
| new_category['algorithms']) |
| new_data['categories'].append(new_category) |
| new_data['categories'] = sort_by_name(new_data['categories']) |
| new_data['api_level'] = str(api_level) |
| new_data['last_updated'] = date.strftime('%Y-%m-%d %H:%M:%S UTC') |
| |
| return new_data |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description='Update JSON support file') |
| parser.add_argument('--api_level', |
| required=True, |
| type=int, |
| help='The current API level') |
| parser.add_argument('--rewrite_file', |
| action='store_true', |
| help='If specified, rewrite the' |
| ' input file with the result') |
| parser.add_argument('file', |
| help='The JSON file to update') |
| args = parser.parse_args() |
| |
| prev_data = crypto_docs.load_json(args.file) |
| |
| current_data, name_dict = get_current_data(sys.stdin) |
| |
| new_data = update_data(prev_data, |
| current_data, |
| name_dict, |
| args.api_level, |
| datetime.datetime.utcnow()) |
| |
| if args.rewrite_file: |
| f = open(args.file, 'w') |
| f.write('# This file is autogenerated.' |
| ' See libcore/tools/docs/crypto/README for details.\n') |
| json.dump( |
| new_data, f, indent=2, sort_keys=True, separators=(',', ': ')) |
| f.close() |
| else: |
| print json.dumps( |
| new_data, indent=2, sort_keys=True, separators=(',', ': ')) |
| |
| |
| if __name__ == '__main__': |
| main() |