OLD | NEW |
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "base/crypto/rsa_private_key.h" | 5 #include "base/crypto/rsa_private_key.h" |
6 | 6 |
7 #include <iostream> | 7 #include <iostream> |
8 #include <list> | 8 #include <list> |
9 | 9 |
10 #include "base/logging.h" | 10 #include "base/logging.h" |
11 #include "base/scoped_ptr.h" | 11 #include "base/scoped_ptr.h" |
12 #include "base/string_util.h" | 12 #include "base/string_util.h" |
13 | 13 |
14 | |
15 // This file manually encodes and decodes RSA private keys using PrivateKeyInfo | |
16 // from PKCS #8 and RSAPrivateKey from PKCS #1. These structures are: | |
17 // | |
18 // PrivateKeyInfo ::= SEQUENCE { | |
19 // version Version, | |
20 // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, | |
21 // privateKey PrivateKey, | |
22 // attributes [0] IMPLICIT Attributes OPTIONAL | |
23 // } | |
24 // | |
25 // RSAPrivateKey ::= SEQUENCE { | |
26 // version Version, | |
27 // modulus INTEGER, | |
28 // publicExponent INTEGER, | |
29 // privateExponent INTEGER, | |
30 // prime1 INTEGER, | |
31 // prime2 INTEGER, | |
32 // exponent1 INTEGER, | |
33 // exponent2 INTEGER, | |
34 // coefficient INTEGER | |
35 // } | |
36 | |
37 | |
38 namespace { | 14 namespace { |
39 | 15 // Helper for error handling during key import. |
40 // ASN.1 encoding of the AlgorithmIdentifier from PKCS #8. | |
41 const uint8 kRsaAlgorithmIdentifier[] = { | |
42 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, | |
43 0x05, 0x00 | |
44 }; | |
45 | |
46 // ASN.1 tags for some types we use. | |
47 const uint8 kSequenceTag = 0x30; | |
48 const uint8 kIntegerTag = 0x02; | |
49 const uint8 kNullTag = 0x05; | |
50 const uint8 kOctetStringTag = 0x04; | |
51 | |
52 // Helper function to prepend an array of bytes into a list, reversing their | |
53 // order. This is needed because ASN.1 integers are big-endian, while CryptoAPI | |
54 // uses little-endian. | |
55 static void PrependBytesInReverseOrder(uint8* val, int num_bytes, | |
56 std::list<uint8>* data) { | |
57 for (int i = 0; i < num_bytes; ++i) | |
58 data->push_front(val[i]); | |
59 } | |
60 | |
61 // Helper to prepend an ASN.1 length field. | |
62 static void PrependLength(size_t size, std::list<uint8>* data) { | |
63 // The high bit is used to indicate whether additional octets are needed to | |
64 // represent the length. | |
65 if (size < 0x80) { | |
66 data->push_front(static_cast<uint8>(size)); | |
67 } else { | |
68 uint8 num_bytes = 0; | |
69 while (size > 0) { | |
70 data->push_front(static_cast<uint8>(size & 0xFF)); | |
71 size >>= 8; | |
72 num_bytes++; | |
73 } | |
74 CHECK(num_bytes <= 4); | |
75 data->push_front(0x80 | num_bytes); | |
76 } | |
77 } | |
78 | |
79 // Helper to prepend an ASN.1 type header. | |
80 static void PrependTypeHeaderAndLength(uint8 type, uint32 length, | |
81 std::list<uint8>* output) { | |
82 PrependLength(length, output); | |
83 output->push_front(type); | |
84 } | |
85 | |
86 // Helper to prepend an ASN.1 integer. | |
87 static void PrependInteger(uint8* val, int num_bytes, std::list<uint8>* data) { | |
88 // Skip trailing null bytes off the MSB end, which is the tail since the input | |
89 // is little endian. | |
90 while (num_bytes > 1 && val[num_bytes - 1] == 0x00) | |
91 num_bytes--; | |
92 | |
93 PrependBytesInReverseOrder(val, num_bytes, data); | |
94 | |
95 // If the MSB is set, we need to add an extra null byte, otherwise the integer | |
96 // could be interpreted as negative. | |
97 if ((val[num_bytes - 1] & 0x80) != 0) { | |
98 data->push_front(0x00); | |
99 num_bytes++; | |
100 } | |
101 | |
102 PrependTypeHeaderAndLength(kIntegerTag, num_bytes, data); | |
103 } | |
104 | |
105 // Helper for error handling during key import. | |
106 #define READ_ASSERT(truth) \ | 16 #define READ_ASSERT(truth) \ |
107 if (!(truth)) { \ | 17 if (!(truth)) { \ |
108 NOTREACHED(); \ | 18 NOTREACHED(); \ |
109 return false; \ | 19 return false; \ |
110 } | 20 } |
111 | |
112 // Read an ASN.1 length field. This also checks that the length does not extend | |
113 // beyond |end|. | |
114 static bool ReadLength(uint8** pos, uint8* end, uint32* result) { | |
115 READ_ASSERT(*pos < end); | |
116 int length = 0; | |
117 | |
118 // If the MSB is not set, the length is just the byte itself. | |
119 if (!(**pos & 0x80)) { | |
120 length = **pos; | |
121 (*pos)++; | |
122 } else { | |
123 // Otherwise, the lower 7 indicate the length of the length. | |
124 int length_of_length = **pos & 0x7F; | |
125 READ_ASSERT(length_of_length <= 4); | |
126 (*pos)++; | |
127 READ_ASSERT(*pos + length_of_length < end); | |
128 | |
129 length = 0; | |
130 for (int i = 0; i < length_of_length; ++i) { | |
131 length <<= 8; | |
132 length |= **pos; | |
133 (*pos)++; | |
134 } | |
135 } | |
136 | |
137 READ_ASSERT(*pos + length <= end); | |
138 if (result) *result = length; | |
139 return true; | |
140 } | |
141 | |
142 // Read an ASN.1 type header and its length. | |
143 static bool ReadTypeHeaderAndLength(uint8** pos, uint8* end, | |
144 uint8 expected_tag, uint32* length) { | |
145 READ_ASSERT(*pos < end); | |
146 READ_ASSERT(**pos == expected_tag); | |
147 (*pos)++; | |
148 | |
149 return ReadLength(pos, end, length); | |
150 } | |
151 | |
152 // Read an ASN.1 sequence declaration. This consumes the type header and length | |
153 // field, but not the contents of the sequence. | |
154 static bool ReadSequence(uint8** pos, uint8* end) { | |
155 return ReadTypeHeaderAndLength(pos, end, kSequenceTag, NULL); | |
156 } | |
157 | |
158 // Read the RSA AlgorithmIdentifier. | |
159 static bool ReadAlgorithmIdentifier(uint8** pos, uint8* end) { | |
160 READ_ASSERT(*pos + sizeof(kRsaAlgorithmIdentifier) < end); | |
161 READ_ASSERT(memcmp(*pos, kRsaAlgorithmIdentifier, | |
162 sizeof(kRsaAlgorithmIdentifier)) == 0); | |
163 (*pos) += sizeof(kRsaAlgorithmIdentifier); | |
164 return true; | |
165 } | |
166 | |
167 // Read one of the two version fields in PrivateKeyInfo. | |
168 static bool ReadVersion(uint8** pos, uint8* end) { | |
169 uint32 length = 0; | |
170 if (!ReadTypeHeaderAndLength(pos, end, kIntegerTag, &length)) | |
171 return false; | |
172 | |
173 // The version should be zero. | |
174 for (uint32 i = 0; i < length; ++i) { | |
175 READ_ASSERT(**pos == 0x00); | |
176 (*pos)++; | |
177 } | |
178 | |
179 return true; | |
180 } | |
181 | |
182 // Read an ASN.1 integer. | |
183 static bool ReadInteger(uint8** pos, uint8* end, std::vector<uint8>* out) { | |
184 uint32 length = 0; | |
185 if (!ReadTypeHeaderAndLength(pos, end, kIntegerTag, &length)) | |
186 return false; | |
187 | |
188 // Read the bytes out in reverse order because of endianness. | |
189 for (uint32 i = length - 1; i > 0; --i) | |
190 out->push_back(*(*pos + i)); | |
191 | |
192 // The last byte can be zero to force positiveness. We can ignore this. | |
193 if (**pos != 0x00) | |
194 out->push_back(**pos); | |
195 | |
196 (*pos) += length; | |
197 return true; | |
198 } | |
199 | |
200 static bool ReadIntegerWithExpectedSize(uint8** pos, uint8* end, | |
201 int expected_size, | |
202 std::vector<uint8>* out) { | |
203 if (!ReadInteger(pos, end, out)) | |
204 return false; | |
205 | |
206 if (out->size() == expected_size + 1) { | |
207 READ_ASSERT(out->back() == 0x00); | |
208 out->pop_back(); | |
209 } else { | |
210 READ_ASSERT(out->size() <= expected_size); | |
211 } | |
212 | |
213 // Pad out any missing bytes with null. | |
214 for (size_t i = out->size(); i < expected_size; ++i) | |
215 out->push_back(0x00); | |
216 | |
217 return true; | |
218 } | |
219 | |
220 } // namespace | 21 } // namespace |
221 | 22 |
222 | |
223 namespace base { | 23 namespace base { |
224 | 24 |
225 // static | 25 // static |
226 RSAPrivateKey* RSAPrivateKey::Create(uint16 num_bits) { | 26 RSAPrivateKey* RSAPrivateKey::Create(uint16 num_bits) { |
227 scoped_ptr<RSAPrivateKey> result(new RSAPrivateKey); | 27 scoped_ptr<RSAPrivateKey> result(new RSAPrivateKey); |
228 if (!result->InitProvider()) | 28 if (!result->InitProvider()) |
229 return NULL; | 29 return NULL; |
230 | 30 |
231 DWORD flags = CRYPT_EXPORTABLE; | 31 DWORD flags = CRYPT_EXPORTABLE; |
232 | 32 |
233 // The size is encoded as the upper 16 bits of the flags. :: sigh ::. | 33 // The size is encoded as the upper 16 bits of the flags. :: sigh ::. |
234 flags |= (num_bits << 16); | 34 flags |= (num_bits << 16); |
235 if (!CryptGenKey(result->provider_, CALG_RSA_SIGN, flags, &result->key_)) | 35 if (!CryptGenKey(result->provider_, CALG_RSA_SIGN, flags, &result->key_)) |
236 return NULL; | 36 return NULL; |
237 | 37 |
238 return result.release(); | 38 return result.release(); |
239 } | 39 } |
240 | 40 |
241 // static | 41 // static |
242 RSAPrivateKey* RSAPrivateKey::CreateFromPrivateKeyInfo( | 42 RSAPrivateKey* RSAPrivateKey::CreateFromPrivateKeyInfo( |
243 const std::vector<uint8>& input) { | 43 const std::vector<uint8>& input) { |
244 scoped_ptr<RSAPrivateKey> result(new RSAPrivateKey); | 44 scoped_ptr<RSAPrivateKey> result(new RSAPrivateKey); |
245 if (!result->InitProvider()) | 45 if (!result->InitProvider()) |
246 return NULL; | 46 return NULL; |
247 | 47 |
248 uint8* src = const_cast<uint8*>(&input.front()); | 48 PrivateKeyInfoCodec pki(false); // Little-Endian |
249 uint8* end = src + input.size(); | 49 pki.Import(input); |
250 int version = -1; | |
251 std::vector<uint8> modulus; | |
252 std::vector<uint8> public_exponent; | |
253 std::vector<uint8> private_exponent; | |
254 std::vector<uint8> prime1; | |
255 std::vector<uint8> prime2; | |
256 std::vector<uint8> exponent1; | |
257 std::vector<uint8> exponent2; | |
258 std::vector<uint8> coefficient; | |
259 | 50 |
260 if (!ReadSequence(&src, end) || | 51 int blob_size = sizeof(PUBLICKEYSTRUC) + |
261 !ReadVersion(&src, end) || | 52 sizeof(RSAPUBKEY) + |
262 !ReadAlgorithmIdentifier(&src, end) || | 53 pki.modulus()->size() + |
263 !ReadTypeHeaderAndLength(&src, end, kOctetStringTag, NULL) || | 54 pki.prime1()->size() + |
264 !ReadSequence(&src, end) || | 55 pki.prime2()->size() + |
265 !ReadVersion(&src, end) || | 56 pki.exponent1()->size() + |
266 !ReadInteger(&src, end, &modulus)) | 57 pki.exponent2()->size() + |
267 return false; | 58 pki.coefficient()->size() + |
268 | 59 pki.private_exponent()->size(); |
269 int mod_size = modulus.size(); | |
270 READ_ASSERT(mod_size % 2 == 0); | |
271 int primes_size = mod_size / 2; | |
272 | |
273 if (!ReadIntegerWithExpectedSize(&src, end, 4, &public_exponent) || | |
274 !ReadIntegerWithExpectedSize(&src, end, mod_size, &private_exponent) || | |
275 !ReadIntegerWithExpectedSize(&src, end, primes_size, &prime1) || | |
276 !ReadIntegerWithExpectedSize(&src, end, primes_size, &prime2) || | |
277 !ReadIntegerWithExpectedSize(&src, end, primes_size, &exponent1) || | |
278 !ReadIntegerWithExpectedSize(&src, end, primes_size, &exponent2) || | |
279 !ReadIntegerWithExpectedSize(&src, end, primes_size, &coefficient)) | |
280 return false; | |
281 | |
282 READ_ASSERT(src == end); | |
283 | |
284 int blob_size = sizeof(PUBLICKEYSTRUC) + sizeof(RSAPUBKEY) + modulus.size() + | |
285 prime1.size() + prime2.size() + | |
286 exponent1.size() + exponent2.size() + | |
287 coefficient.size() + private_exponent.size(); | |
288 scoped_array<BYTE> blob(new BYTE[blob_size]); | 60 scoped_array<BYTE> blob(new BYTE[blob_size]); |
289 | 61 |
290 uint8* dest = blob.get(); | 62 uint8* dest = blob.get(); |
291 PUBLICKEYSTRUC* public_key_struc = reinterpret_cast<PUBLICKEYSTRUC*>(dest); | 63 PUBLICKEYSTRUC* public_key_struc = reinterpret_cast<PUBLICKEYSTRUC*>(dest); |
292 public_key_struc->bType = PRIVATEKEYBLOB; | 64 public_key_struc->bType = PRIVATEKEYBLOB; |
293 public_key_struc->bVersion = 0x02; | 65 public_key_struc->bVersion = 0x02; |
294 public_key_struc->reserved = 0; | 66 public_key_struc->reserved = 0; |
295 public_key_struc->aiKeyAlg = CALG_RSA_SIGN; | 67 public_key_struc->aiKeyAlg = CALG_RSA_SIGN; |
296 dest += sizeof(PUBLICKEYSTRUC); | 68 dest += sizeof(PUBLICKEYSTRUC); |
297 | 69 |
298 RSAPUBKEY* rsa_pub_key = reinterpret_cast<RSAPUBKEY*>(dest); | 70 RSAPUBKEY* rsa_pub_key = reinterpret_cast<RSAPUBKEY*>(dest); |
299 rsa_pub_key->magic = 0x32415352; | 71 rsa_pub_key->magic = 0x32415352; |
300 rsa_pub_key->bitlen = modulus.size() * 8; | 72 rsa_pub_key->bitlen = pki.modulus()->size() * 8; |
301 int public_exponent_int = 0; | 73 int public_exponent_int = 0; |
302 for (size_t i = public_exponent.size(); i > 0; --i) { | 74 for (size_t i = pki.public_exponent()->size(); i > 0; --i) { |
303 public_exponent_int <<= 8; | 75 public_exponent_int <<= 8; |
304 public_exponent_int |= public_exponent[i - 1]; | 76 public_exponent_int |= (*pki.public_exponent())[i - 1]; |
305 } | 77 } |
306 rsa_pub_key->pubexp = public_exponent_int; | 78 rsa_pub_key->pubexp = public_exponent_int; |
307 dest += sizeof(RSAPUBKEY); | 79 dest += sizeof(RSAPUBKEY); |
308 | 80 |
309 memcpy(dest, &modulus.front(), modulus.size()); | 81 memcpy(dest, &pki.modulus()->front(), pki.modulus()->size()); |
310 dest += modulus.size(); | 82 dest += pki.modulus()->size(); |
311 memcpy(dest, &prime1.front(), prime1.size()); | 83 memcpy(dest, &pki.prime1()->front(), pki.prime1()->size()); |
312 dest += prime1.size(); | 84 dest += pki.prime1()->size(); |
313 memcpy(dest, &prime2.front(), prime2.size()); | 85 memcpy(dest, &pki.prime2()->front(), pki.prime2()->size()); |
314 dest += prime2.size(); | 86 dest += pki.prime2()->size(); |
315 memcpy(dest, &exponent1.front(), exponent1.size()); | 87 memcpy(dest, &pki.exponent1()->front(), pki.exponent1()->size()); |
316 dest += exponent1.size(); | 88 dest += pki.exponent1()->size(); |
317 memcpy(dest, &exponent2.front(), exponent2.size()); | 89 memcpy(dest, &pki.exponent2()->front(), pki.exponent2()->size()); |
318 dest += exponent2.size(); | 90 dest += pki.exponent2()->size(); |
319 memcpy(dest, &coefficient.front(), coefficient.size()); | 91 memcpy(dest, &pki.coefficient()->front(), pki.coefficient()->size()); |
320 dest += coefficient.size(); | 92 dest += pki.coefficient()->size(); |
321 memcpy(dest, &private_exponent.front(), private_exponent.size()); | 93 memcpy(dest, &pki.private_exponent()->front(), pki.private_exponent()->size())
; |
322 dest += private_exponent.size(); | 94 dest += pki.private_exponent()->size(); |
323 | 95 |
324 READ_ASSERT(dest == blob.get() + blob_size); | 96 READ_ASSERT(dest == blob.get() + blob_size); |
325 if (!CryptImportKey( | 97 if (!CryptImportKey( |
326 result->provider_, reinterpret_cast<uint8*>(public_key_struc), blob_size, | 98 result->provider_, reinterpret_cast<uint8*>(public_key_struc), blob_size, |
327 NULL, CRYPT_EXPORTABLE, &result->key_)) { | 99 NULL, CRYPT_EXPORTABLE, &result->key_)) { |
328 return NULL; | 100 return NULL; |
329 } | 101 } |
330 | 102 |
331 return result.release(); | 103 return result.release(); |
332 } | 104 } |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
368 uint8* pos = blob.get(); | 140 uint8* pos = blob.get(); |
369 PUBLICKEYSTRUC *publickey_struct = reinterpret_cast<PUBLICKEYSTRUC*>(pos); | 141 PUBLICKEYSTRUC *publickey_struct = reinterpret_cast<PUBLICKEYSTRUC*>(pos); |
370 pos += sizeof(PUBLICKEYSTRUC); | 142 pos += sizeof(PUBLICKEYSTRUC); |
371 | 143 |
372 RSAPUBKEY *rsa_pub_key = reinterpret_cast<RSAPUBKEY*>(pos); | 144 RSAPUBKEY *rsa_pub_key = reinterpret_cast<RSAPUBKEY*>(pos); |
373 pos += sizeof(RSAPUBKEY); | 145 pos += sizeof(RSAPUBKEY); |
374 | 146 |
375 int mod_size = rsa_pub_key->bitlen / 8; | 147 int mod_size = rsa_pub_key->bitlen / 8; |
376 int primes_size = rsa_pub_key->bitlen / 16; | 148 int primes_size = rsa_pub_key->bitlen / 16; |
377 | 149 |
378 uint8* modulus = pos; | 150 PrivateKeyInfoCodec pki(false); // Little-Endian |
| 151 |
| 152 pki.modulus()->assign(pos, pos + mod_size); |
379 pos += mod_size; | 153 pos += mod_size; |
380 | 154 |
381 uint8* prime1 = pos; | 155 pki.prime1()->assign(pos, pos + primes_size); |
382 pos += primes_size; | 156 pos += primes_size; |
383 uint8* prime2 = pos; | 157 pki.prime2()->assign(pos, pos + primes_size); |
384 pos += primes_size; | 158 pos += primes_size; |
385 | 159 |
386 uint8* exponent1 = pos; | 160 pki.exponent1()->assign(pos, pos + primes_size); |
387 pos += primes_size; | 161 pos += primes_size; |
388 uint8* exponent2 = pos; | 162 pki.exponent2()->assign(pos, pos + primes_size); |
389 pos += primes_size; | 163 pos += primes_size; |
390 | 164 |
391 uint8* coefficient = pos; | 165 pki.coefficient()->assign(pos, pos + primes_size); |
392 pos += primes_size; | 166 pos += primes_size; |
393 | 167 |
394 uint8* private_exponent = pos; | 168 pki.private_exponent()->assign(pos, pos + mod_size); |
395 pos += mod_size; | 169 pos += mod_size; |
396 | 170 |
| 171 pki.public_exponent()->assign(reinterpret_cast<uint8*>(&rsa_pub_key->pubexp), |
| 172 reinterpret_cast<uint8*>(&rsa_pub_key->pubexp) + 4); |
| 173 |
397 CHECK((pos - blob_length) == reinterpret_cast<BYTE*>(publickey_struct)); | 174 CHECK((pos - blob_length) == reinterpret_cast<BYTE*>(publickey_struct)); |
398 | 175 |
399 std::list<uint8> content; | 176 return pki.Export(output); |
400 | |
401 // Version (always zero) | |
402 uint8 version = 0; | |
403 | |
404 // We build up the output in reverse order to prevent having to do copies to | |
405 // figure out the length. | |
406 PrependInteger(coefficient, primes_size, &content); | |
407 PrependInteger(exponent2, primes_size, &content); | |
408 PrependInteger(exponent1, primes_size, &content); | |
409 PrependInteger(prime2, primes_size, &content); | |
410 PrependInteger(prime1, primes_size, &content); | |
411 PrependInteger(private_exponent, mod_size, &content); | |
412 PrependInteger(reinterpret_cast<uint8*>(&rsa_pub_key->pubexp), 4, &content); | |
413 PrependInteger(modulus, mod_size, &content); | |
414 PrependInteger(&version, 1, &content); | |
415 PrependTypeHeaderAndLength(kSequenceTag, content.size(), &content); | |
416 PrependTypeHeaderAndLength(kOctetStringTag, content.size(), &content); | |
417 | |
418 // RSA algorithm OID | |
419 for (size_t i = sizeof(kRsaAlgorithmIdentifier); i > 0; --i) | |
420 content.push_front(kRsaAlgorithmIdentifier[i - 1]); | |
421 | |
422 PrependInteger(&version, 1, &content); | |
423 PrependTypeHeaderAndLength(kSequenceTag, content.size(), &content); | |
424 | |
425 // Copy everying into the output. | |
426 output->reserve(content.size()); | |
427 for (std::list<uint8>::iterator i = content.begin(); i != content.end(); ++i) | |
428 output->push_back(*i); | |
429 | |
430 return true; | |
431 } | 177 } |
432 | 178 |
433 bool RSAPrivateKey::ExportPublicKey(std::vector<uint8>* output) { | 179 bool RSAPrivateKey::ExportPublicKey(std::vector<uint8>* output) { |
434 DWORD key_info_len; | 180 DWORD key_info_len; |
435 if (!CryptExportPublicKeyInfo( | 181 if (!CryptExportPublicKeyInfo( |
436 provider_, AT_SIGNATURE, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, | 182 provider_, AT_SIGNATURE, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, |
437 NULL, &key_info_len)) { | 183 NULL, &key_info_len)) { |
438 NOTREACHED(); | 184 NOTREACHED(); |
439 return false; | 185 return false; |
440 } | 186 } |
(...skipping 24 matching lines...) Expand all Loading... |
465 return false; | 211 return false; |
466 } | 212 } |
467 | 213 |
468 for (size_t i = 0; i < encoded_length; ++i) | 214 for (size_t i = 0; i < encoded_length; ++i) |
469 output->push_back(encoded[i]); | 215 output->push_back(encoded[i]); |
470 | 216 |
471 return true; | 217 return true; |
472 } | 218 } |
473 | 219 |
474 } // namespace base | 220 } // namespace base |
OLD | NEW |