blob: c8d19df89298ffe2a8ee50bce7ee70b27b840cc2 [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
32# TODO(b/35793879): Support more categories
33SUPPORTED_CATEGORIES = [
Adam Vartanian3a1143b2017-03-13 15:52:14 +000034 'AlgorithmParameterGenerator',
35 'AlgorithmParameters',
36 'CertificateFactory',
37 'CertPathBuilder',
38 'CertPathValidator',
39 'CertStore',
40 '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"
76 return name
77
Adam Vartanian9ae0b402017-03-08 16:39:31 +000078def get_current_data(f):
79 """Returns a map of the algorithms in the given input.
80
81 The input file-like object must supply a "BEGIN ALGORITHM LIST" line
82 followed by any number of lines of an algorithm category and algorithm name
83 separated by whitespace followed by a "END ALGORITHM LIST" line. The
84 input can supply arbitrary values outside of the BEGIN and END lines, it
85 will be ignored.
86
Adam Vartanian3a1143b2017-03-13 15:52:14 +000087 The returned algorithms will have their names normalized.
Adam Vartanian9ae0b402017-03-08 16:39:31 +000088
89 Raises:
90 EOFError: If either the BEGIN or END sentinel lines are not present.
91 ValueError: If a line between the BEGIN and END sentinel lines is not
92 made up of two identifiers separated by whitespace.
93 """
94 current_data = collections.defaultdict(list)
95
96 saw_begin = False
97 saw_end = False
98 for line in f.readlines():
99 line = line.strip()
100 if not saw_begin:
101 if line.strip() == 'BEGIN ALGORITHM LIST':
102 saw_begin = True
103 continue
104 if line == 'END ALGORITHM LIST':
105 saw_end = True
106 break
107 category, algorithm = line.split()
108 if category not in SUPPORTED_CATEGORIES:
109 continue
Adam Vartanian3a1143b2017-03-13 15:52:14 +0000110 current_data[category].append(normalize_name(algorithm))
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000111
112 if not saw_begin:
113 raise EOFError(
114 'Reached the end of input without encountering the begin sentinel')
115 if not saw_end:
116 raise EOFError(
117 'Reached the end of input without encountering the end sentinel')
118 return dict(current_data)
119
120
121def update_data(prev_data, current_data, api_level, date):
122 """Returns a copy of prev_data, modified to take into account current_data.
123
124 Updates the algorithm support metadata structure by starting with the
125 information in prev_data and updating it to take into account the algorithms
126 listed in current_data. Algorithms not present in current_data will still
127 be present in the return value, but their supported_api_levels may be
128 modified to indicate that they are no longer supported.
129
130 Args:
131 prev_data: The data on algorithm support from the previous API level.
132 current_data: The algorithms supported in the current API level, as a map
133 from algorithm category to list of algorithm names.
134 api_level: An integer representing the current API level.
135 date: A datetime object containing the time of update.
136 """
137 new_data = {'categories': []}
138
139 for category in SUPPORTED_CATEGORIES:
140 prev_category = find_by_name(prev_data['categories'], category)
141 if prev_category is None:
142 prev_category = {'name': category, 'algorithms': []}
143 current_category = (
144 current_data[category] if category in current_data else [])
145 new_category = {'name': category, 'algorithms': []}
146 prev_algorithms = [x['name'] for x in prev_category['algorithms']]
147 alg_union = set(prev_algorithms) | set(current_category)
148 for alg in alg_union:
149 new_algorithm = {'name': alg}
150 new_level = None
151 if alg in current_category and alg in prev_algorithms:
152 # Both old and new have it, just ensure the API level is right
153 prev_alg = find_by_name(prev_category['algorithms'], alg)
154 if prev_alg['supported_api_levels'].endswith('+'):
155 new_level = prev_alg['supported_api_levels']
156 else:
157 new_level = (prev_alg['supported_api_levels']
158 + ',%d+' % api_level)
159 elif alg in prev_algorithms:
160 # Only in the old set, so ensure the API level is marked
161 # as ending
162 prev_alg = find_by_name(prev_category['algorithms'], alg)
163 if prev_alg['supported_api_levels'].endswith('+'):
164 # The algorithm is newly missing, so modify the support
165 # to end at the previous level
166 new_level = prev_alg['supported_api_levels'][:-1]
167 if not new_level.endswith(str(api_level - 1)):
168 new_level += '-%d' % (api_level - 1)
169 else:
170 new_level = prev_alg['supported_api_levels']
171 new_algorithm['deprecated'] = 'true'
172 else:
173 # Only in the new set, so add it
174 new_level = '%d+' % api_level
175 new_algorithm['supported_api_levels'] = new_level
176 new_category['algorithms'].append(new_algorithm)
177 if new_category['algorithms']:
178 new_category['algorithms'] = sort_by_name(
179 new_category['algorithms'])
180 new_data['categories'].append(new_category)
181 new_data['categories'] = sort_by_name(new_data['categories'])
182 new_data['api_level'] = str(api_level)
183 new_data['last_updated'] = date.strftime('%Y-%m-%d %H:%M:%S UTC')
184
185 return new_data
186
187
188def main():
189 parser = argparse.ArgumentParser(description='Update JSON support file')
190 parser.add_argument('--api_level',
191 required=True,
192 type=int,
193 help='The current API level')
194 parser.add_argument('--rewrite_file',
195 action='store_true',
196 help='If specified, rewrite the'
197 ' input file with the result')
198 parser.add_argument('file',
199 help='The JSON file to update')
200 args = parser.parse_args()
201
202 f = open(args.file)
Adam Vartanian6fb68742017-03-13 15:15:59 +0000203 # JSON doesn't allow comments, but we have some header docs in our file,
204 # so strip comments out before parsing
205 stripped_contents = ''
206 for line in f:
207 if not line.strip().startswith('#'):
208 stripped_contents += line
209 prev_data = json.loads(stripped_contents)
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000210 f.close()
211
212 current_data = get_current_data(sys.stdin)
213
214 new_data = update_data(prev_data,
215 current_data,
216 args.api_level,
217 datetime.datetime.utcnow())
218
219 if args.rewrite_file:
220 f = open(args.file, 'w')
Adam Vartanian6fb68742017-03-13 15:15:59 +0000221 f.write('# This file is autogenerated.'
222 ' See libcore/tools/docs/crypto/README for details.\n')
223 json.dump(
224 new_data, f, indent=2, sort_keys=True, separators=(',', ': '))
225 f.close()
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000226 else:
Adam Vartanian6fb68742017-03-13 15:15:59 +0000227 print json.dumps(
228 new_data, indent=2, sort_keys=True, separators=(',', ': '))
Adam Vartanian9ae0b402017-03-08 16:39:31 +0000229
230
231if __name__ == '__main__':
232 main()