Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(119)

Side by Side Diff: third_party/google-endpoints/rsa/key.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/google-endpoints/rsa/core.py ('k') | third_party/google-endpoints/rsa/parallel.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright 2011 Sybren A. Stüvel <sybren@stuvel.eu>
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 # https://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 """RSA key generation code.
18
19 Create new keys with the newkeys() function. It will give you a PublicKey and a
20 PrivateKey object.
21
22 Loading and saving keys requires the pyasn1 module. This module is imported as
23 late as possible, such that other functionality will remain working in absence
24 of pyasn1.
25
26 .. note::
27
28 Storing public and private keys via the `pickle` module is possible.
29 However, it is insecure to load a key from an untrusted source.
30 The pickle module is not secure against erroneous or maliciously
31 constructed data. Never unpickle data received from an untrusted
32 or unauthenticated source.
33
34 """
35
36 import logging
37 from rsa._compat import b
38
39 import rsa.prime
40 import rsa.pem
41 import rsa.common
42 import rsa.randnum
43 import rsa.core
44
45 log = logging.getLogger(__name__)
46 DEFAULT_EXPONENT = 65537
47
48
49 class AbstractKey(object):
50 """Abstract superclass for private and public keys."""
51
52 __slots__ = ('n', 'e')
53
54 def __init__(self, n, e):
55 self.n = n
56 self.e = e
57
58 @classmethod
59 def load_pkcs1(cls, keyfile, format='PEM'):
60 """Loads a key in PKCS#1 DER or PEM format.
61
62 :param keyfile: contents of a DER- or PEM-encoded file that contains
63 the public key.
64 :param format: the format of the file to load; 'PEM' or 'DER'
65
66 :return: a PublicKey object
67 """
68
69 methods = {
70 'PEM': cls._load_pkcs1_pem,
71 'DER': cls._load_pkcs1_der,
72 }
73
74 method = cls._assert_format_exists(format, methods)
75 return method(keyfile)
76
77 @staticmethod
78 def _assert_format_exists(file_format, methods):
79 """Checks whether the given file format exists in 'methods'.
80 """
81
82 try:
83 return methods[file_format]
84 except KeyError:
85 formats = ', '.join(sorted(methods.keys()))
86 raise ValueError('Unsupported format: %r, try one of %s' % (file_for mat,
87 formats) )
88
89 def save_pkcs1(self, format='PEM'):
90 """Saves the public key in PKCS#1 DER or PEM format.
91
92 :param format: the format to save; 'PEM' or 'DER'
93 :returns: the DER- or PEM-encoded public key.
94 """
95
96 methods = {
97 'PEM': self._save_pkcs1_pem,
98 'DER': self._save_pkcs1_der,
99 }
100
101 method = self._assert_format_exists(format, methods)
102 return method()
103
104 def blind(self, message, r):
105 """Performs blinding on the message using random number 'r'.
106
107 :param message: the message, as integer, to blind.
108 :type message: int
109 :param r: the random number to blind with.
110 :type r: int
111 :return: the blinded message.
112 :rtype: int
113
114 The blinding is such that message = unblind(decrypt(blind(encrypt(messag e))).
115
116 See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29
117 """
118
119 return (message * pow(r, self.e, self.n)) % self.n
120
121 def unblind(self, blinded, r):
122 """Performs blinding on the message using random number 'r'.
123
124 :param blinded: the blinded message, as integer, to unblind.
125 :param r: the random number to unblind with.
126 :return: the original message.
127
128 The blinding is such that message = unblind(decrypt(blind(encrypt(messag e))).
129
130 See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29
131 """
132
133 return (rsa.common.inverse(r, self.n) * blinded) % self.n
134
135
136 class PublicKey(AbstractKey):
137 """Represents a public RSA key.
138
139 This key is also known as the 'encryption key'. It contains the 'n' and 'e'
140 values.
141
142 Supports attributes as well as dictionary-like access. Attribute accesss is
143 faster, though.
144
145 >>> PublicKey(5, 3)
146 PublicKey(5, 3)
147
148 >>> key = PublicKey(5, 3)
149 >>> key.n
150 5
151 >>> key['n']
152 5
153 >>> key.e
154 3
155 >>> key['e']
156 3
157
158 """
159
160 __slots__ = ('n', 'e')
161
162 def __getitem__(self, key):
163 return getattr(self, key)
164
165 def __repr__(self):
166 return 'PublicKey(%i, %i)' % (self.n, self.e)
167
168 def __getstate__(self):
169 """Returns the key as tuple for pickling."""
170 return self.n, self.e
171
172 def __setstate__(self, state):
173 """Sets the key from tuple."""
174 self.n, self.e = state
175
176 def __eq__(self, other):
177 if other is None:
178 return False
179
180 if not isinstance(other, PublicKey):
181 return False
182
183 return self.n == other.n and self.e == other.e
184
185 def __ne__(self, other):
186 return not (self == other)
187
188 @classmethod
189 def _load_pkcs1_der(cls, keyfile):
190 """Loads a key in PKCS#1 DER format.
191
192 :param keyfile: contents of a DER-encoded file that contains the public
193 key.
194 :return: a PublicKey object
195
196 First let's construct a DER encoded key:
197
198 >>> import base64
199 >>> b64der = 'MAwCBQCNGmYtAgMBAAE='
200 >>> der = base64.standard_b64decode(b64der)
201
202 This loads the file:
203
204 >>> PublicKey._load_pkcs1_der(der)
205 PublicKey(2367317549, 65537)
206
207 """
208
209 from pyasn1.codec.der import decoder
210 from rsa.asn1 import AsnPubKey
211
212 (priv, _) = decoder.decode(keyfile, asn1Spec=AsnPubKey())
213 return cls(n=int(priv['modulus']), e=int(priv['publicExponent']))
214
215 def _save_pkcs1_der(self):
216 """Saves the public key in PKCS#1 DER format.
217
218 @returns: the DER-encoded public key.
219 """
220
221 from pyasn1.codec.der import encoder
222 from rsa.asn1 import AsnPubKey
223
224 # Create the ASN object
225 asn_key = AsnPubKey()
226 asn_key.setComponentByName('modulus', self.n)
227 asn_key.setComponentByName('publicExponent', self.e)
228
229 return encoder.encode(asn_key)
230
231 @classmethod
232 def _load_pkcs1_pem(cls, keyfile):
233 """Loads a PKCS#1 PEM-encoded public key file.
234
235 The contents of the file before the "-----BEGIN RSA PUBLIC KEY-----" and
236 after the "-----END RSA PUBLIC KEY-----" lines is ignored.
237
238 :param keyfile: contents of a PEM-encoded file that contains the public
239 key.
240 :return: a PublicKey object
241 """
242
243 der = rsa.pem.load_pem(keyfile, 'RSA PUBLIC KEY')
244 return cls._load_pkcs1_der(der)
245
246 def _save_pkcs1_pem(self):
247 """Saves a PKCS#1 PEM-encoded public key file.
248
249 :return: contents of a PEM-encoded file that contains the public key.
250 """
251
252 der = self._save_pkcs1_der()
253 return rsa.pem.save_pem(der, 'RSA PUBLIC KEY')
254
255 @classmethod
256 def load_pkcs1_openssl_pem(cls, keyfile):
257 """Loads a PKCS#1.5 PEM-encoded public key file from OpenSSL.
258
259 These files can be recognised in that they start with BEGIN PUBLIC KEY
260 rather than BEGIN RSA PUBLIC KEY.
261
262 The contents of the file before the "-----BEGIN PUBLIC KEY-----" and
263 after the "-----END PUBLIC KEY-----" lines is ignored.
264
265 :param keyfile: contents of a PEM-encoded file that contains the public
266 key, from OpenSSL.
267 :return: a PublicKey object
268 """
269
270 der = rsa.pem.load_pem(keyfile, 'PUBLIC KEY')
271 return cls.load_pkcs1_openssl_der(der)
272
273 @classmethod
274 def load_pkcs1_openssl_der(cls, keyfile):
275 """Loads a PKCS#1 DER-encoded public key file from OpenSSL.
276
277 :param keyfile: contents of a DER-encoded file that contains the public
278 key, from OpenSSL.
279 :return: a PublicKey object
280
281 """
282
283 from rsa.asn1 import OpenSSLPubKey
284 from pyasn1.codec.der import decoder
285 from pyasn1.type import univ
286
287 (keyinfo, _) = decoder.decode(keyfile, asn1Spec=OpenSSLPubKey())
288
289 if keyinfo['header']['oid'] != univ.ObjectIdentifier('1.2.840.113549.1.1 .1'):
290 raise TypeError("This is not a DER-encoded OpenSSL-compatible public key")
291
292 return cls._load_pkcs1_der(keyinfo['key'][1:])
293
294
295 class PrivateKey(AbstractKey):
296 """Represents a private RSA key.
297
298 This key is also known as the 'decryption key'. It contains the 'n', 'e',
299 'd', 'p', 'q' and other values.
300
301 Supports attributes as well as dictionary-like access. Attribute accesss is
302 faster, though.
303
304 >>> PrivateKey(3247, 65537, 833, 191, 17)
305 PrivateKey(3247, 65537, 833, 191, 17)
306
307 exp1, exp2 and coef can be given, but if None or omitted they will be calcul ated:
308
309 >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287, exp2=4)
310 >>> pk.exp1
311 55063
312 >>> pk.exp2 # this is of course not a correct value, but it is the one we p assed.
313 4
314 >>> pk.coef
315 50797
316
317 If you give exp1, exp2 or coef, they will be used as-is:
318
319 >>> pk = PrivateKey(1, 2, 3, 4, 5, 6, 7, 8)
320 >>> pk.exp1
321 6
322 >>> pk.exp2
323 7
324 >>> pk.coef
325 8
326
327 """
328
329 __slots__ = ('n', 'e', 'd', 'p', 'q', 'exp1', 'exp2', 'coef')
330
331 def __init__(self, n, e, d, p, q, exp1=None, exp2=None, coef=None):
332 AbstractKey.__init__(self, n, e)
333 self.d = d
334 self.p = p
335 self.q = q
336
337 # Calculate the other values if they aren't supplied
338 if exp1 is None:
339 self.exp1 = int(d % (p - 1))
340 else:
341 self.exp1 = exp1
342
343 if exp2 is None:
344 self.exp2 = int(d % (q - 1))
345 else:
346 self.exp2 = exp2
347
348 if coef is None:
349 self.coef = rsa.common.inverse(q, p)
350 else:
351 self.coef = coef
352
353 def __getitem__(self, key):
354 return getattr(self, key)
355
356 def __repr__(self):
357 return 'PrivateKey(%(n)i, %(e)i, %(d)i, %(p)i, %(q)i)' % self
358
359 def __getstate__(self):
360 """Returns the key as tuple for pickling."""
361 return self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, sel f.coef
362
363 def __setstate__(self, state):
364 """Sets the key from tuple."""
365 self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef = state
366
367 def __eq__(self, other):
368 if other is None:
369 return False
370
371 if not isinstance(other, PrivateKey):
372 return False
373
374 return (self.n == other.n and
375 self.e == other.e and
376 self.d == other.d and
377 self.p == other.p and
378 self.q == other.q and
379 self.exp1 == other.exp1 and
380 self.exp2 == other.exp2 and
381 self.coef == other.coef)
382
383 def __ne__(self, other):
384 return not (self == other)
385
386 def blinded_decrypt(self, encrypted):
387 """Decrypts the message using blinding to prevent side-channel attacks.
388
389 :param encrypted: the encrypted message
390 :type encrypted: int
391
392 :returns: the decrypted message
393 :rtype: int
394 """
395
396 blind_r = rsa.randnum.randint(self.n - 1)
397 blinded = self.blind(encrypted, blind_r) # blind before decrypting
398 decrypted = rsa.core.decrypt_int(blinded, self.d, self.n)
399
400 return self.unblind(decrypted, blind_r)
401
402 def blinded_encrypt(self, message):
403 """Encrypts the message using blinding to prevent side-channel attacks.
404
405 :param message: the message to encrypt
406 :type message: int
407
408 :returns: the encrypted message
409 :rtype: int
410 """
411
412 blind_r = rsa.randnum.randint(self.n - 1)
413 blinded = self.blind(message, blind_r) # blind before encrypting
414 encrypted = rsa.core.encrypt_int(blinded, self.d, self.n)
415 return self.unblind(encrypted, blind_r)
416
417 @classmethod
418 def _load_pkcs1_der(cls, keyfile):
419 """Loads a key in PKCS#1 DER format.
420
421 :param keyfile: contents of a DER-encoded file that contains the private
422 key.
423 :return: a PrivateKey object
424
425 First let's construct a DER encoded key:
426
427 >>> import base64
428 >>> b64der = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbw IDAMZt'
429 >>> der = base64.standard_b64decode(b64der)
430
431 This loads the file:
432
433 >>> PrivateKey._load_pkcs1_der(der)
434 PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
435
436 """
437
438 from pyasn1.codec.der import decoder
439 (priv, _) = decoder.decode(keyfile)
440
441 # ASN.1 contents of DER encoded private key:
442 #
443 # RSAPrivateKey ::= SEQUENCE {
444 # version Version,
445 # modulus INTEGER, -- n
446 # publicExponent INTEGER, -- e
447 # privateExponent INTEGER, -- d
448 # prime1 INTEGER, -- p
449 # prime2 INTEGER, -- q
450 # exponent1 INTEGER, -- d mod (p-1)
451 # exponent2 INTEGER, -- d mod (q-1)
452 # coefficient INTEGER, -- (inverse of q) mod p
453 # otherPrimeInfos OtherPrimeInfos OPTIONAL
454 # }
455
456 if priv[0] != 0:
457 raise ValueError('Unable to read this file, version %s != 0' % priv[ 0])
458
459 as_ints = tuple(int(x) for x in priv[1:9])
460 return cls(*as_ints)
461
462 def _save_pkcs1_der(self):
463 """Saves the private key in PKCS#1 DER format.
464
465 @returns: the DER-encoded private key.
466 """
467
468 from pyasn1.type import univ, namedtype
469 from pyasn1.codec.der import encoder
470
471 class AsnPrivKey(univ.Sequence):
472 componentType = namedtype.NamedTypes(
473 namedtype.NamedType('version', univ.Integer()),
474 namedtype.NamedType('modulus', univ.Integer()),
475 namedtype.NamedType('publicExponent', univ.Integer()),
476 namedtype.NamedType('privateExponent', univ.Integer()),
477 namedtype.NamedType('prime1', univ.Integer()),
478 namedtype.NamedType('prime2', univ.Integer()),
479 namedtype.NamedType('exponent1', univ.Integer()),
480 namedtype.NamedType('exponent2', univ.Integer()),
481 namedtype.NamedType('coefficient', univ.Integer()),
482 )
483
484 # Create the ASN object
485 asn_key = AsnPrivKey()
486 asn_key.setComponentByName('version', 0)
487 asn_key.setComponentByName('modulus', self.n)
488 asn_key.setComponentByName('publicExponent', self.e)
489 asn_key.setComponentByName('privateExponent', self.d)
490 asn_key.setComponentByName('prime1', self.p)
491 asn_key.setComponentByName('prime2', self.q)
492 asn_key.setComponentByName('exponent1', self.exp1)
493 asn_key.setComponentByName('exponent2', self.exp2)
494 asn_key.setComponentByName('coefficient', self.coef)
495
496 return encoder.encode(asn_key)
497
498 @classmethod
499 def _load_pkcs1_pem(cls, keyfile):
500 """Loads a PKCS#1 PEM-encoded private key file.
501
502 The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" an d
503 after the "-----END RSA PRIVATE KEY-----" lines is ignored.
504
505 :param keyfile: contents of a PEM-encoded file that contains the private
506 key.
507 :return: a PrivateKey object
508 """
509
510 der = rsa.pem.load_pem(keyfile, b('RSA PRIVATE KEY'))
511 return cls._load_pkcs1_der(der)
512
513 def _save_pkcs1_pem(self):
514 """Saves a PKCS#1 PEM-encoded private key file.
515
516 :return: contents of a PEM-encoded file that contains the private key.
517 """
518
519 der = self._save_pkcs1_der()
520 return rsa.pem.save_pem(der, b('RSA PRIVATE KEY'))
521
522
523 def find_p_q(nbits, getprime_func=rsa.prime.getprime, accurate=True):
524 """Returns a tuple of two different primes of nbits bits each.
525
526 The resulting p * q has exacty 2 * nbits bits, and the returned p and q
527 will not be equal.
528
529 :param nbits: the number of bits in each of p and q.
530 :param getprime_func: the getprime function, defaults to
531 :py:func:`rsa.prime.getprime`.
532
533 *Introduced in Python-RSA 3.1*
534
535 :param accurate: whether to enable accurate mode or not.
536 :returns: (p, q), where p > q
537
538 >>> (p, q) = find_p_q(128)
539 >>> from rsa import common
540 >>> common.bit_size(p * q)
541 256
542
543 When not in accurate mode, the number of bits can be slightly less
544
545 >>> (p, q) = find_p_q(128, accurate=False)
546 >>> from rsa import common
547 >>> common.bit_size(p * q) <= 256
548 True
549 >>> common.bit_size(p * q) > 240
550 True
551
552 """
553
554 total_bits = nbits * 2
555
556 # Make sure that p and q aren't too close or the factoring programs can
557 # factor n.
558 shift = nbits // 16
559 pbits = nbits + shift
560 qbits = nbits - shift
561
562 # Choose the two initial primes
563 log.debug('find_p_q(%i): Finding p', nbits)
564 p = getprime_func(pbits)
565 log.debug('find_p_q(%i): Finding q', nbits)
566 q = getprime_func(qbits)
567
568 def is_acceptable(p, q):
569 """Returns True iff p and q are acceptable:
570
571 - p and q differ
572 - (p * q) has the right nr of bits (when accurate=True)
573 """
574
575 if p == q:
576 return False
577
578 if not accurate:
579 return True
580
581 # Make sure we have just the right amount of bits
582 found_size = rsa.common.bit_size(p * q)
583 return total_bits == found_size
584
585 # Keep choosing other primes until they match our requirements.
586 change_p = False
587 while not is_acceptable(p, q):
588 # Change p on one iteration and q on the other
589 if change_p:
590 p = getprime_func(pbits)
591 else:
592 q = getprime_func(qbits)
593
594 change_p = not change_p
595
596 # We want p > q as described on
597 # http://www.di-mgt.com.au/rsa_alg.html#crt
598 return max(p, q), min(p, q)
599
600
601 def calculate_keys_custom_exponent(p, q, exponent):
602 """Calculates an encryption and a decryption key given p, q and an exponent,
603 and returns them as a tuple (e, d)
604
605 :param p: the first large prime
606 :param q: the second large prime
607 :param exponent: the exponent for the key; only change this if you know
608 what you're doing, as the exponent influences how difficult your
609 private key can be cracked. A very common choice for e is 65537.
610 :type exponent: int
611
612 """
613
614 phi_n = (p - 1) * (q - 1)
615
616 try:
617 d = rsa.common.inverse(exponent, phi_n)
618 except ValueError:
619 raise ValueError("e (%d) and phi_n (%d) are not relatively prime" %
620 (exponent, phi_n))
621
622 if (exponent * d) % phi_n != 1:
623 raise ValueError("e (%d) and d (%d) are not mult. inv. modulo "
624 "phi_n (%d)" % (exponent, d, phi_n))
625
626 return exponent, d
627
628
629 def calculate_keys(p, q):
630 """Calculates an encryption and a decryption key given p and q, and
631 returns them as a tuple (e, d)
632
633 :param p: the first large prime
634 :param q: the second large prime
635
636 :return: tuple (e, d) with the encryption and decryption exponents.
637 """
638
639 return calculate_keys_custom_exponent(p, q, DEFAULT_EXPONENT)
640
641
642 def gen_keys(nbits, getprime_func, accurate=True, exponent=DEFAULT_EXPONENT):
643 """Generate RSA keys of nbits bits. Returns (p, q, e, d).
644
645 Note: this can take a long time, depending on the key size.
646
647 :param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and
648 ``q`` will use ``nbits/2`` bits.
649 :param getprime_func: either :py:func:`rsa.prime.getprime` or a function
650 with similar signature.
651 :param exponent: the exponent for the key; only change this if you know
652 what you're doing, as the exponent influences how difficult your
653 private key can be cracked. A very common choice for e is 65537.
654 :type exponent: int
655 """
656
657 # Regenerate p and q values, until calculate_keys doesn't raise a
658 # ValueError.
659 while True:
660 (p, q) = find_p_q(nbits // 2, getprime_func, accurate)
661 try:
662 (e, d) = calculate_keys_custom_exponent(p, q, exponent=exponent)
663 break
664 except ValueError:
665 pass
666
667 return p, q, e, d
668
669
670 def newkeys(nbits, accurate=True, poolsize=1, exponent=DEFAULT_EXPONENT):
671 """Generates public and private keys, and returns them as (pub, priv).
672
673 The public key is also known as the 'encryption key', and is a
674 :py:class:`rsa.PublicKey` object. The private key is also known as the
675 'decryption key' and is a :py:class:`rsa.PrivateKey` object.
676
677 :param nbits: the number of bits required to store ``n = p*q``.
678 :param accurate: when True, ``n`` will have exactly the number of bits you
679 asked for. However, this makes key generation much slower. When False,
680 `n`` may have slightly less bits.
681 :param poolsize: the number of processes to use to generate the prime
682 numbers. If set to a number > 1, a parallel algorithm will be used.
683 This requires Python 2.6 or newer.
684 :param exponent: the exponent for the key; only change this if you know
685 what you're doing, as the exponent influences how difficult your
686 private key can be cracked. A very common choice for e is 65537.
687 :type exponent: int
688
689 :returns: a tuple (:py:class:`rsa.PublicKey`, :py:class:`rsa.PrivateKey`)
690
691 The ``poolsize`` parameter was added in *Python-RSA 3.1* and requires
692 Python 2.6 or newer.
693
694 """
695
696 if nbits < 16:
697 raise ValueError('Key too small')
698
699 if poolsize < 1:
700 raise ValueError('Pool size (%i) should be >= 1' % poolsize)
701
702 # Determine which getprime function to use
703 if poolsize > 1:
704 from rsa import parallel
705 import functools
706
707 getprime_func = functools.partial(parallel.getprime, poolsize=poolsize)
708 else:
709 getprime_func = rsa.prime.getprime
710
711 # Generate the key components
712 (p, q, e, d) = gen_keys(nbits, getprime_func, accurate=accurate, exponent=ex ponent)
713
714 # Create the key objects
715 n = p * q
716
717 return (
718 PublicKey(n, e),
719 PrivateKey(n, e, d, p, q)
720 )
721
722
723 __all__ = ['PublicKey', 'PrivateKey', 'newkeys']
724
725 if __name__ == '__main__':
726 import doctest
727
728 try:
729 for count in range(100):
730 (failures, tests) = doctest.testmod()
731 if failures:
732 break
733
734 if (count and count % 10 == 0) or count == 1:
735 print('%i times' % count)
736 except KeyboardInterrupt:
737 print('Aborted')
738 else:
739 print('Doctests done')
OLDNEW
« no previous file with comments | « third_party/google-endpoints/rsa/core.py ('k') | third_party/google-endpoints/rsa/parallel.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698