Home | History | Annotate | Download | only in crypto
      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