-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add public/private key encryption and decryption #110
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ __pycache__ | |
/.coverage | ||
/.pytest_cache | ||
/bitcash.egg-info | ||
/BitCash.egg-info | ||
/build | ||
/dist | ||
/docs/build | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
from hashlib import new, sha256 as _sha256 | ||
from hashlib import new, sha256 as _sha256, sha512 as _sha512 | ||
import base64 | ||
import hmac | ||
|
||
import pyaes | ||
from coincurve import PrivateKey as ECPrivateKey, PublicKey as ECPublicKey | ||
|
||
|
||
|
@@ -20,3 +23,110 @@ def ripemd160_sha256(bytestr): | |
|
||
|
||
hash160 = ripemd160_sha256 | ||
|
||
|
||
def sha512(bytestr): | ||
return _sha512(bytestr).digest() | ||
|
||
|
||
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used | ||
# as the cipher; hmac-sha256 is used as the mac | ||
# Implementation follows the Elecrton-Cash implementaion of the same | ||
merc1er marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def aes_encrypt_with_iv(key, iv, data): | ||
"""Provides AES-CBC encryption of data with key and iv | ||
|
||
:param key: key for the encryption | ||
:type key: ``bytes`` | ||
:param iv: Initialisation vector for the encryption | ||
:type iv: ``bytes`` | ||
:param data: the data to be encrypted | ||
:type data: ``bytes`` | ||
""" | ||
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) | ||
aes = pyaes.Encrypter(aes_cbc) | ||
# empty aes.feed() flushes buffer | ||
return aes.feed(data) + aes.feed() | ||
|
||
|
||
def aes_decrypt_with_iv(key, iv, data): | ||
"""Provides AES-CBC decryption of data with key and iv | ||
|
||
:param key: key for the decryption | ||
:type key: ``bytes`` | ||
:param iv: Initialisation vector for the decryption | ||
:type iv: ``bytes`` | ||
:param data: the data to be decrypted | ||
:type data: ``bytes`` | ||
:raises ValueError: if incorrect ``key`` or ``iv`` give a padding error | ||
during decryption | ||
""" | ||
# assert_bytes(key, iv, data) | ||
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) | ||
aes = pyaes.Decrypter(aes_cbc) | ||
try: | ||
# empty aes.feed() flushes buffer | ||
return aes.feed(data) + aes.feed() | ||
except ValueError: | ||
raise ValueError('Invalid key or iv') | ||
|
||
|
||
def ecies_encrypt(message, pubkey): | ||
"""Encrypt message with the given pubkey | ||
|
||
:param message: the message to be encrypted | ||
:type message: ``bytes`` | ||
:param pubkey: the public key to be used | ||
:type pubkey: ``bytes`` | ||
""" | ||
pk = ECPublicKey(pubkey) | ||
|
||
# random key | ||
ephemeral = ECPrivateKey() | ||
ecdh_key = pk.multiply(ephemeral.secret).format() | ||
key = sha512(ecdh_key) | ||
|
||
# aes key and iv, and hmac key | ||
iv, key_e, key_m = key[0:16], key[16:32], key[32:] | ||
ciphertext = aes_encrypt_with_iv(key_e, iv, message) | ||
encrypted = ( | ||
b'BIE1' | ||
+ ephemeral.public_key.format() | ||
+ ciphertext | ||
) | ||
mac = hmac.new(key_m, encrypted, _sha256).digest() | ||
|
||
return base64.b64encode(encrypted + mac) | ||
|
||
|
||
def ecies_decrypt(encrypted, secret): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might consider making all functions except There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made aes_* functions private, since they're only used by ecies_* functions -- which should be public. But the other functions should be left public, since they expose hashlib functions in a simpler way, as used by Bitcash in other functions/classes. |
||
"""Decrypt the encrypted message using the instance's private key | ||
|
||
:param encrypted: the message to be decrypted | ||
:type encrypted: ``bytes`` | ||
:param secret: the private key secret to be used | ||
:type secret: ``bytes`` | ||
:raises ValueError: if magic bytes or HMAC bytes are invalid | ||
""" | ||
encrypted = base64.b64decode(encrypted) | ||
if len(encrypted) < 85: | ||
raise ValueError('Invalid cipher length') | ||
|
||
# splitting data | ||
magic = encrypted[:4] | ||
ephemeral_pubkey = ECPublicKey(encrypted[4:37]) | ||
ciphertext = encrypted[37:-32] | ||
mac = encrypted[-32:] | ||
if magic != b'BIE1': | ||
raise ValueError('Invalid magic bytes') | ||
|
||
# retrieving keys | ||
ecdh_key = ephemeral_pubkey.multiply(secret).format() | ||
key = sha512(ecdh_key) | ||
iv, key_e, key_m = key[0:16], key[16:32], key[32:] | ||
|
||
# validating hmac | ||
if mac != hmac.new(key_m, encrypted[:-32], _sha256).digest(): | ||
raise ValueError("Invalid HMAC bytes") | ||
|
||
# decrypting | ||
return aes_decrypt_with_iv(key_e, iv, ciphertext) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,7 @@ | |
'Programming Language :: Python :: Implementation :: PyPy' | ||
], | ||
|
||
install_requires=['coincurve>=4.3.0', 'requests'], | ||
install_requires=['coincurve>=4.3.0', 'requests', 'pyaes'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's very cool that pyaes has no dependencies, but it's a bit dated and possibly prone to timing attacks. I'd just like to point that out. Is this what Electron Cash uses? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Electron cash uses cryptodome; however, falls back to pyaes if cryptodome import fails. Pyaes is simply a pythonic implementation of AES-128. I think, mitigating attack such as timing attack would be more relevant on how bitcash -- and apps/implementation that use bitcash -- uses pyaes. |
||
extras_require={ | ||
'cli': ('appdirs', 'click', 'privy', 'tinydb'), | ||
'cache': ('lmdb', ), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import pytest | ||
|
||
from bitcash.crypto import ( | ||
aes_encrypt_with_iv, | ||
aes_decrypt_with_iv, | ||
ecies_encrypt, | ||
ecies_decrypt | ||
) | ||
|
||
|
||
KEY_AES = b'$\x99\xd7-\x10\x1aY\xa4"\xd6\x9c\x7f\x0f\xd7\x0aT' | ||
|
||
KEY_AES2 = b'$\x99\xd7-\x10\x1aY\xa4"\xd6\x9c\x7f\x0f\xd7\x0bT' | ||
|
||
IV = b'\\\xdf\x8c\xdd\xebA\xa6\x7f\xfa\xbfq\x0cn\xccr\xc8' | ||
|
||
PUBKEY = ( | ||
b"\x03=\\(u\xc9\xbd\x11hu\xa7\x1a]\xb6L\xff\xcb\x139k\x16=\x03\x9b" | ||
+ b"\x1d\x93'\x82H\x91\x80C4" | ||
) | ||
|
||
SECRET = ( | ||
b'\xc2\x8a\x9f\x80s\x8fw\rRx\x03\xa5f\xcfo\xc3\xed\xf6\xce\xa5\x86' | ||
+ b'\xc4\xfcJR#\xa5\xady~\x1a\xc3' | ||
) | ||
|
||
SECRET2 = ( | ||
b'\xc2\x8a\x9f\x80s\x8fw\rRx\x03\xa5f\xcfo\xc3\xed\xf6\xce\xa5\x86' | ||
+ b'\xc4\xfcJR#\xa5\xady~\x1a\xc4' | ||
) | ||
|
||
|
||
class TestAes: | ||
def test_aes_success(self): | ||
message = b'test' | ||
encrypted_message = aes_encrypt_with_iv(KEY_AES, IV, message) | ||
decrypted_message = aes_decrypt_with_iv(KEY_AES, IV, encrypted_message) | ||
assert message == decrypted_message | ||
|
||
def test_aes_fail(self): | ||
message = b'test' | ||
encrypted_message = aes_encrypt_with_iv(KEY_AES, IV, message) | ||
with pytest.raises(ValueError): | ||
decrypted_message = aes_decrypt_with_iv(KEY_AES2, | ||
IV, | ||
encrypted_message) | ||
|
||
|
||
class TestEcies: | ||
def test_ecies_success(self): | ||
message = b'test' | ||
encrypted_message = ecies_encrypt(message, PUBKEY) | ||
decrypted_message = ecies_decrypt(encrypted_message, SECRET) | ||
assert message == decrypted_message | ||
|
||
def test_ecies_fail(self): | ||
message = b'test' | ||
encrypted_message = ecies_encrypt(message, PUBKEY) | ||
with pytest.raises(ValueError): | ||
decrypted_message = ecies_decrypt(encrypted_message, SECRET2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that only the minimum should contain public methods. This could be
_sha512()
.