Skip to content

Commit 0522d26

Browse files
authored
Merge pull request #24 from ZhaoQi99/feat/add-license
support encrypt with license
2 parents 16349d3 + a2ba940 commit 0522d26

File tree

9 files changed

+613
-153
lines changed

9 files changed

+613
-153
lines changed

README.md

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# pyencrypt-pye
2+
23
---
4+
35
encrypt python source code and import module dynamically.
46

57
```
@@ -16,10 +18,12 @@ encrypt python source code and import module dynamically.
1618
```
1719

1820
## Install
21+
1922
```bash
2023
pip install git+https://github.com/ZhaoQi99/pyencrypt-pye.git
2124
✨🍰✨
2225
```
26+
2327
## Usage
2428

2529
```shell
@@ -33,23 +37,45 @@ Commands:
3337
decrypt Decrypt encrypted pye file
3438
encrypt Encrypt your python code
3539
generate Generate loader file using specified key
40+
license Generate license file using specified key
3641
```
3742

3843
### Encrypt
44+
3945
```shell
4046
~$ pyencrypt encrypt -h
4147
Usage: pyencrypt encrypt [OPTIONS] PATHNAME
4248

4349
Encrypt your python code
4450

4551
Options:
46-
-i, --in-place make changes to files in place
47-
-k, --key TEXT Your encryption key.If you don‘t specify key, pyencrypt will
48-
generate encryption key randomly.
49-
--yes Automatically answer yes for confirm questions.
50-
-h, --help Show this message and exit.
52+
-i, --in-place make changes to files in place
53+
-k, --key TEXT Your encryption key.If you don‘t specify
54+
key, pyencrypt will generate encryption key
55+
randomly.
56+
--with-license Add license to encrypted file
57+
-m, --bind-mac TEXT Bind mac address to encrypted file
58+
-4, --bind-ipv4 TEXT Bind ipv4 address to encrypted file
59+
-b, --before [%Y-%m-%dT%H:%M:%S %z|%Y-%m-%d %H:%M:%S|%Y-%m-%d]
60+
License is invalid before this date.
61+
-a, --after [%Y-%m-%dT%H:%M:%S %z|%Y-%m-%d %H:%M:%S|%Y-%m-%d]
62+
License is invalid after this date.
63+
-y, --yes Automatically answer yes for confirm
64+
questions.
65+
-h, --help Show this message and exit.
66+
```
67+
68+
### Entry File
69+
70+
In your entry file, you must import `loader` firstly, and then you can import encrypted modules as usual.
71+
72+
```python
73+
import loader
74+
from test import *
5175
```
76+
5277
### Decrypt
78+
5379
```shell
5480
~$ pyencrypt decrypt -h
5581
Usage: pyencrypt decrypt [OPTIONS] PATHNAME
@@ -61,6 +87,7 @@ Options:
6187
-k, --key TEXT Your encryption key. [required]
6288
-h, --help Show this message and exit.
6389
```
90+
6491
### Generate
6592

6693
```shell
@@ -74,26 +101,63 @@ Options:
74101
-h, --help Show this message and exit.
75102
```
76103

104+
### License
105+
106+
pyencrypt's loader will search for the license file in the following manner:
107+
108+
1. `~/.licenses/license.lic` file in your home directory.
109+
110+
2. `licenses/license.lic` file in `loader` file's directory.
111+
112+
3. `licenses/license.lic` file in the current working directory.
113+
114+
```shell
115+
~$ pyencrypt license -h
116+
Usage: pyencrypt license [OPTIONS]
117+
118+
Generate license file using specified key
119+
120+
Options:
121+
-h, --help Show this message and exit.
122+
-k, --key TEXT Your encryption key. [required]
123+
-m, --bind-mac TEXT Your mac address.
124+
-4, --bind-ipv4 TEXT Your ipv4 address.
125+
-b, --before [%Y-%m-%dT%H:%M:%S %z|%Y-%m-%d %H:%M:%S|%Y-%m-%d]
126+
License is invalid before this date.
127+
-a, --after [%Y-%m-%dT%H:%M:%S %z|%Y-%m-%d %H:%M:%S|%Y-%m-%d]
128+
License is invalid after this date.
129+
```
130+
77131
## Example
132+
78133
### Encrypt
134+
79135
```shell
80136
~$ pyencrypt encrypt --in-place -y test.py
81137
~$ pyencrypt encrypt test/
138+
~$ pyencrypt encrypt test.py -y --with-license\
139+
--before="2000-01-01T00:00:00 +0800" --after="2030-01-01T00:00:00 +0800"\
140+
--bind-mac="AC:DE:48:00:11:22" --bind-ipv4="192.168.0.1"
82141
```
83142

