inferlock 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferlock-0.4.0/PKG-INFO +193 -0
- inferlock-0.4.0/README.md +185 -0
- inferlock-0.4.0/inferlock/__init__.py +7 -0
- inferlock-0.4.0/inferlock/cli.py +142 -0
- inferlock-0.4.0/inferlock/crypto.py +61 -0
- inferlock-0.4.0/inferlock/format.py +31 -0
- inferlock-0.4.0/inferlock/license.py +199 -0
- inferlock-0.4.0/inferlock/loader.py +67 -0
- inferlock-0.4.0/inferlock.egg-info/PKG-INFO +193 -0
- inferlock-0.4.0/inferlock.egg-info/SOURCES.txt +14 -0
- inferlock-0.4.0/inferlock.egg-info/dependency_links.txt +1 -0
- inferlock-0.4.0/inferlock.egg-info/entry_points.txt +2 -0
- inferlock-0.4.0/inferlock.egg-info/requires.txt +1 -0
- inferlock-0.4.0/inferlock.egg-info/top_level.txt +4 -0
- inferlock-0.4.0/pyproject.toml +19 -0
- inferlock-0.4.0/setup.cfg +4 -0
inferlock-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inferlock
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: File encryption and licensing for AI models
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography
|
|
8
|
+
|
|
9
|
+
# inferlock
|
|
10
|
+
|
|
11
|
+
Protect AI model files with encryption and license-based runtime loading.
|
|
12
|
+
|
|
13
|
+
inferlock encrypts model files and loads them securely with expiration-based licenses.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
* Model file encryption
|
|
20
|
+
* Runtime in-memory decryption
|
|
21
|
+
* License-based loading
|
|
22
|
+
* Expiration-based license
|
|
23
|
+
* Single-command packaging
|
|
24
|
+
* Bundle-based distribution
|
|
25
|
+
* CLI-first design
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install inferlock
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or local dev:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
Encrypt model and create license:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
inferlock encrypt model.pt --expire 30d
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
model.inferlock/
|
|
55
|
+
├── model.inferlock
|
|
56
|
+
└── license.inferlock
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Load model:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from inferlock import load
|
|
63
|
+
|
|
64
|
+
model = load("model.inferlock")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## CLI Usage
|
|
70
|
+
|
|
71
|
+
Encrypt model:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
inferlock encrypt model.pt --expire 30d
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Generate license manually:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
inferlock license model.key --expire 30d
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Show bundle info:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
inferlock info model.inferlock
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Device Binding
|
|
92
|
+
|
|
93
|
+
Bind license to machine:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
inferlock encrypt model.pt --expire 30d --bind-device
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The license will be tied to the current machine's MAC address and cannot be used on other devices.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Bundle Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
model.inferlock/
|
|
107
|
+
├── model.inferlock
|
|
108
|
+
└── license.inferlock
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Python API
|
|
114
|
+
|
|
115
|
+
Load bundle:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from inferlock import load
|
|
119
|
+
|
|
120
|
+
model = load("model.inferlock")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Load manually:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
load("model.inferlock", "license.inferlock")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## License Format
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"key": "...",
|
|
136
|
+
"expire": "YYYY-MM-DD",
|
|
137
|
+
"version": 1
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Version
|
|
142
|
+
|
|
143
|
+
### v0.2.0
|
|
144
|
+
|
|
145
|
+
* license system
|
|
146
|
+
* expiration support
|
|
147
|
+
* bundle packaging
|
|
148
|
+
* single command encrypt + license
|
|
149
|
+
* auto bundle loader
|
|
150
|
+
|
|
151
|
+
### v0.3.0
|
|
152
|
+
|
|
153
|
+
* device binding (MAC)
|
|
154
|
+
* per-device license
|
|
155
|
+
|
|
156
|
+
### v0.4.0
|
|
157
|
+
|
|
158
|
+
* signed license
|
|
159
|
+
* tamper protection
|
|
160
|
+
|
|
161
|
+
### v0.5.0
|
|
162
|
+
|
|
163
|
+
* encrypted license
|
|
164
|
+
* hidden key
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Roadmap
|
|
169
|
+
|
|
170
|
+
Future features:
|
|
171
|
+
|
|
172
|
+
* multi-device license
|
|
173
|
+
* trial license
|
|
174
|
+
* offline activation
|
|
175
|
+
* hardware fingerprint
|
|
176
|
+
* bulk license generation
|
|
177
|
+
* license revoke
|
|
178
|
+
* metadata bundle
|
|
179
|
+
* verify command
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Example
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
inferlock encrypt yolov7.weights --expire 7d
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Load:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
load("yolov7.inferlock")
|
|
193
|
+
```
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# inferlock
|
|
2
|
+
|
|
3
|
+
Protect AI model files with encryption and license-based runtime loading.
|
|
4
|
+
|
|
5
|
+
inferlock encrypts model files and loads them securely with expiration-based licenses.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* Model file encryption
|
|
12
|
+
* Runtime in-memory decryption
|
|
13
|
+
* License-based loading
|
|
14
|
+
* Expiration-based license
|
|
15
|
+
* Single-command packaging
|
|
16
|
+
* Bundle-based distribution
|
|
17
|
+
* CLI-first design
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install inferlock
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or local dev:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Encrypt model and create license:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
inferlock encrypt model.pt --expire 30d
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Output:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
model.inferlock/
|
|
47
|
+
├── model.inferlock
|
|
48
|
+
└── license.inferlock
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Load model:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from inferlock import load
|
|
55
|
+
|
|
56
|
+
model = load("model.inferlock")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## CLI Usage
|
|
62
|
+
|
|
63
|
+
Encrypt model:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
inferlock encrypt model.pt --expire 30d
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Generate license manually:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
inferlock license model.key --expire 30d
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Show bundle info:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
inferlock info model.inferlock
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Device Binding
|
|
84
|
+
|
|
85
|
+
Bind license to machine:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
inferlock encrypt model.pt --expire 30d --bind-device
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The license will be tied to the current machine's MAC address and cannot be used on other devices.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Bundle Structure
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
model.inferlock/
|
|
99
|
+
├── model.inferlock
|
|
100
|
+
└── license.inferlock
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Python API
|
|
106
|
+
|
|
107
|
+
Load bundle:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from inferlock import load
|
|
111
|
+
|
|
112
|
+
model = load("model.inferlock")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Load manually:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
load("model.inferlock", "license.inferlock")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## License Format
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"key": "...",
|
|
128
|
+
"expire": "YYYY-MM-DD",
|
|
129
|
+
"version": 1
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Version
|
|
134
|
+
|
|
135
|
+
### v0.2.0
|
|
136
|
+
|
|
137
|
+
* license system
|
|
138
|
+
* expiration support
|
|
139
|
+
* bundle packaging
|
|
140
|
+
* single command encrypt + license
|
|
141
|
+
* auto bundle loader
|
|
142
|
+
|
|
143
|
+
### v0.3.0
|
|
144
|
+
|
|
145
|
+
* device binding (MAC)
|
|
146
|
+
* per-device license
|
|
147
|
+
|
|
148
|
+
### v0.4.0
|
|
149
|
+
|
|
150
|
+
* signed license
|
|
151
|
+
* tamper protection
|
|
152
|
+
|
|
153
|
+
### v0.5.0
|
|
154
|
+
|
|
155
|
+
* encrypted license
|
|
156
|
+
* hidden key
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Roadmap
|
|
161
|
+
|
|
162
|
+
Future features:
|
|
163
|
+
|
|
164
|
+
* multi-device license
|
|
165
|
+
* trial license
|
|
166
|
+
* offline activation
|
|
167
|
+
* hardware fingerprint
|
|
168
|
+
* bulk license generation
|
|
169
|
+
* license revoke
|
|
170
|
+
* metadata bundle
|
|
171
|
+
* verify command
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Example
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
inferlock encrypt yolov7.weights --expire 7d
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Load:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
load("yolov7.inferlock")
|
|
185
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from .crypto import encrypt_bytes
|
|
9
|
+
from .format import pack_file, unpack_file
|
|
10
|
+
from .license import create_license as create_license_bytes, parse_license, get_device_id
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_expire(expire: str) -> datetime.datetime:
|
|
14
|
+
"""Parse expiration string to datetime."""
|
|
15
|
+
if expire.endswith('d'):
|
|
16
|
+
days = int(expire[:-1])
|
|
17
|
+
return datetime.datetime.now() + datetime.timedelta(days=days)
|
|
18
|
+
try:
|
|
19
|
+
return datetime.datetime.strptime(expire, "%Y-%m-%d")
|
|
20
|
+
except ValueError:
|
|
21
|
+
raise argparse.ArgumentTypeError(f"Invalid expire format: {expire}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_encrypt(args):
|
|
25
|
+
with open(args.input, 'rb') as f:
|
|
26
|
+
data = f.read()
|
|
27
|
+
|
|
28
|
+
# Generate random 32-byte key and base64 encode
|
|
29
|
+
key_bytes = os.urandom(32)
|
|
30
|
+
key_b64 = base64.b64encode(key_bytes).decode('utf-8')
|
|
31
|
+
|
|
32
|
+
# Encrypt file using the generated key
|
|
33
|
+
encrypted = encrypt_bytes(data, key_b64)
|
|
34
|
+
|
|
35
|
+
header = {
|
|
36
|
+
"format": "inferlock-v1",
|
|
37
|
+
"original_name": args.input,
|
|
38
|
+
}
|
|
39
|
+
packed = pack_file(header, encrypted)
|
|
40
|
+
|
|
41
|
+
# Determine bundle directory name
|
|
42
|
+
if args.output:
|
|
43
|
+
bundle_dir = args.output
|
|
44
|
+
else:
|
|
45
|
+
base_name = os.path.splitext(args.input)[0]
|
|
46
|
+
bundle_dir = f"{base_name}.inferlock"
|
|
47
|
+
|
|
48
|
+
# Ensure bundle_dir ends with .inferlock
|
|
49
|
+
if not bundle_dir.endswith(".inferlock"):
|
|
50
|
+
bundle_dir = f"{bundle_dir}.inferlock"
|
|
51
|
+
|
|
52
|
+
os.makedirs(bundle_dir, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Save encrypted model inside as "model.inferlock"
|
|
55
|
+
model_path = os.path.join(bundle_dir, "model.inferlock")
|
|
56
|
+
with open(model_path, 'wb') as f:
|
|
57
|
+
f.write(packed)
|
|
58
|
+
|
|
59
|
+
# Save license inside as "license.inferlock" (if expire provided)
|
|
60
|
+
if args.expire:
|
|
61
|
+
device = get_device_id() if args.bind_device else None
|
|
62
|
+
license_bytes = create_license_bytes(key_b64, args.expire, device)
|
|
63
|
+
license_path = os.path.join(bundle_dir, args.license_name)
|
|
64
|
+
with open(license_path, 'wb') as f:
|
|
65
|
+
f.write(license_bytes)
|
|
66
|
+
print(f"Encrypted: {args.input} -> {bundle_dir}/")
|
|
67
|
+
print(f"Files: model.inferlock, {args.license_name}")
|
|
68
|
+
else:
|
|
69
|
+
print(f"Encrypted: {args.input} -> {bundle_dir}/model.inferlock")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cmd_license(args):
|
|
73
|
+
with open(args.keyfile, 'r') as f:
|
|
74
|
+
key = f.read().strip()
|
|
75
|
+
|
|
76
|
+
device = get_device_id() if args.bind_device else None
|
|
77
|
+
license_bytes = create_license_bytes(key, args.expire, device)
|
|
78
|
+
|
|
79
|
+
output = args.output if args.output else "license.inferlock"
|
|
80
|
+
with open(output, 'wb') as f:
|
|
81
|
+
f.write(license_bytes)
|
|
82
|
+
|
|
83
|
+
print(f"License created: {output}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_bundle_info(bundle_path):
|
|
87
|
+
"""Print bundle information."""
|
|
88
|
+
print(f"Model bundle: {bundle_path}")
|
|
89
|
+
|
|
90
|
+
# Check if license.inferlock exists in bundle
|
|
91
|
+
license_path = os.path.join(bundle_path, "license.inferlock")
|
|
92
|
+
if os.path.exists(license_path):
|
|
93
|
+
with open(license_path, 'rb') as f:
|
|
94
|
+
license_data = f.read()
|
|
95
|
+
|
|
96
|
+
license_dict = parse_license(license_data)
|
|
97
|
+
|
|
98
|
+
print(f"Expire: {license_dict.get('expire', 'unknown')}")
|
|
99
|
+
print(f"Version: {license_dict.get('version', 'unknown')}")
|
|
100
|
+
|
|
101
|
+
if "device" in license_dict:
|
|
102
|
+
print("Device bound: yes")
|
|
103
|
+
else:
|
|
104
|
+
print("Device bound: no")
|
|
105
|
+
else:
|
|
106
|
+
print("No license found")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_info(args):
|
|
110
|
+
print_bundle_info(args.input)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
parser = argparse.ArgumentParser(description="InferLock CLI")
|
|
115
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
116
|
+
|
|
117
|
+
encrypt_parser = subparsers.add_parser('encrypt', help='Encrypt a file')
|
|
118
|
+
encrypt_parser.add_argument('input', help='Input file')
|
|
119
|
+
encrypt_parser.add_argument('--expire', help='Expiration (30d, 7d, 2026-12-01)')
|
|
120
|
+
encrypt_parser.add_argument('-o', '--output', help='Output file')
|
|
121
|
+
encrypt_parser.add_argument('--password', help='Password')
|
|
122
|
+
encrypt_parser.add_argument('--bind-device', action='store_true', help='Bind license to device')
|
|
123
|
+
encrypt_parser.add_argument('--license-name', default='license.inferlock', help='License filename inside bundle')
|
|
124
|
+
encrypt_parser.set_defaults(func=cmd_encrypt)
|
|
125
|
+
|
|
126
|
+
license_parser = subparsers.add_parser('license', help='Create license')
|
|
127
|
+
license_parser.add_argument('keyfile', help='Key file (.key)')
|
|
128
|
+
license_parser.add_argument('--expire', required=True, help='Expiration (30d, 7d, 2026-12-01)')
|
|
129
|
+
license_parser.add_argument('-o', '--output', help='Output file')
|
|
130
|
+
license_parser.add_argument('--bind-device', action='store_true', help='Bind license to device')
|
|
131
|
+
license_parser.set_defaults(func=cmd_license)
|
|
132
|
+
|
|
133
|
+
info_parser = subparsers.add_parser('info', help='Show file info')
|
|
134
|
+
info_parser.add_argument('input', help='Input .inferlock file')
|
|
135
|
+
info_parser.set_defaults(func=cmd_info)
|
|
136
|
+
|
|
137
|
+
args = parser.parse_args()
|
|
138
|
+
args.func(args)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
5
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
|
7
|
+
from cryptography.hazmat.backends import default_backend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Constants
|
|
11
|
+
KEY_SIZE = 32 # 256 bits
|
|
12
|
+
SALT_SIZE = 16
|
|
13
|
+
NONCE_SIZE = 12 # 96 bits for GCM
|
|
14
|
+
ITERATIONS = 600_000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _derive_key(password: str, salt: bytes) -> bytes:
|
|
18
|
+
"""Derive a 256-bit key from password using PBKDF2-HMAC-SHA256."""
|
|
19
|
+
kdf = PBKDF2HMAC(
|
|
20
|
+
algorithm=hashes.SHA256(),
|
|
21
|
+
length=KEY_SIZE,
|
|
22
|
+
salt=salt,
|
|
23
|
+
iterations=ITERATIONS,
|
|
24
|
+
backend=default_backend(),
|
|
25
|
+
)
|
|
26
|
+
return kdf.derive(password.encode('utf-8'))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encrypt_bytes(data: bytes, password: str) -> bytes:
|
|
30
|
+
"""
|
|
31
|
+
Encrypt bytes using AES-256-GCM with password-derived key.
|
|
32
|
+
|
|
33
|
+
Returns salt + nonce + ciphertext + tag (16 bytes).
|
|
34
|
+
"""
|
|
35
|
+
salt = os.urandom(SALT_SIZE)
|
|
36
|
+
nonce = os.urandom(NONCE_SIZE)
|
|
37
|
+
key = _derive_key(password, salt)
|
|
38
|
+
|
|
39
|
+
aesgcm = AESGCM(key)
|
|
40
|
+
ciphertext = aesgcm.encrypt(nonce, data, None)
|
|
41
|
+
|
|
42
|
+
return salt + nonce + ciphertext
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def decrypt_bytes(data: bytes, password: str) -> bytes:
|
|
46
|
+
"""
|
|
47
|
+
Decrypt bytes using AES-256-GCM with password-derived key.
|
|
48
|
+
|
|
49
|
+
Expects salt + nonce + ciphertext + tag (16 bytes).
|
|
50
|
+
"""
|
|
51
|
+
if len(data) < SALT_SIZE + NONCE_SIZE + 16:
|
|
52
|
+
raise ValueError("Data too short to contain valid encrypted content")
|
|
53
|
+
|
|
54
|
+
salt = data[:SALT_SIZE]
|
|
55
|
+
nonce = data[SALT_SIZE:SALT_SIZE + NONCE_SIZE]
|
|
56
|
+
ciphertext = data[SALT_SIZE + NONCE_SIZE:]
|
|
57
|
+
|
|
58
|
+
key = _derive_key(password, salt)
|
|
59
|
+
|
|
60
|
+
aesgcm = AESGCM(key)
|
|
61
|
+
return aesgcm.decrypt(nonce, ciphertext, None)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
DELIMITER = b"INFERLOCK"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def pack_file(header: dict, data: bytes) -> bytes:
|
|
8
|
+
"""
|
|
9
|
+
Pack a file with JSON header + delimiter + data.
|
|
10
|
+
|
|
11
|
+
Format: header_json + b"INFERLOCK" + data
|
|
12
|
+
"""
|
|
13
|
+
header_bytes = json.dumps(header).encode('utf-8')
|
|
14
|
+
return header_bytes + DELIMITER + data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def unpack_file(file_bytes: bytes) -> Tuple[dict, bytes]:
|
|
18
|
+
"""
|
|
19
|
+
Unpack a file into header dict and data bytes.
|
|
20
|
+
|
|
21
|
+
Expects: header_json + b"INFERLOCK" + data
|
|
22
|
+
"""
|
|
23
|
+
delimiter_pos = file_bytes.find(DELIMITER)
|
|
24
|
+
if delimiter_pos == -1:
|
|
25
|
+
raise ValueError("Missing delimiter in file data")
|
|
26
|
+
|
|
27
|
+
header_bytes = file_bytes[:delimiter_pos]
|
|
28
|
+
header = json.loads(header_bytes.decode('utf-8'))
|
|
29
|
+
encrypted_bytes = file_bytes[delimiter_pos + len(DELIMITER):]
|
|
30
|
+
|
|
31
|
+
return header, encrypted_bytes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
7
|
+
from cryptography.hazmat.primitives import serialization
|
|
8
|
+
|
|
9
|
+
_PK = "..." # Base64 encoded Ed25519 public key
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _parse_expire(expire: str) -> datetime:
|
|
13
|
+
"""Parse expiration string to datetime."""
|
|
14
|
+
if expire.endswith('d'):
|
|
15
|
+
days = int(expire[:-1])
|
|
16
|
+
return datetime.now() + timedelta(days=days)
|
|
17
|
+
try:
|
|
18
|
+
return datetime.strptime(expire, "%Y-%m-%d")
|
|
19
|
+
except ValueError:
|
|
20
|
+
raise ValueError(f"Invalid expire format: {expire}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_license(key: str, expire: str, device: str = None, private_key: str = None) -> bytes:
|
|
24
|
+
"""
|
|
25
|
+
Create license bytes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
key: License key string
|
|
29
|
+
expire: Expiration string (30d, 7d, YYYY-MM-DD)
|
|
30
|
+
device: Optional device ID to bind license
|
|
31
|
+
private_key: Base64 encoded private key for signing (optional)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
JSON license bytes with optional signature
|
|
35
|
+
"""
|
|
36
|
+
expire_dt = _parse_expire(expire)
|
|
37
|
+
|
|
38
|
+
license_data = {
|
|
39
|
+
"key": key,
|
|
40
|
+
"expire": expire_dt.strftime("%Y-%m-%d"),
|
|
41
|
+
"version": 1,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if device:
|
|
45
|
+
license_data["device"] = device
|
|
46
|
+
|
|
47
|
+
if private_key:
|
|
48
|
+
license_json = json.dumps(license_data).encode('utf-8')
|
|
49
|
+
signature = sign_data(private_key, license_json)
|
|
50
|
+
return json.dumps({
|
|
51
|
+
"data": license_data,
|
|
52
|
+
"signature": signature
|
|
53
|
+
}).encode('utf-8')
|
|
54
|
+
|
|
55
|
+
return json.dumps(license_data).encode('utf-8')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_license(data: bytes) -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Parse license bytes to dict.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
data: License bytes
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
License dictionary (handles both old and new formats)
|
|
67
|
+
"""
|
|
68
|
+
parsed = json.loads(data.decode('utf-8'))
|
|
69
|
+
|
|
70
|
+
# New format with signature
|
|
71
|
+
if "data" in parsed:
|
|
72
|
+
return parsed["data"]
|
|
73
|
+
|
|
74
|
+
# Old format (backward compatible)
|
|
75
|
+
return parsed
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _v(data: bytes) -> dict:
|
|
79
|
+
"""
|
|
80
|
+
Verify license signature and return license data.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
data: License bytes
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
License dictionary
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
Exception: Invalid license signature
|
|
90
|
+
"""
|
|
91
|
+
parsed = json.loads(data.decode('utf-8'))
|
|
92
|
+
|
|
93
|
+
# New format with signature
|
|
94
|
+
if "signature" in parsed:
|
|
95
|
+
license_data = "data" in parsed and parsed["data"] or parsed
|
|
96
|
+
license_json = json.dumps(license_data).encode('utf-8')
|
|
97
|
+
|
|
98
|
+
if not verify_signature(_PK, license_json, parsed["signature"]):
|
|
99
|
+
raise Exception("Invalid license signature")
|
|
100
|
+
|
|
101
|
+
return license_data
|
|
102
|
+
|
|
103
|
+
# Old format (backward compatible)
|
|
104
|
+
return parsed
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_expired(license_dict: dict) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if license is expired.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
license_dict: License dictionary
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if expired, False otherwise
|
|
116
|
+
"""
|
|
117
|
+
expire_at = datetime.strptime(license_dict["expire"], "%Y-%m-%d")
|
|
118
|
+
return datetime.now() >= expire_at
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_device_id() -> str:
|
|
122
|
+
"""
|
|
123
|
+
Get unique device identifier.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Hex string of hashed MAC address
|
|
127
|
+
"""
|
|
128
|
+
mac = uuid.getnode()
|
|
129
|
+
hashed = hashlib.sha256(str(mac).encode()).hexdigest()
|
|
130
|
+
return hashed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def generate_keypair() -> tuple[str, str]:
|
|
134
|
+
"""
|
|
135
|
+
Generate Ed25519 keypair.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple of (private_key_b64, public_key_b64) as base64 strings
|
|
139
|
+
"""
|
|
140
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
141
|
+
public_key = private_key.public_key()
|
|
142
|
+
|
|
143
|
+
private_bytes = private_key.private_bytes(
|
|
144
|
+
encoding=serialization.Encoding.Raw,
|
|
145
|
+
format=serialization.PrivateFormat.Raw,
|
|
146
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
public_bytes = public_key.public_bytes(
|
|
150
|
+
encoding=serialization.Encoding.Raw,
|
|
151
|
+
format=serialization.PublicFormat.Raw
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
base64.b64encode(private_bytes).decode('utf-8'),
|
|
156
|
+
base64.b64encode(public_bytes).decode('utf-8')
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def sign_data(private_key: str, data_bytes: bytes) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Sign data bytes with Ed25519 private key.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
private_key: Base64 encoded private key
|
|
166
|
+
data_bytes: Data to sign
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Base64 encoded signature
|
|
170
|
+
"""
|
|
171
|
+
private_bytes = base64.b64decode(private_key)
|
|
172
|
+
private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(private_bytes)
|
|
173
|
+
|
|
174
|
+
signature = private_key_obj.sign(data_bytes)
|
|
175
|
+
return base64.b64encode(signature).decode('utf-8')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def verify_signature(public_key: str, data_bytes: bytes, signature: str) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Verify signature with Ed25519 public key.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
public_key: Base64 encoded public key
|
|
184
|
+
data_bytes: Original data
|
|
185
|
+
signature: Base64 encoded signature
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if signature is valid, False otherwise
|
|
189
|
+
"""
|
|
190
|
+
public_bytes = base64.b64decode(public_key)
|
|
191
|
+
public_key_obj = ed25519.Ed25519PublicKey.from_public_bytes(public_bytes)
|
|
192
|
+
|
|
193
|
+
signature_bytes = base64.b64decode(signature)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
public_key_obj.verify(signature_bytes, data_bytes)
|
|
197
|
+
return True
|
|
198
|
+
except Exception:
|
|
199
|
+
return False
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .crypto import decrypt_bytes
|
|
5
|
+
from .format import unpack_file
|
|
6
|
+
from .license import _v, is_expired, get_device_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_model(path: str, license: str = None):
|
|
10
|
+
"""
|
|
11
|
+
Load a model from an encrypted .inferlock file or bundle directory.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
path: Path to .inferlock file or bundle directory
|
|
15
|
+
license: Path to license.inferlock file (required if path is file)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Loaded model object
|
|
19
|
+
"""
|
|
20
|
+
# Handle bundle directory
|
|
21
|
+
if os.path.isdir(path):
|
|
22
|
+
model_path = os.path.join(path, "model.inferlock")
|
|
23
|
+
license_path = os.path.join(path, "license.inferlock")
|
|
24
|
+
else:
|
|
25
|
+
# Handle file path
|
|
26
|
+
if not license:
|
|
27
|
+
raise Exception("License required")
|
|
28
|
+
model_path = path
|
|
29
|
+
license_path = license
|
|
30
|
+
|
|
31
|
+
# Read and parse license
|
|
32
|
+
with open(license_path, 'rb') as f:
|
|
33
|
+
license_data = f.read()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
license_dict = _v(license_data)
|
|
37
|
+
except Exception:
|
|
38
|
+
raise Exception("License validation failed")
|
|
39
|
+
|
|
40
|
+
# Check expiration
|
|
41
|
+
if is_expired(license_dict):
|
|
42
|
+
raise Exception("License validation failed")
|
|
43
|
+
|
|
44
|
+
# Check device binding
|
|
45
|
+
if "device" in license_dict:
|
|
46
|
+
current_device = get_device_id()
|
|
47
|
+
if current_device != license_dict["device"]:
|
|
48
|
+
raise Exception("License validation failed")
|
|
49
|
+
|
|
50
|
+
# Extract key from license
|
|
51
|
+
key = license_dict["key"]
|
|
52
|
+
|
|
53
|
+
# Read and decrypt model
|
|
54
|
+
with open(model_path, 'rb') as f:
|
|
55
|
+
file_bytes = f.read()
|
|
56
|
+
|
|
57
|
+
header, encrypted = unpack_file(file_bytes)
|
|
58
|
+
decrypted = decrypt_bytes(encrypted, key)
|
|
59
|
+
|
|
60
|
+
original_name = header.get("original_name", "")
|
|
61
|
+
|
|
62
|
+
if original_name.endswith(".pt"):
|
|
63
|
+
import torch
|
|
64
|
+
return torch.load(io.BytesIO(decrypted))
|
|
65
|
+
|
|
66
|
+
# Default: return bytes for other formats
|
|
67
|
+
return decrypted
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inferlock
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: File encryption and licensing for AI models
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography
|
|
8
|
+
|
|
9
|
+
# inferlock
|
|
10
|
+
|
|
11
|
+
Protect AI model files with encryption and license-based runtime loading.
|
|
12
|
+
|
|
13
|
+
inferlock encrypts model files and loads them securely with expiration-based licenses.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
* Model file encryption
|
|
20
|
+
* Runtime in-memory decryption
|
|
21
|
+
* License-based loading
|
|
22
|
+
* Expiration-based license
|
|
23
|
+
* Single-command packaging
|
|
24
|
+
* Bundle-based distribution
|
|
25
|
+
* CLI-first design
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install inferlock
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
or local dev:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
Encrypt model and create license:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
inferlock encrypt model.pt --expire 30d
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
model.inferlock/
|
|
55
|
+
├── model.inferlock
|
|
56
|
+
└── license.inferlock
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Load model:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from inferlock import load
|
|
63
|
+
|
|
64
|
+
model = load("model.inferlock")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## CLI Usage
|
|
70
|
+
|
|
71
|
+
Encrypt model:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
inferlock encrypt model.pt --expire 30d
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Generate license manually:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
inferlock license model.key --expire 30d
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Show bundle info:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
inferlock info model.inferlock
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Device Binding
|
|
92
|
+
|
|
93
|
+
Bind license to machine:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
inferlock encrypt model.pt --expire 30d --bind-device
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The license will be tied to the current machine's MAC address and cannot be used on other devices.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Bundle Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
model.inferlock/
|
|
107
|
+
├── model.inferlock
|
|
108
|
+
└── license.inferlock
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Python API
|
|
114
|
+
|
|
115
|
+
Load bundle:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from inferlock import load
|
|
119
|
+
|
|
120
|
+
model = load("model.inferlock")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Load manually:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
load("model.inferlock", "license.inferlock")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## License Format
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"key": "...",
|
|
136
|
+
"expire": "YYYY-MM-DD",
|
|
137
|
+
"version": 1
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Version
|
|
142
|
+
|
|
143
|
+
### v0.2.0
|
|
144
|
+
|
|
145
|
+
* license system
|
|
146
|
+
* expiration support
|
|
147
|
+
* bundle packaging
|
|
148
|
+
* single command encrypt + license
|
|
149
|
+
* auto bundle loader
|
|
150
|
+
|
|
151
|
+
### v0.3.0
|
|
152
|
+
|
|
153
|
+
* device binding (MAC)
|
|
154
|
+
* per-device license
|
|
155
|
+
|
|
156
|
+
### v0.4.0
|
|
157
|
+
|
|
158
|
+
* signed license
|
|
159
|
+
* tamper protection
|
|
160
|
+
|
|
161
|
+
### v0.5.0
|
|
162
|
+
|
|
163
|
+
* encrypted license
|
|
164
|
+
* hidden key
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Roadmap
|
|
169
|
+
|
|
170
|
+
Future features:
|
|
171
|
+
|
|
172
|
+
* multi-device license
|
|
173
|
+
* trial license
|
|
174
|
+
* offline activation
|
|
175
|
+
* hardware fingerprint
|
|
176
|
+
* bulk license generation
|
|
177
|
+
* license revoke
|
|
178
|
+
* metadata bundle
|
|
179
|
+
* verify command
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Example
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
inferlock encrypt yolov7.weights --expire 7d
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Load:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
load("yolov7.inferlock")
|
|
193
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
inferlock/__init__.py
|
|
4
|
+
inferlock/cli.py
|
|
5
|
+
inferlock/crypto.py
|
|
6
|
+
inferlock/format.py
|
|
7
|
+
inferlock/license.py
|
|
8
|
+
inferlock/loader.py
|
|
9
|
+
inferlock.egg-info/PKG-INFO
|
|
10
|
+
inferlock.egg-info/SOURCES.txt
|
|
11
|
+
inferlock.egg-info/dependency_links.txt
|
|
12
|
+
inferlock.egg-info/entry_points.txt
|
|
13
|
+
inferlock.egg-info/requires.txt
|
|
14
|
+
inferlock.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inferlock"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "File encryption and licensing for AI models"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cryptography",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
inferlock = "inferlock.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["."]
|