blob: 7d7861f975e5d8146110530a0f58f69772aa544c [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
27import copy
28import datetime
29import json
30import sys
31
Adam Vartanian9ae0b402017-03-08 16:39:31 +000032SUPPORTED_CATEGORIES = [
Adam Vartanian3a1143b2017-03-13 15:52:14 +000033 'AlgorithmParameterGenerator',
34 'AlgorithmParameters',
35 'CertificateFactory',
36 'CertPathBuilder',
37 'CertPathValidator',
38 'CertStore',
Adam Vartanian6d9dab52017-03-15 11:56:20 +000039 'Cipher',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000040 'KeyAgreement',
41 'KeyFactory',
42 'KeyGenerator',
43 'KeyManagerFactory',
44 'KeyPairGenerator',
45 'KeyStore',
Adam Vartanian9ae0b402017-03-08 16:39:31 +000046 'Mac',
47 'MessageDigest',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000048 'SecretKeyFactory',
49 'SecureRandom',
Adam Vartanian652e5d12017-03-14 14:19:10 +000050 'Signature',
Adam Vartanian3a1143b2017-03-13 15:52:14 +000051 'SSLContext',
52 'TrustManagerFactory',
Adam Vartanian9ae0b402017-03-08 16:39:31 +000053]
54
55
56def find_by_name(seq, name):
57 """Returns the first element in seq with the given name."""
58 for item in seq:
59 if item['name'] == name:
60 return item
61 return None
62
63
64def sort_by_name(seq):
65 """Returns a copy of the input sequence sorted by name."""
66 return sorted(seq, key=lambda x: x['name'])
67
68
Adam Vartanian3a1143b2017-03-13 15:52:14 +000069def normalize_name(name):
70 """Returns a normalized version of the given algorithm name."""
71 name = name.upper()
72 # BouncyCastle uses X.509 with an alias of X509, Conscrypt does the
73 # reverse. X.509 is the official name of the standard, so use that.
74 if name == "X509":
75 name = "X.509"
Adam Vartanian6d9dab52017-03-15 11:56:20 +000076 # PKCS5PADDING and PKCS7PADDING are the same thing (more accurately, PKCS#5
77 # is a special case of PKCS#7), but providers are inconsistent in their
78 # naming. Use PKCS5PADDING because that's what our docs have used
79 # historically.
80 if name.endswith("/PKCS7PADDING"):
81 name = name[:-1 * len("/PKCS7PADDING")] + "/PKCS5PADDING"
Adam Vartanian3a1143b2017-03-13 15:52:14 +000082 return name
83
Adam Vartanian9ae0b402017-03-08 16:39:31 +000084def get_current_data(f):
85 """Returns a map of the algorithms in the given input.
86
87 The input file-like object must supply a "BEGIN ALGORITHM LIST" line
88 followed by any number of lines of an algorithm category and algorithm name
89 separated by whitespace followed by a "END ALGORITHM LIST" line. The
90 input can supply arbitrary values outside of the BEGIN and END lines, it
91 will be ignored.
92
Adam Vartanian3a1143b2017-03-13 15:52:14 +000093 The returned algorithms will have their names normalized.
Adam Vartanian9ae0b402017-03-08 16:39:31 +000094
95 Raises:
96 EOFError: If either the BEGIN or END sentinel lines are not present.
97 ValueError: If a line between the BEGIN and END sentinel lines is not
98 made up of two identifiers separated by whitespace.
99 """
100 current_data = collections.defaultdict(list)
101
102 saw_begin = False
103 saw_end = False
104 for line in f.readlines():
105 line = line.strip()
106 if not saw_begin:
107 if line.strip() == 'BEGIN ALGORITHM LIST':
108 saw_begin = True
109 continue
110 if line == 'END ALGORITHM LIST':
111 saw_end = True
112 break
113 category, algorithm = line.split()
114 if category not in SUPPORTED_CATEGORIES:
115 continue
Adam Vartanian3a1143b2017-03-13 15:52:14 +0000116 current_data[category].append(normalize_name(algorithm))
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000117
118 if not saw_begin:
119 raise EOFError(
120 'Reached the end of input without encountering the begin sentinel')
121 if not saw_end:
122 raise EOFError(
123 'Reached the end of input without encountering the end sentinel')
124 return dict(current_data)
125
126
127def update_data(prev_data, current_data, api_level, date):
128 """Returns a copy of prev_data, modified to take into account current_data.
129
130 Updates the algorithm support metadata structure by starting with the
131 information in prev_data and updating it to take into account the algorithms
132 listed in current_data. Algorithms not present in current_data will still
133 be present in the return value, but their supported_api_levels may be
134 modified to indicate that they are no longer supported.
135
136 Args:
137 prev_data: The data on algorithm support from the previous API level.
138 current_data: The algorithms supported in the current API level, as a map
139 from algorithm category to list of algorithm names.
140 api_level: An integer representing the current API level.
141 date: A datetime object containing the time of update.
142 """
143 new_data = {'categories': []}
144
145 for category in SUPPORTED_CATEGORIES:
146 prev_category = find_by_name(prev_data['categories'], category)
147 if prev_category is None:
148 prev_category = {'name': category, 'algorithms': []}
149 current_category = (
150 current_data[category] if category in current_data else [])
151 new_category = {'name': category, 'algorithms': []}
152 prev_algorithms = [x['name'] for x in prev_category['algorithms']]
153 alg_union = set(prev_algorithms) | set(current_category)
154 for alg in alg_union:
155 new_algorithm = {'name': alg}
156 new_level = None
157 if alg in current_category and alg in prev_algorithms:
158 # Both old and new have it, just ensure the API level is right
159 prev_alg = find_by_name(prev_category['algorithms'], alg)
160 if prev_alg['supported_api_levels'].endswith('+'):
161 new_level = prev_alg['supported_api_levels']
162 else:
163 new_level = (prev_alg['supported_api_levels']
164 + ',%d+' % api_level)
165 elif alg in prev_algorithms:
166 # Only in the old set, so ensure the API level is marked
167 # as ending
168 prev_alg = find_by_name(prev_category['algorithms'], alg)
169 if prev_alg['supported_api_levels'].endswith('+'):
170 # The algorithm is newly missing, so modify the support
171 # to end at the previous level
172 new_level = prev_alg['supported_api_levels'][:-1]
173 if not new_level.endswith(str(api_level - 1)):
174 new_level += '-%d' % (api_level - 1)
175 else:
176 new_level = prev_alg['supported_api_levels']
177 new_algorithm['deprecated'] = 'true'
178 else:
179 # Only in the new set, so add it
180 new_level = '%d+' % api_level
181 new_algorithm['supported_api_levels'] = new_level
182 new_category['algorithms'].append(new_algorithm)
183 if new_category['algorithms']:
184 new_category['algorithms'] = sort_by_name(
185 new_category['algorithms'])
186 new_data['categories'].append(new_category)
187 new_data['categories'] = sort_by_name(new_data['categories'])
188 new_data['api_level'] = str(api_level)
189 new_data['last_updated'] = date.strftime('%Y-%m-%d %H:%M:%S UTC')
190
191 return new_data
192
193
194def main():
195 parser = argparse.ArgumentParser(description='Update JSON support file')
196 parser.add_argument('--api_level',
197 required=True,
198 type=int,
199 help='The current API level')
200 parser.add_argument('--rewrite_file',
201 action='store_true',
202 help='If specified, rewrite the'
203 ' input file with the result')
204 parser.add_argument('file',
205 help='The JSON file to update')
206 args = parser.parse_args()
207
208 f = open(args.file)
Adam Vartanian6fb68742017-03-13 15:15:59 +0000209 # JSON doesn't allow comments, but we have some header docs in our file,
210 # so strip comments out before parsing
211 stripped_contents = ''
212 for line in f:
213 if not line.strip().startswith('#'):
214 stripped_contents += line
215 prev_data = json.loads(stripped_contents)
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000216 f.close()
217
218 current_data = get_current_data(sys.stdin)
219
220 new_data = update_data(prev_data,
221 current_data,
222 args.api_level,
223 datetime.datetime.utcnow())
224
225 if args.rewrite_file:
226 f = open(args.file, 'w')
Adam Vartanian6fb68742017-03-13 15:15:59 +0000227 f.write('# This file is autogenerated.'
228 ' See libcore/tools/docs/crypto/README for details.\n')
229 json.dump(
230 new_data, f, indent=2, sort_keys=True, separators=(',', ': '))
231 f.close()
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000232 else:
Adam Vartanian6fb68742017-03-13 15:15:59 +0000233 print json.dumps(
234 new_data, indent=2, sort_keys=True, separators=(',', ': '))
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000235
236
237if __name__ == '__main__':
238 main()