84143
### Decrypt
144+
85145
```shell
86146
~$ pyencrypt decrypt -k xxx test.pye
87147
```
148+
88149
### Generate
150+
89151
```shell
90152
~$ pyencrypt generate -k xxx
91153
```
92154

93-
### Entry File
94-
```python
95-
import loader
96-
from test import *
155+
### License
156+
157+
```shell
158+
~$ pyencrypt license -k xxx\
159+
--before="2000-01-01T00:00:00 +0800" --after="2030-01-01T00:00:00 +0800"\
160+
--bind-mac="AC:DE:48:00:11:22" --bind-ipv4="192.168.0.1"
97161
```
98162

99163
## Development
@@ -104,7 +168,11 @@ from test import *
104168
yapf --recursive -i pyencrypt
105169
isort pyencrypt
106170
```
171+
107172
## License
173+
108174
[GNU General Public License v3.0](https://github.com/ZhaoQi99/pyencrypt-pye/blob/main/LICENSE)
175+
109176
## Author
177+
110178

pyencrypt/cli.py

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from pathlib import Path
66

77
import click
8+
89
from pyencrypt import __description__, __version__
910
from pyencrypt.decrypt import decrypt_file
10-
from pyencrypt.encrypt import (can_encrypt, encrypt_file, encrypt_key,
11-
generate_so_file)
11+
from pyencrypt.encrypt import (can_encrypt, encrypt_file, encrypt_key, generate_so_file)
1212
from pyencrypt.generate import generate_aes_key
13+
from pyencrypt.license import generate_license_file, MIN_DATETIME, MAX_DATETIME
1314

1415
VERSION = f"""\
1516
_
@@ -33,16 +34,18 @@
3334
PYTHON_MAJOR, PYTHON_MINOR = sys.version_info[:2]
3435
LAODER_FILE_NAME = click.style(
3536
"encrypted/loader.cpython-{major}{minor}{abi}-{platform}.so".format(
36-
major=PYTHON_MAJOR,
37-
minor=PYTHON_MINOR,
38-
abi=sys.abiflags,
39-
platform=sys.platform),
37+
major=PYTHON_MAJOR, minor=PYTHON_MINOR, abi=sys.abiflags, platform=sys.platform
38+
),
4039
blink=True,
41-
fg='blue')
40+
fg='blue'
41+
)
42+
LICENSE_FILE_NAME = click.style("license.lic", blink=True, fg='blue')
4243

4344
SUCCESS_ANSI = click.style('successfully', fg='green')
4445
INVALID_KEY_MSG = click.style('Your encryption key is invalid.', fg='red')
4546

47+
INVALID_EXPIRED_MSG = 'Expired before date must be less than expired after date.'
48+
4649
FINISH_ENCRYPT_MSG = f"""
4750
Encryption completed {SUCCESS_ANSI}.
4851
Please copy {LAODER_FILE_NAME} into your encrypted directory.
@@ -54,10 +57,16 @@
5457
Decryption completed {SUCCESS_ANSI}. Your origin source code has be put: %s
5558
"""
5659

57-
FINISH_GENERATE_MSG = f"""
60+
FINISH_GENERATE_LOADER_MSG = f"""
5861
Generate loader file {SUCCESS_ANSI}. Your loader file is located in {LAODER_FILE_NAME}
5962
"""
6063

64+
FINISH_GENERATE_LICENSE_MSG = f"""
65+
Generate license file {SUCCESS_ANSI}. Your license file is located in {LICENSE_FILE_NAME}
66+
"""
67+
68+
DATETIME_FORMATS = ['%Y-%m-%dT%H:%M:%S %z', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d']
69+
6170

6271
def _check_key(key: str) -> bool:
6372
return not (len(key) % 4 or len(base64.b64decode(key)) % 16)
@@ -72,33 +81,26 @@ def cli():
7281

7382
@cli.command(name='encrypt')
7483
@click.argument('pathname', type=click.Path(exists=True, resolve_path=True))
75-
@click.option('-i',
76-
'--in-place',
77-
'replace',
78-
default=False,
79-
help='make changes to files in place',
80-
is_flag=True)
81-
@click.option('-k',
82-
'--key',
83-
default=None,
84-
help=KEY_OPTION_HELP,
85-
type=click.STRING)
86-
@click.confirmation_option(
87-
'-y',
88-
'--yes',
89-
prompt='Are you sure you want to encrypt your python file?',
90-
help='Automatically answer yes for confirm questions.')
84+
@click.option('-i', '--in-place', 'replace', default=False, help='make changes to files in place', is_flag=True)
85+
@click.option('-k', '--key', default=None, help=KEY_OPTION_HELP, type=click.STRING)
86+
@click.option('--with-license', default=False, help='Add license to encrypted file', is_flag=True)
87+
@click.option('-m', '--bind-mac', 'mac', default=None, help='Bind mac address to encrypted file', type=click.STRING)
88+
@click.option('-4', '--bind-ipv4', 'ipv4', default=None, help='Bind ipv4 address to encrypted file', type=click.STRING)
89+
@click.option('-b', '--before', default=MAX_DATETIME, help='License is invalid before this date.', type=click.DateTime(formats=DATETIME_FORMATS))
90+
@click.option('-a', '--after', default=MIN_DATETIME, help='License is invalid after this date.', type=click.DateTime(formats=DATETIME_FORMATS))
91+
@click.confirmation_option('-y', '--yes', prompt='Are you sure you want to encrypt your python file?', help='Automatically answer yes for confirm questions.')
9192
@click.help_option('-h', '--help')
9293
@click.pass_context
93-
def encrypt_command(ctx, pathname, replace, key):
94+
def encrypt_command(ctx, pathname, replace, key, with_license, mac, ipv4, before, after):
9495
"""Encrypt your python code"""
9596
if key is not None and not _check_key(key):
9697
ctx.fail(INVALID_KEY_MSG)
9798
if key is None:
9899
key = generate_aes_key().decode()
99-
click.echo(
100-
f'Your randomly encryption 🔑 is {click.style(key,underline=True, fg="yellow")}'
101-
)
100+
click.echo(f'Your randomly encryption 🔑 is {click.style(key,underline=True, fg="yellow")}')
101+
102+
if before > after:
103+
ctx.fail(INVALID_EXPIRED_MSG)
102104

103105
path = Path(pathname)
104106

@@ -126,22 +128,16 @@ def encrypt_command(ctx, pathname, replace, key):
126128

127129
cipher_key, d, n = encrypt_key(key.encode()) # 需要放进导入器中
128130
generate_so_file(cipher_key, d, n)
131+
if with_license is True:
132+
generate_license_file(key, Path(os.getcwd()), after, before, mac, ipv4)
133+
ctx.echo(FINISH_GENERATE_LICENSE_MSG)
129134
click.echo(FINISH_ENCRYPT_MSG)
130135

131136

132137
@cli.command(name='decrypt')
133138
@click.argument('pathname', type=click.Path(exists=True, resolve_path=True))
134-
@click.option('-i',
135-
'--in-place',
136-
'replace',
137-
default=False,
138-
help='make changes to files in place',
139-
is_flag=True)
140-
@click.option('-k',
141-
'--key',
142-
required=True,
143-
help='Your encryption key.',
144-
type=click.STRING)
139+
@click.option('-i', '--in-place', 'replace', default=False, help='make changes to files in place', is_flag=True)
140+
@click.option('-k', '--key', required=True, help='Your encryption key.', type=click.STRING)
145141
@click.help_option('-h', '--help')
146142
@click.pass_context
147143
def decrypt_command(ctx, pathname, replace, key):
@@ -177,20 +173,35 @@ def decrypt_command(ctx, pathname, replace, key):
177173

178174

179175
@cli.command(name='generate')
180-
@click.option('-k',
181-
'--key',
182-
required=True,
183-
help='Your encryption key.',
184-
type=click.STRING)
176+
@click.option('-k', '--key', required=True, help='Your encryption key.', type=click.STRING)
185177
@click.help_option('-h', '--help')
186178
@click.pass_context
187179
def generate_loader(ctx, key):
188180
"""Generate loader file using specified key"""
189181
if not _check_key(key):
190182
ctx.fail(INVALID_KEY_MSG)
191183
cipher_key, d, n = encrypt_key(key.encode())
192-
generate_so_file(cipher_key, d, n)
193-
click.echo(FINISH_GENERATE_MSG)
184+
generate_so_file(cipher_key, d, n, Path(os.getcwd()))
185+
click.echo(FINISH_GENERATE_LOADER_MSG)
186+
187+
188+
@cli.command(name='license')
189+
@click.help_option('-h', '--help')
190+
@click.option('-k', '--key', required=True, help='Your encryption key.', type=click.STRING)
191+
@click.option('-m', '--bind-mac', help='Your mac address.', type=click.STRING)
192+
@click.option('-4', '--bind-ipv4', help='Your ipv4 address.', type=click.STRING)
193+
@click.option('-b', '--before', default=MAX_DATETIME, help='License is invalid before this date.', type=click.DateTime(formats=DATETIME_FORMATS))
194+
@click.option('-a', '--after', default=MIN_DATETIME, help='License is invalid after this date.', type=click.DateTime(formats=DATETIME_FORMATS))
195+
@click.pass_context
196+
def generate_license(ctx, key, mac, ipv4, before, after):
197+
"""Generate license file using specified key"""
198+
if not _check_key(key):
199+
ctx.fail(INVALID_KEY_MSG)
200+
if before > after:
201+
ctx.fail(INVALID_EXPIRED_MSG)
202+
203+
generate_license_file(key, Path(os.getcwd()), after, before, mac, ipv4)
204+
click.echo(FINISH_GENERATE_LICENSE_MSG)
194205

195206

196207
if __name__ == '__main__':

pyencrypt/encrypt.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import subprocess
34
from pathlib import Path
45

@@ -10,6 +11,8 @@
1011
'__init__.py',
1112
]
1213

14+
REMOVE_SELF_IMPORT = re.compile(r'^from pyencrypt\.[\s\S]*?$', re.MULTILINE)
15+
1316

1417
def _encrypt_file(
1518
data: bytes,
@@ -44,23 +47,22 @@ def encrypt_key(key: bytes) -> str:
4447
return 'O'.join(map(str, cipher_ls)), numbers['d'], numbers['n']
4548

4649

47-
def generate_so_file(cipher_key: str, d: int, n: int, base_dir: Path = None):
50+
def generate_so_file(cipher_key: str, d: int, n: int, base_dir: Path = None, license: bool = False) -> Path:
4851
private_key = f'{n}O{d}'
4952
path = Path(os.path.abspath(__file__)).parent
5053

5154
decrypt_source_ls = list()
52-
need_import_files = ['ntt.py', 'aes.py', 'decrypt.py']
55+
need_import_files = ['ntt.py', 'aes.py', 'decrypt.py', 'license.py']
5356
for file in need_import_files:
5457
file_path = path / file
55-
decrypt_source_ls.append(
56-
file_path.read_text().replace('from pyencrypt.ntt import intt',
57-
'').replace('from pyencrypt.aes import aes_decrypt', '')
58-
)
58+
decrypt_source_ls.append(REMOVE_SELF_IMPORT.sub('',file_path.read_text()))
5959

6060
loader_source_path = path / 'loader.py'
61-
loader_source = loader_source_path.read_text().replace(
61+
loader_source = REMOVE_SELF_IMPORT.sub('', loader_source_path.read_text()).replace(
6262
"__private_key = ''", f"__private_key = '{private_key}'", 1
63-
).replace("__cipher_key = ''", f"__cipher_key = '{cipher_key}'", 1).replace("from pyencrypt.decrypt import *", '')
63+
).replace("__cipher_key = ''", f"__cipher_key = '{cipher_key}'", 1).replace(
64+
'license = None', f'license = {license}', 1
65+
)
6466

6567
if base_dir is None:
6668
base_dir = Path(os.getcwd())
@@ -100,9 +102,7 @@ def generate_so_file(cipher_key: str, d: int, n: int, base_dir: Path = None):
100102
script_args=['build_ext', '--build-lib', temp_dir.as_posix()],
101103
cmdclass={'build_ext': build_ext},
102104
)
103-
104-
return True
105-
105+
return list(temp_dir.glob('loader.cpython-*-*.so'))[0].absolute()
106106

107107
def encrypt_file(path: Path, key: str, delete_origin: bool = False, new_path: Path = None):
108108
if not can_encrypt(path):

0 commit comments

Comments
 (0)