blob: 83c4ec89fed7335b46057d6199040845a0918b68 [file] [log] [blame]
Adam Vartanian9ae0b402017-03-08 16:39:31 +00001#!/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
19Takes input on stdin a list of provided algorithms as produced by
20ListProviders.java along with a JSON file of the previous set of algorithm
21support and what the current API level is, and produces an updated JSON
22record of algorithm support.
23"""
24
25import argparse
26import collections
Adam Vartanian9ae0b402017-03-08 16:39:31 +000027import datetime
28import json
Adam Vartanianeb121382017-03-21 15:42:20 +000029import re
Adam Vartanian9ae0b402017-03-08 16:39:31 +000030import sys
31
Adam Vartanianbefc86a2017-03-16 14:26:08 +000032import crypto_docs
33
Adam Vartanian9ae0b402017-03-08 16:39:31 +000034SUPPORTED_CATEGORIES = [
Adam Vartanian3a1143b2017-03-13 15:52:14 +000035 'AlgorithmParameterGenerator',
36 'AlgorithmParameters',
37 'CertificateFactory',
38 'CertPathBuilder',
39 'CertPathValidator',
40 'CertStore',
Adam Vartanian6d9dab52017-03-15 11:56:20 +000041 'Cipher',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000042 'KeyAgreement',
43 'KeyFactory',
44 'KeyGenerator',
45 'KeyManagerFactory',
46 'KeyPairGenerator',
47 'KeyStore',
Adam Vartanian9ae0b402017-03-08 16:39:31 +000048 'Mac',
49 'MessageDigest',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000050 'SecretKeyFactory',
51 'SecureRandom',
Adam Vartanian652e5d12017-03-14 14:19:10 +000052 'Signature',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000053 'SSLContext',
Adam Vartanian22051132017-05-04 12:11:30 +010054 'SSLEngine.Enabled',
55 'SSLEngine.Supported',
56 'SSLSocket.Enabled',
57 'SSLSocket.Supported',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000058 'TrustManagerFactory',
Adam Vartanian9ae0b402017-03-08 16:39:31 +000059]
60
Adam Vartanian22051132017-05-04 12:11:30 +010061# For these categories, we really want to maintain the casing that was in the
62# original data, so avoid changing it.
63CASE_SENSITIVE_CATEGORIES = [
64 'SSLEngine.Enabled',
65 'SSLEngine.Supported',
66 'SSLSocket.Enabled',
67 'SSLSocket.Supported',
68]
Adam Vartanian9ae0b402017-03-08 16:39:31 +000069
Adam Vartanian22051132017-05-04 12:11:30 +010070
71find_by_name = crypto_docs.find_by_name
Adam Vartanian9ae0b402017-03-08 16:39:31 +000072
73
Adam Vartanianeb121382017-03-21 15:42:20 +000074def 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
Adam Vartanian9ae0b402017-03-08 16:39:31 +000082def 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
Adam Vartanian3a1143b2017-03-13 15:52:14 +000087def 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"
Adam Vartanian6d9dab52017-03-15 11:56:20 +000094 # 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"
Adam Vartanian3a1143b2017-03-13 15:52:14 +0000100 return name
101
Adam Vartanianbefc86a2017-03-16 14:26:08 +0000102
Adam Vartanianeb121382017-03-21 15:42:20 +0000103def 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
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000119def 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
Adam Vartanian3a1143b2017-03-13 15:52:14 +0000128 The returned algorithms will have their names normalized.
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000129
Adam Vartanianeb121382017-03-21 15:42:20 +0000130 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
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000134 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)
Adam Vartanianeb121382017-03-21 15:42:20 +0000140 name_dict = {}
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000141
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
Adam Vartanianeb121382017-03-21 15:42:20 +0000156 normalized_name = normalize_name(algorithm)
157 current_data[category].append(normalized_name)
158 name_dict[normalized_name] = algorithm
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000159
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')
Adam Vartanianeb121382017-03-21 15:42:20 +0000166 return dict(current_data), name_dict
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000167
168
Adam Vartanianeb121382017-03-21 15:42:20 +0000169def update_data(prev_data, current_data, name_dict, api_level, date):
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000170 """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': []}
Adam Vartanianeb121382017-03-21 15:42:20 +0000194 prev_algorithms = [normalize_name(x['name']) for x in prev_category['algorithms']]
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000195 alg_union = set(prev_algorithms) | set(current_category)
196 for alg in alg_union:
Adam Vartanianeb121382017-03-21 15:42:20 +0000197 prev_alg = find_by_normalized_name(prev_category['algorithms'], alg)
Adam Vartanian11796722018-02-02 14:09:14 +0000198 if prev_alg is not None:
Adam Vartanianeb121382017-03-21 15:42:20 +0000199 new_algorithm = {'name': prev_alg['name']}
Adam Vartanian11796722018-02-02 14:09:14 +0000200 elif alg in name_dict:
201 new_algorithm = {'name': name_dict[alg]}
Adam Vartanianeb121382017-03-21 15:42:20 +0000202 else:
203 new_algorithm = {'name': alg}
Adam Vartanian22051132017-05-04 12:11:30 +0100204 if category not in CASE_SENSITIVE_CATEGORIES:
205 new_algorithm['name'] = fix_name_caps_for_output(new_algorithm['name'])
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000206 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
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000209 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
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000217 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
Adam Vartanian11796722018-02-02 14:09:14 +0000229 if alg in prev_algorithms and 'note' in prev_alg:
230 new_algorithm['note'] = prev_alg['note']
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000231 new_algorithm['supported_api_levels'] = new_level
232 new_category['algorithms'].append(new_algorithm)
233 if new_category['algorithms']:
234 new_category['algorithms'] = sort_by_name(
235 new_category['algorithms'])
236 new_data['categories'].append(new_category)
237 new_data['categories'] = sort_by_name(new_data['categories'])
238 new_data['api_level'] = str(api_level)
239 new_data['last_updated'] = date.strftime('%Y-%m-%d %H:%M:%S UTC')
240
241 return new_data
242
243
244def main():
245 parser = argparse.ArgumentParser(description='Update JSON support file')
246 parser.add_argument('--api_level',
247 required=True,
248 type=int,
249 help='The current API level')
250 parser.add_argument('--rewrite_file',
251 action='store_true',
252 help='If specified, rewrite the'
253 ' input file with the result')
254 parser.add_argument('file',
255 help='The JSON file to update')
256 args = parser.parse_args()
257
Adam Vartanianbefc86a2017-03-16 14:26:08 +0000258 prev_data = crypto_docs.load_json(args.file)
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000259
Adam Vartanianeb121382017-03-21 15:42:20 +0000260 current_data, name_dict = get_current_data(sys.stdin)
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000261
262 new_data = update_data(prev_data,
263 current_data,
Adam Vartanianeb121382017-03-21 15:42:20 +0000264 name_dict,
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000265 args.api_level,
266 datetime.datetime.utcnow())
267
268 if args.rewrite_file:
269 f = open(args.file, 'w')
Adam Vartanian6fb68742017-03-13 15:15:59 +0000270 f.write('# This file is autogenerated.'
271 ' See libcore/tools/docs/crypto/README for details.\n')
272 json.dump(
273 new_data, f, indent=2, sort_keys=True, separators=(',', ': '))
274 f.close()
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000275 else:
Adam Vartanian6fb68742017-03-13 15:15:59 +0000276 print json.dumps(
277 new_data, indent=2, sort_keys=True, separators=(',', ': '))
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000278
279
280if __name__ == '__main__':
281 main()