|
| 1 | +# Mechanism and implementation |
| 2 | + |
| 3 | +This library combines `secp256k1` and `AES-256-GCM` (powered by [`coincurve`](https://github.com/ofek/coincurve) and [`pycryptodome`](https://github.com/Legrandin/pycryptodome)) to provide an API of encrypting with `secp256k1` public key and decrypting with `secp256k1`'s private key. It has two parts generally: |
| 4 | + |
| 5 | +1. Use [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie–Hellman) to exchange an AES session key; |
| 6 | + |
| 7 | + > Notice that the sender public key is generated every time when `ecies.encrypt` is invoked, thus, the AES session key varies. |
| 8 | + > |
| 9 | + > We are using HKDF-SHA256 instead of SHA256 to derive the AES keys. |
| 10 | +
|
| 11 | +2. Use this AES session key to encrypt/decrypt the data under `AES-256-GCM`. |
| 12 | + |
| 13 | +Basically the encrypted data will be like this: |
| 14 | + |
| 15 | +```plaintext |
| 16 | ++-------------------------------+----------+----------+-----------------+ |
| 17 | +| 65 Bytes | 16 Bytes | 16 Bytes | == data size | |
| 18 | ++-------------------------------+----------+----------+-----------------+ |
| 19 | +| Sender Public Key (ephemeral) | Nonce/IV | Tag/MAC | Encrypted data | |
| 20 | ++-------------------------------+----------+----------+-----------------+ |
| 21 | +| sender_pk | nonce | tag | encrypted_data | |
| 22 | ++-------------------------------+----------+----------+-----------------+ |
| 23 | +| Secp256k1 | AES-256-GCM | |
| 24 | ++-------------------------------+---------------------------------------+ |
| 25 | +``` |
| 26 | + |
| 27 | +## Secp256k1 |
| 28 | + |
| 29 | +### Glance at ECDH |
| 30 | + |
| 31 | +So, **how** do we calculate the ECDH key under `secp256k1`? If you use a library like [`coincurve`](https://github.com/ofek/coincurve), you might just simply call `k1.ecdh(k2.public_key.format())`, then uh-huh, you got it! Let's see how to do it in simple Python snippets: |
| 32 | + |
| 33 | +```python |
| 34 | +>>> from coincurve import PrivateKey |
| 35 | +>>> k1 = PrivateKey.from_int(3) |
| 36 | +>>> k2 = PrivateKey.from_int(2) |
| 37 | +>>> k1.public_key.format(False).hex() # 65 bytes, False means uncompressed key |
| 38 | +'04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672' |
| 39 | +>>> k2.public_key.format(False).hex() # 65 bytes |
| 40 | +'04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a' |
| 41 | +>>> k1.ecdh(k2.public_key.format()).hex() |
| 42 | +'c7d9ba2fa1496c81be20038e5c608f2fd5d0246d8643783730df6c2bbb855cb2' |
| 43 | +>>> k2.ecdh(k1.public_key.format()).hex() |
| 44 | +'c7d9ba2fa1496c81be20038e5c608f2fd5d0246d8643783730df6c2bbb855cb2' |
| 45 | +``` |
| 46 | + |
| 47 | +### Calculate your ecdh key manually |
| 48 | + |
| 49 | +However, as a hacker like you with strong desire to learn something, you must be curious about the magic under the ground. |
| 50 | + |
| 51 | +In one sentence, the `secp256k1`'s ECDH key of `k1` and `k2` is nothing but `sha256(k2.public_key.multiply(k1))`. |
| 52 | + |
| 53 | +```python |
| 54 | +>>> k1.to_int() |
| 55 | +3 |
| 56 | +>>> shared_pub = k2.public_key.multiply(k1.secret) |
| 57 | +>>> shared_pub.point() |
| 58 | +(115780575977492633039504758427830329241728645270042306223540962614150928364886, |
| 59 | + 78735063515800386211891312544505775871260717697865196436804966483607426560663) |
| 60 | +>>> import hashlib |
| 61 | +>>> h = hashlib.sha256() |
| 62 | +>>> h.update(shared_pub.format()) |
| 63 | +>>> h.hexdigest() # here you got the ecdh key same as above! |
| 64 | +'c7d9ba2fa1496c81be20038e5c608f2fd5d0246d8643783730df6c2bbb855cb2' |
| 65 | +``` |
| 66 | + |
| 67 | +> Warning: **NEVER** use small integers as private keys on any production systems or storing any valuable assets. |
| 68 | +> |
| 69 | +> Warning: **ALWAYS** use safe methods like [`os.urandom`](https://docs.python.org/3/library/os.html#os.urandom) to generate private keys. |
| 70 | +
|
| 71 | +### Math on ecdh |
| 72 | + |
| 73 | +Let's discuss in details. The word _multiply_ here means multiplying a **point** of a public key on elliptic curve (like `(x, y)`) with a **scalar** (like `k`). Here `k` is the integer format of a private key, for instance, it can be `3` for `k1` here, and `(x, y)` here is an extremely large number pair like `(115780575977492633039504758427830329241728645270042306223540962614150928364886, 78735063515800386211891312544505775871260717697865196436804966483607426560663)`. |
| 74 | + |
| 75 | +> Warning: 1 \* (x, y) == (x, y) is always true, since 1 is the **identity element** for multiplication. If you take integer 1 as a private key, the public key will be the [base point](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Signature_generation_algorithm). |
| 76 | +
|
| 77 | +Mathematically, the elliptic curve cryptography is based on the fact that you can easily multiply point `A` (aka base point, or public key in ECDH) and scalar `k` (aka private key) to get another point `B` (aka public key), but it's almost impossible to calculate `A` from `B` reversely (which means it's a "one-way function"). |
| 78 | + |
| 79 | +### Compressed and uncompressed keys |
| 80 | + |
| 81 | +A point multiplying a scalar can be regarded that this point adds itself multiple times, and the point `B` can be converted to a readable public key in a compressed or uncompressed format. |
| 82 | + |
| 83 | +- Compressed format (`x` coordinate only) |
| 84 | + |
| 85 | +```python |
| 86 | +>>> point = (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930) |
| 87 | +>>> point == k2.public_key.point() |
| 88 | +True |
| 89 | +>>> prefix = '02' if point[1] % 2 == 0 else '03' |
| 90 | +>>> compressed_key_hex = prefix + hex(point[0])[2:] |
| 91 | +>>> compressed_key = bytes.fromhex(compressed_key_hex) |
| 92 | +>>> compressed_key.hex() |
| 93 | +'02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5' |
| 94 | +``` |
| 95 | + |
| 96 | +- Uncompressed format (`(x, y)` coordinate) |
| 97 | + |
| 98 | +```python |
| 99 | +>>> uncompressed_key_hex = '04' + hex(point[0])[2:] + hex(point[1])[2:] |
| 100 | +>>> uncompressed_key = bytes.fromhex(uncompressed_key_hex) |
| 101 | +>>> uncompressed_key.hex() |
| 102 | +'04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a' |
| 103 | +``` |
| 104 | + |
| 105 | +The format is depicted by the image below from the [bitcoin book](https://github.com/bitcoinbook/bitcoinbook). |
| 106 | + |
| 107 | + |
| 108 | + |
| 109 | +> If you want to convert the compressed format to uncompressed, basically, you need to calculate `y` from `x` by solving the equation using [Cipolla's Algorithm](https://en.wikipedia.org/wiki/Cipolla's_algorithm): |
| 110 | +> |
| 111 | +> %20%5Cbmod%20p%2C%5C%20where%5C%20p%3D2%5E%7B256%7D-2%5E%7B32%7D-2%5E%7B9%7D-2%5E%7B8%7D-2%5E%7B7%7D-2%5E%7B6%7D-2%5E%7B4%7D-1%20>) |
| 112 | +> |
| 113 | +> You can check the [bitcoin wiki](https://en.bitcoin.it/wiki/Secp256k1) and this thread on [bitcointalk.org](https://bitcointalk.org/index.php?topic=644919.msg7205689#msg7205689) for more details. |
| 114 | +
|
| 115 | +Then, the shared key between `k1` and `k2` is the `sha256` hash of the **compressed** ECDH public key. It's better to use the compressed format, since you can always get `x` from `x` or `(x, y)` without any calculation. |
| 116 | + |
| 117 | +You may want to ask, what if we don't hash it? Briefly, hash can: |
| 118 | + |
| 119 | +1. Make the shared key's length fixed; |
| 120 | +2. Make it safer since hash functions can remove "weak bits" in the original computed key. Check the introduction section of this [paper](http://cacr.uwaterloo.ca/techreports/1998/corr98-05.pdf) for more details. |
| 121 | + |
| 122 | +> Warning: According to some recent research, although widely used, the `sha256` key derivation function is [not secure enough](https://github.com/ecies/py/issues/82). |
| 123 | +
|
| 124 | +## AES |
| 125 | + |
| 126 | +Now we have the shared key, and we can use the `nonce` and `tag` to decrypt. This is quite straight, and the example derives from `pycryptodome`'s [documentation](https://pycryptodome.readthedocs.io/en/latest/src/examples.html#encrypt-data-with-aes). |
| 127 | + |
| 128 | +```python |
| 129 | +>>> from Crypto.Cipher import AES |
| 130 | +>>> key = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
| 131 | +>>> nonce = b'\xf3\xe1\xba\x81\r,\x89\x00\xb1\x13\x12\xb7\xc7%V_' |
| 132 | +>>> tag = b'\xec;q\xe1|\x11\xdb\xe3\x14\x84\xda\x94P\xed\xcfl' |
| 133 | +>>> data = b'\x02\xd2\xff\xed\x93\xb8V\xf1H\xb9' |
| 134 | +>>> decipher = AES.new(key, AES.MODE_GCM, nonce=nonce) |
| 135 | +>>> decipher.decrypt_and_verify(data, tag) |
| 136 | +b'helloworld' |
| 137 | +``` |
| 138 | + |
| 139 | +> Strictly speaking, `nonce` != `iv`, but this is a little bit off topic, if you are curious, you can check [the comment in `utils/symmetric.py`](./ecies/utils/symmetric.py#L79). |
| 140 | +> |
| 141 | +> Warning: it's dangerous to reuse nonce, if you don't know what you are doing, just follow the default setting. |
0 commit comments