Skip to content

Commit 15d5dc9

Browse files
committed
Add configuration
1 parent ef79c90 commit 15d5dc9

21 files changed

+912
-576
lines changed

.coveragerc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[run]
2-
include =
3-
ecies/__init__.py
4-
ecies/utils.py
52
omit =
63
ecies/__main__.py
4+
[report]
5+
exclude_also =
6+
raise NotImplementedError

.cspell.jsonc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"words": [
3+
"coincurve",
4+
"dataclass",
5+
"ecdh",
6+
"ecies",
7+
"eciespy",
8+
"helloworld",
9+
"hkdf",
10+
"pycryptodome",
11+
"pycryptodome's",
12+
"readablize",
13+
"secp",
14+
"urandom",
15+
"xchacha"
16+
],
17+
"ignorePaths": [".cspell.jsonc", "LICENSE"]
18+
}

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
matrix:
1515
os: [ubuntu-latest, macos-latest, windows-latest]
16-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
16+
python-version: ["3.8", "3.9", "3.10", "3.11"] # "3.12-dev"
1717
steps:
1818
- uses: actions/checkout@v3
1919
- uses: actions/setup-python@v4
@@ -31,7 +31,10 @@ jobs:
3131
- run: brew install automake
3232
if: matrix.os == 'macos-latest'
3333

34-
- run: poetry install --only main
34+
- run: poetry install
35+
36+
- name: Run test
37+
run: poetry run pytest -s --cov=ecies tests --cov-report xml
3538

3639
- run: ./scripts/ci.sh
3740

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Release Notes
2+
3+
## 0.4.0
4+
5+
- Drop Python 3.7
6+
- Revamp documentation
7+
- Add configuration and XChacha20 as an optional encryption backend
8+
9+
## 0.3.1 ~ 0.3.13
10+
11+
- Support Python 3.8, 3.9, 3.10, 3.11
12+
- Drop Python 3.5, 3.6
13+
- Bump dependencies
14+
- Update documentation
15+
16+
## 0.3.0
17+
18+
- API change: use `HKDF-sha256` to derive shared keys instead of `sha256`
19+
20+
## 0.2.0
21+
22+
- API change: `ecies.encrypt` and `ecies.decrypt` now can take both hex `str` and raw `bytes`
23+
- Bump dependencies
24+
- Update documentation
25+
26+
## 0.1.1 ~ 0.1.9
27+
28+
- Bump dependencies
29+
- Update documentation
30+
- Switch to Circle CI
31+
- Change license to MIT
32+
33+
## 0.1.0
34+
35+
- First beta version release

DETAILS.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
![EC public key format](https://raw.githubusercontent.com/bitcoinbook/bitcoinbook/develop/images/mbc2_0407.png)
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+
> ![y^2=(x^3 + 7) mod p, where p=2^{256}-2^{32}-2^{9}-2^{8}-2^{7}-2^{6}-2^{4}-1](<https://tex.s2cms.ru/svg/%20y%5E2%3D(x%5E3%20%2B%207)%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.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2018-2021 Weiliang Li
3+
Copyright (c) 2018-2023 Weiliang Li
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

0 commit comments

Comments
 (0)