diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0a6226c..e9358c9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -6,6 +6,7 @@ Version 3.4 - in development ---------------------------------------- - Moved development to Github. +- Solved side-channel vulnerability by implementing blinding, fixes #19 - Fixed bugs #14, #27, #30 diff --git a/rsa/key.py b/rsa/key.py index d530f33..840407c 100644 --- a/rsa/key.py +++ b/rsa/key.py @@ -321,6 +321,42 @@ def __eq__(self, other): def __ne__(self, other): return not (self == other) + def blind(self, message, r): + """Performs blinding on the message using random number 'r'. + + @param message: the message, as integer, to blind. + @param r: the random number to blind with. + @return: the blinded message. + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + + >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + >>> message = 12345 + >>> encrypted = rsa.core.encrypt_int(message, pk.e, pk.n) + >>> blinded = pk.blind(encrypted, 4134431) # blind before decrypting + >>> decrypted = rsa.core.decrypt_int(blinded, pk.d, pk.n) + >>> pk.unblind(decrypted, 4134431) + 12345 + """ + + return (message * pow(r, self.e, self.n)) % self.n + + def unblind(self, blinded, r): + """Performs blinding on the message using random number 'r'. + + @param blinded: the blinded message, as integer, to unblind. + @param r: the random number to unblind with. + @return: the original message. + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + """ + + return (rsa.common.inverse(r, self.n) * blinded) % self.n + @classmethod def _load_pkcs1_der(cls, keyfile): """Loads a key in PKCS#1 DER format. diff --git a/rsa/pkcs1.py b/rsa/pkcs1.py index 7d6814c..0b7982c 100644 --- a/rsa/pkcs1.py +++ b/rsa/pkcs1.py @@ -229,8 +229,14 @@ def decrypt(crypto, priv_key): blocksize = common.byte_size(priv_key.n) encrypted = transform.bytes2int(crypto) - decrypted = core.decrypt_int(encrypted, priv_key.d, priv_key.n) - cleartext = transform.int2bytes(decrypted, blocksize) + + # Perform blinded decryption to prevent side-channel attacks. + # See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + blinded = priv_key.blind(encrypted, 4134431) # blind before decrypting + decrypted = core.decrypt_int(blinded, priv_key.d, priv_key.n) + unblinded = priv_key.unblind(decrypted, 4134431) + + cleartext = transform.int2bytes(unblinded, blocksize) # If we can't find the cleartext marker, decryption failed. if cleartext[0:2] != b('\x00\x02'): diff --git a/tests/test_key.py b/tests/test_key.py new file mode 100644 index 0000000..df35335 --- /dev/null +++ b/tests/test_key.py @@ -0,0 +1,30 @@ +""" +Some tests for the rsa/key.py file. +""" + + +import unittest + +import rsa.key +import rsa.core + + +class BlindingTest(unittest.TestCase): + + def test_blinding(self): + """Test blinding and unblinding. + + This is basically the doctest of the PrivateKey.blind method, but then + implemented as unittest to allow running on different Python versions. + """ + + pk = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + + message = 12345 + encrypted = rsa.core.encrypt_int(message, pk.e, pk.n) + + blinded = pk.blind(encrypted, 4134431) # blind before decrypting + decrypted = rsa.core.decrypt_int(blinded, pk.d, pk.n) + unblinded = pk.unblind(decrypted, 4134431) + + self.assertEqual(unblinded, message)