knightcrypt 4.0.0__py3-none-any.whl

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.
@@ -0,0 +1,53 @@
1
+ """
2
+ Author: Sancho (@sanchoforza / @gloriasancho on Discord)
3
+ Version: 1.0.0
4
+ License: MIT
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+ __author__ = "Sancho"
9
+ __discord__ = "@sanchoforza / @gloriasancho"
10
+ __license__ = "MIT"
11
+
12
+ # Import core classes for easy access
13
+ from knightcrypt.obfuscation import KnightObfuscator
14
+ from knightcrypt.cli import main as cli_main
15
+
16
+ # Optional: Expose key functions for programmatic use
17
+ def encrypt_file(input_path, output_path=None, key="default", level="low"):
18
+ """
19
+ Encrypts a Python file using KnightCrypt.
20
+
21
+ Args:
22
+ input_path (str): Path to the .py file to encrypt.
23
+ output_path (str): Optional path for the encrypted output.
24
+ key (str): Encryption key.
25
+ level (str): Obfuscation level ('low', 'medium', 'high').
26
+
27
+ Returns:
28
+ str: Path to the generated encrypted file.
29
+ """
30
+ obf = KnightObfuscator(key=key, level=level)
31
+ return obf.encrypt_file(input_path, output_path)
32
+
33
+ def decrypt_file(enc_path, key="default"):
34
+ """
35
+ Decrypts a KnightCrypt encrypted file.
36
+
37
+ Args:
38
+ enc_path (str): Path to the .kn encrypted file.
39
+ key (str): Decryption key.
40
+
41
+ Returns:
42
+ str: The decrypted source code.
43
+ """
44
+ from knightcrypt.runtime import KnightRuntime
45
+ runtime = KnightRuntime.from_file(enc_path, key)
46
+ return runtime.decrypt_source()
47
+
48
+ __all__ = [
49
+ "KnightObfuscator",
50
+ "encrypt_file",
51
+ "decrypt_file",
52
+ "cli_main",
53
+ ]
knightcrypt/cli.py ADDED
@@ -0,0 +1,39 @@
1
+ # knightcrypt/cli.py
2
+ import sys
3
+ import os
4
+ import argparse
5
+ from knightcrypt.obfuscation import KnightObfuscator
6
+ from knightcrypt.utils import print_banner, log_info, log_success, log_warning, log_error
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(description="KnightCrypt - Advanced Python Encryption")
10
+ parser.add_argument("file", help="Python file to encrypt")
11
+ parser.add_argument("-k", "--key", default="secret", help="Encryption key")
12
+ parser.add_argument("-o", "--output", help="Output filename (optional)")
13
+ parser.add_argument("-l", "--level", choices=["low", "medium", "high"], default="high", help="Obfuscation level")
14
+
15
+ args = parser.parse_args()
16
+
17
+ if not os.path.exists(args.file):
18
+ log_error(f"File not found: {args.file}")
19
+ sys.exit(1)
20
+
21
+ print_banner()
22
+
23
+ obfuscator = KnightObfuscator(key=args.key, level=args.level)
24
+ enc_path = obfuscator.encrypt_file(args.file, args.output)
25
+
26
+ # Generate wrapper
27
+ wrapper_name = os.path.splitext(args.file)[0] + "_run.py"
28
+ obfuscator.generate_wrapper(enc_path, wrapper_name)
29
+
30
+ log_success(f"Encrypted {args.file} -> {enc_path}")
31
+ log_success(f"Generated wrapper: {wrapper_name}")
32
+ log_info(f"Run your app with: python {wrapper_name}")
33
+
34
+ # Display stats
35
+ if hasattr(obfuscator, 'obfuscation_stats'):
36
+ log_info(f"Obfuscation Stats: {obfuscator.obfuscation_stats}")
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,189 @@
1
+ # knightcrypt/obfuscator.py
2
+ import ast
3
+ import marshal
4
+ import os
5
+ import zlib
6
+ import random
7
+ import string
8
+ import struct
9
+ from knightcrypt.utils import log_info, log_success, log_warning
10
+
11
+ class VariableRenamer(ast.NodeTransformer):
12
+ """Renames all variables to random hex strings."""
13
+ def __init__(self):
14
+ self.counter = 0
15
+
16
+ def _new_name(self):
17
+ name = f"_0x{self.counter:x}"
18
+ self.counter += 1
19
+ return name
20
+
21
+ def visit_Name(self, node):
22
+ if isinstance(node.ctx, (ast.Store, ast.Load)):
23
+ new_node = ast.Name(id=self._new_name(), ctx=node.ctx)
24
+ ast.copy_location(new_node, node)
25
+ return new_node
26
+ return node
27
+
28
+ def visit_FunctionDef(self, node):
29
+ # Rename function arguments too
30
+ for arg in node.args.args:
31
+ arg.arg = self._new_name()
32
+ return self.generic_visit(node)
33
+
34
+ class StringEncryptor(ast.NodeTransformer):
35
+ """Encrypts all string literals using XOR."""
36
+ def __init__(self, key="default"):
37
+ self.key = key
38
+
39
+ def visit_Str(self, node):
40
+ original_str = node.s
41
+ encrypted_bytes = bytes([ord(c) ^ ord(self.key[i % len(self.key)]) for i, c in enumerate(original_str)])
42
+ # Return a call to a helper function that decrypts at runtime
43
+ # For simplicity, we'll just replace with a decrypted constant
44
+ # In a real scenario, you'd generate a decryption function
45
+ return ast.Constant(value=original_str) # Simplified for demo
46
+
47
+ class ControlFlowFlattener(ast.NodeVisitor):
48
+ """
49
+ Converts basic blocks into a switch-case style loop.
50
+ This makes static analysis very difficult.
51
+ """
52
+ def __init__(self):
53
+ self.blocks = []
54
+ self.current_block_id = 0
55
+
56
+ def visit_If(self, node):
57
+ # Create two blocks: one for True, one for False
58
+ true_block_id = self.current_block_id + 1
59
+ false_block_id = self.current_block_id + 2
60
+
61
+ self.blocks.append({
62
+ 'id': self.current_block_id,
63
+ 'type': 'if',
64
+ 'condition': ast.unparse(node.test),
65
+ 'true_target': true_block_id,
66
+ 'false_target': false_block_id,
67
+ 'body': [ast.dump(stmt) for stmt in node.body],
68
+ 'orelse': [ast.dump(stmt) for stmt in node.orelse]
69
+ })
70
+
71
+ self.current_block_id += 3
72
+ self.visit_children(node)
73
+
74
+ def visit_Assign(self, node):
75
+ self.blocks.append({
76
+ 'id': self.current_block_id,
77
+ 'type': 'assign',
78
+ 'target': ast.unparse(node.targets[0]),
79
+ 'value': ast.unparse(node.value),
80
+ 'next': self.current_block_id + 1
81
+ })
82
+ self.current_block_id += 1
83
+
84
+ def visit_children(self, node):
85
+ for child in ast.iter_child_nodes(node):
86
+ self.visit(child)
87
+
88
+ def generate_switch_code(self):
89
+ """Generates Python code for a switch-case loop."""
90
+ code = "state = 0\nwhile True:\n"
91
+ for block in self.blocks:
92
+ indent = " "
93
+ if block['type'] == 'if':
94
+ code += f"{indent}if state == {block['id']}:\n"
95
+ code += f"{indent} if {block['condition']}:\n"
96
+ code += f"{indent} state = {block['true_target']}\n"
97
+ code += f"{indent} else:\n"
98
+ code += f"{indent} state = {block['false_target']}\n"
99
+ elif block['type'] == 'assign':
100
+ code += f"{indent}if state == {block['id']}:\n"
101
+ code += f"{indent} {block['target']} = {block['value']}\n"
102
+ code += f"{indent} state = {block['next']}\n"
103
+ code += " break\n"
104
+ return code
105
+
106
+ class KnightObfuscator:
107
+ def __init__(self, key="default_key", level="high"):
108
+ self.key = key
109
+ self.level = level # low, medium, high
110
+ self.obfuscation_stats = {}
111
+
112
+ def obfuscate_source(self, source_code):
113
+ tree = ast.parse(source_code)
114
+
115
+ # Step 1: Rename Variables
116
+ renamer = VariableRenamer()
117
+ tree = renamer.visit(tree)
118
+ self.obfuscation_stats['variables_renamed'] = renamer.counter
119
+
120
+ # Step 2: Encrypt Strings (Simplified)
121
+ encryptor = StringEncryptor(key=self.key)
122
+ tree = encryptor.visit(tree)
123
+
124
+ # Step 3: Flatten Control Flow (if level is high)
125
+ if self.level == "high":
126
+ flattener = ControlFlowFlattener()
127
+ flattener.visit(tree)
128
+ switch_code = flattener.generate_switch_code()
129
+ # Append the switch code to the end of the source
130
+ source_code = source_code + "\n" + switch_code
131
+
132
+ # Step 4: Add Opaque Predicates
133
+ predicate = "if (hash(str(__name__)) % 2 == 0): pass\n"
134
+ source_code = predicate + source_code
135
+
136
+ return source_code
137
+
138
+ def encrypt_file(self, input_path, output_path=None):
139
+ if output_path is None:
140
+ output_path = input_path.replace('.py', '_enc.kn')
141
+
142
+ with open(input_path, 'r') as f:
143
+ source = f.read()
144
+
145
+ # Obfuscate
146
+ obfuscated_source = self.obfuscate_source(source)
147
+
148
+ # Compile to bytecode
149
+ code_obj = compile(obfuscated_source, input_path, 'exec')
150
+
151
+ # Marshal and Compress
152
+ marshaled = marshal.dumps(code_obj)
153
+ compressed = zlib.compress(marshaled)
154
+
155
+ # Encrypt with XOR
156
+ encrypted = bytes([b ^ ord(self.key[i % len(self.key)]) for i, b in enumerate(compressed)])
157
+
158
+ # Write output with header
159
+ header = len(self.key).to_bytes(2, 'big')
160
+ key_bytes = self.key.encode('utf-8')
161
+
162
+ with open(output_path, 'wb') as f:
163
+ f.write(header)
164
+ f.write(key_bytes)
165
+ f.write(encrypted)
166
+
167
+ return output_path
168
+
169
+ def generate_wrapper(self, enc_filepath, wrapper_name="run_knight.py"):
170
+ wrapper_content = '''
171
+ import sys
172
+ import os
173
+ from knightcrypt.runtime import load_and_run
174
+
175
+ def main():
176
+ enc_path = os.path.join(os.path.dirname(__file__), "{enc_filename}")
177
+ key = "{key}"
178
+ try:
179
+ load_and_run(enc_path, key)
180
+ except Exception as e:
181
+ print(f"[-] KnightCrypt Runtime Error: {{e}}")
182
+ sys.exit(1)
183
+
184
+ if __name__ == "__main__":
185
+ main()
186
+ '''.format(enc_filename=os.path.basename(enc_filepath), key=self.key)
187
+
188
+ with open(wrapper_name, 'w') as f:
189
+ f.write(wrapper_content)
knightcrypt/runtime.py ADDED
@@ -0,0 +1,230 @@
1
+
2
+ import sys
3
+ import os
4
+ import marshal
5
+ import zlib
6
+ import struct
7
+ import hashlib
8
+ import types
9
+ import traceback
10
+ import gc
11
+ import dis
12
+ import time
13
+ import platform
14
+ import subprocess
15
+ import ctypes
16
+ from Crypto.Cipher import AES
17
+ from Crypto.Util.Padding import unpad
18
+ from Crypto.Random import get_random_bytes
19
+ import threading
20
+ import atexit
21
+
22
+ class KnightRuntimeError(Exception):
23
+ """Custom exception for KnightCrypt runtime errors."""
24
+ pass
25
+
26
+ class KnightRuntime:
27
+ # Magic bytes to identify valid KnightCrypt files
28
+ MAGIC_HEADER = b'KNIGHT'
29
+ VERSION = 1
30
+
31
+ # Known debugger processes
32
+ DEBUGGER_PROCESSES = ['devenv.exe', 'ida.exe', 'x64dbg.exe', 'ollydbg.exe', 'gdb', 'lldb']
33
+
34
+ def __init__(self, encrypted_data: bytes, key: str):
35
+ self.encrypted_data = encrypted_data
36
+ self.key = key
37
+ self.globals = {}
38
+ self.locals = {}
39
+ self._verify_integrity()
40
+ self._check_debuggers()
41
+
42
+ def _get_key_material(self, password: str) -> tuple:
43
+ """Derives a 256-bit key and IV from the password using PBKDF2."""
44
+ salt = b'somegoofyahhshi' # Fixed salt for consistency in this demo; in production, store it separately or randomize
45
+ pbkdf2 = hashlib.pbkdf2_hmac('sha256',
46
+ password.encode('utf-8'),
47
+ salt,
48
+ iterations=100000)
49
+ return pbkdf2[:32], pbkdf2[32:44] # 32 bytes key + 12 bytes IV
50
+
51
+ def _verify_integrity(self):
52
+ """Verifies the integrity of the runtime module itself."""
53
+ # In a production scenario, you would hash the runtime.py file
54
+ # and compare it against a known good hash embedded here.
55
+ current_file = os.path.abspath(__file__)
56
+ with open(current_file, 'rb') as f:
57
+ content = f.read()
58
+ current_hash = hashlib.sha256(content).hexdigest()
59
+ expected_hash = "your_expected_hash_here" # Replace with actual hash of this file
60
+ if current_hash != expected_hash:
61
+ raise KnightRuntimeError("Runtime integrity check failed: File may have been tampered with.")
62
+
63
+ def _check_debuggers(self):
64
+ """Checks for common debugger processes."""
65
+ try:
66
+ if platform.system() == "Windows":
67
+ import psutil
68
+ for proc in psutil.process_iter(['name']):
69
+ if proc.info['name'].lower() in [db.lower() for db in self.DEBUGGER_PROCESSES]:
70
+ raise KnightRuntimeError(f"Debugger detected: {proc.info['name']}")
71
+ else:
72
+ # For Linux/Mac, you might use /proc or other methods
73
+ pass
74
+ except ImportError:
75
+ # If psutil is not available, skip this check
76
+ pass
77
+
78
+ def decrypt_payload(self) -> bytes:
79
+ """
80
+ Decrypts the payload using AES-256-GCM.
81
+
82
+ Returns:
83
+ bytes: Decrypted compressed marshaled code object.
84
+
85
+ Raises:
86
+ KnightRuntimeError: If decryption fails or integrity check fails.
87
+ """
88
+ if not self.encrypted_data:
89
+ raise KnightRuntimeError("Empty encrypted data")
90
+
91
+ try:
92
+ # Extract header and ciphertext
93
+ # Format: [MAGIC(6)] [VERSION(1)] [SALT(8)] [IV(12)] [TAG(16)] [CIPHERTEXT]
94
+ if len(self.encrypted_data) < 6 + 1 + 8 + 12 + 16:
95
+ raise KnightRuntimeError("Invalid file format: too short")
96
+
97
+ magic = self.encrypted_data[:6]
98
+ if magic != self.MAGIC_HEADER:
99
+ raise KnightRuntimeError(f"Invalid magic header: {magic}")
100
+
101
+ version = self.encrypted_data[6]
102
+ if version != self.VERSION:
103
+ raise KnightRuntimeError(f"Unsupported version: {version}")
104
+
105
+ salt = self.encrypted_data[7:15]
106
+ iv = self.encrypted_data[15:27]
107
+ tag = self.encrypted_data[27:43]
108
+ ciphertext = self.encrypted_data[43:]
109
+
110
+ # Derive key from password and salt
111
+ key, derived_iv = self._get_key_material(self.key)
112
+
113
+ # Use the stored IV for GCM mode
114
+ cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
115
+
116
+ # Decrypt and verify tag
117
+ plaintext = cipher.decrypt_and_verify(ciphertext, tag)
118
+
119
+ return plaintext
120
+
121
+ except ValueError as e:
122
+ raise KnightRuntimeError(f"Decryption failed: Invalid key or corrupted data. {e}")
123
+ except Exception as e:
124
+ raise KnightRuntimeError(f"Unexpected error during decryption: {e}")
125
+
126
+ def load_code_object(self, decrypted_data: bytes) -> types.CodeType:
127
+ """
128
+ Decompresses and loads the marshaled code object.
129
+
130
+ Args:
131
+ decrypted_data (bytes): Decrypted compressed data.
132
+
133
+ Returns:
134
+ types.CodeType: The compiled code object.
135
+ """
136
+ try:
137
+ # Decompress zlib data
138
+ decompressed = zlib.decompress(decrypted_data)
139
+
140
+ # Load marshaled code object
141
+ code_obj = marshal.loads(decompressed)
142
+
143
+ if not isinstance(code_obj, types.CodeType):
144
+ raise KnightRuntimeError("Invalid code object format")
145
+
146
+ return code_obj
147
+
148
+ except Exception as e:
149
+ raise KnightRuntimeError(f"Failed to load code object: {e}")
150
+
151
+ def execute(self, code_obj: types.CodeType) -> None:
152
+ """
153
+ Executes the code object in a controlled environment.
154
+
155
+ Args:
156
+ code_obj (types.CodeType): The code object to execute.
157
+ """
158
+ try:
159
+ # Create a new frame for isolated execution
160
+ frame = types.FrameType(
161
+ code=code_obj,
162
+ globals=self.globals,
163
+ locals=self.locals
164
+ )
165
+
166
+ # Execute the code
167
+ exec(code_obj, self.globals, self.locals)
168
+
169
+ except Exception as e:
170
+ print(f"[!] Execution Error: {e}", file=sys.stderr)
171
+ traceback.print_exc(file=sys.stderr)
172
+ raise KnightRuntimeError(f"Execution failed: {e}")
173
+
174
+ def run(self) -> None:
175
+ """Main entry point to decrypt and execute the payload."""
176
+ try:
177
+ decrypted_data = self.decrypt_payload()
178
+ code_obj = self.load_code_object(decrypted_data)
179
+ self.execute(code_obj)
180
+ except KnightRuntimeError as e:
181
+ print(f"[!] KnightCrypt Runtime Error: {e}", file=sys.stderr)
182
+ sys.exit(1)
183
+ except Exception as e:
184
+ print(f"[!] Unexpected Runtime Error: {e}", file=sys.stderr)
185
+ traceback.print_exc(file=sys.stderr)
186
+ sys.exit(1)
187
+ finally:
188
+ self._cleanup_memory()
189
+
190
+ def _cleanup_memory(self):
191
+ """Clears sensitive data from memory."""
192
+ # Clear globals and locals
193
+ self.globals.clear()
194
+ self.locals.clear()
195
+
196
+ # Garbage collect
197
+ gc.collect()
198
+
199
+ # Optionally, overwrite encrypted data with zeros
200
+ if hasattr(self, 'encrypted_data'):
201
+ self.encrypted_data = b'\x00' * len(self.encrypted_data)
202
+
203
+ def load_and_run(filepath: str, key: str) -> None:
204
+ """
205
+ Convenience function to load and run an encrypted file.
206
+
207
+ Args:
208
+ filepath (str): Path to the encrypted .kn file.
209
+ key (str): Decryption key.
210
+ """
211
+ try:
212
+ with open(filepath, 'rb') as f:
213
+ encrypted_data = f.read()
214
+
215
+ runtime = KnightRuntime(encrypted_data, key)
216
+ runtime.run()
217
+
218
+ except FileNotFoundError:
219
+ print(f"[!] File not found: {filepath}", file=sys.stderr)
220
+ sys.exit(1)
221
+ except IOError as e:
222
+ print(f"[!] I/O Error: {e}", file=sys.stderr)
223
+ sys.exit(1)
224
+ except KnightRuntimeError as e:
225
+ print(f"[!] KnightCrypt Runtime Error: {e}", file=sys.stderr)
226
+ sys.exit(1)
227
+ except Exception as e:
228
+ print(f"[!] Unexpected Error: {e}", file=sys.stderr)
229
+ traceback.print_exc(file=sys.stderr)
230
+ sys.exit(1)
knightcrypt/utils.py ADDED
@@ -0,0 +1,31 @@
1
+ # knightcrypt/utils.py
2
+ class Colors:
3
+ HEADER = '\033[95m'
4
+ BLUE = '\033[94m'
5
+ CYAN = '\033[96m'
6
+ GREEN = '\033[92m'
7
+ YELLOW = '\033[93m'
8
+ RED = '\033[91m'
9
+ ENDC = '\033[0m'
10
+ BOLD = '\033[1m'
11
+ UNDERLINE = '\033[4m'
12
+
13
+ def print_banner():
14
+ print(f"{Colors.CYAN}{'='*50} {Colors.ENDC}")
15
+ print(f"{Colors.HEADER} KnightCrypt - Python Library for File Encryption {Colors.ENDC}")
16
+ print(f"{Colors.CYAN}{'='*50} {Colors.ENDC}")
17
+ print(f"{Colors.GREEN}[+] Welcome to KnightCrypt! {Colors.ENDC}")
18
+ print(f"{Colors.YELLOW}[!] Use this library responsibly and ensure you have permission to encrypt/decrypt files. {Colors.ENDC}")
19
+ print(f"{Colors.CYAN}{'='*50}{Colors.ENDC}")
20
+
21
+ def log_info(msg):
22
+ print(f"{Colors.BLUE}[INFO]{Colors.ENDC} {msg}")
23
+
24
+ def log_success(msg):
25
+ print(f"{Colors.GREEN}[SUCCESS]{Colors.ENDC} {msg}")
26
+
27
+ def log_warning(msg):
28
+ print(f"{Colors.YELLOW}[WARNING]{Colors.ENDC} {msg}")
29
+
30
+ def log_error(msg):
31
+ print(f"{Colors.RED}[ERROR]{Colors.ENDC} {msg}")
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: knightcrypt
3
+ Version: 4.0.0
4
+ Summary: cool python encryption lib yesyes
5
+ Author: Sancho
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Security :: Cryptography
12
+ Requires-Python: >=3.6
13
+ Requires-Dist: pycryptodome
14
+ Requires-Dist: psutil
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
@@ -0,0 +1,10 @@
1
+ knightcrypt/__init__.py,sha256=dXO26Q5kb1LVxChsX-7bJOk6Ee59a1ZMEqJYb9T-AQ4,1507
2
+ knightcrypt/cli.py,sha256=fV7qRidCL4xFElZRLOz7GEj3z1GPuTsfGkqUcY0S518,1496
3
+ knightcrypt/obfuscation.py,sha256=0DkE8QlJOtmoaCVjlymacXRbUZVZq2YI471uJVPxXP8,6700
4
+ knightcrypt/runtime.py,sha256=oIOb16hU4_2ELnYI3ruhSNIukyC-ALWfYyp1wGMZ0ic,8560
5
+ knightcrypt/utils.py,sha256=vSs4nTC7pL9XWatIaie3be87UAi9pxjkYb5yyEXJjX0,1045
6
+ knightcrypt-4.0.0.dist-info/METADATA,sha256=X9p9_fTDqRGQ_AlZFpuLIAfujf_1vcrWDBJpe0QexZQ,585
7
+ knightcrypt-4.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ knightcrypt-4.0.0.dist-info/entry_points.txt,sha256=TMGcLlvWfwnHxRHCfz_T-tK03GkW2fZ4jLqtBOu53ng,61
9
+ knightcrypt-4.0.0.dist-info/top_level.txt,sha256=WKNDYgL7E6Cb39gowd_4IernmMNbwbLyeXUCUCHRjm0,12
10
+ knightcrypt-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ knightcrypt = knightcrypt.knightcrypt:main
@@ -0,0 +1 @@
1
+ knightcrypt