|
@@ -15,29 +15,33 @@ import argparse
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
-def generate_password_hash(password: str) -> str:
|
|
|
|
|
- """Generate password hash using PBKDF2 with SHA-256"""
|
|
|
|
|
|
|
+def generate_password_hash(password: str, iterations: int = 100000) -> str:
|
|
|
|
|
+ """Generate password hash using PBKDF2 with SHA-256 in canonical format"""
|
|
|
salt = secrets.token_hex(16)
|
|
salt = secrets.token_hex(16)
|
|
|
- password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
|
|
|
|
|
- return f"{salt}:{password_hash.hex()}"
|
|
|
|
|
|
|
+ derived = hashlib.pbkdf2_hmac(
|
|
|
|
|
+ "sha256", password.encode(), salt.encode(), iterations
|
|
|
|
|
+ )
|
|
|
|
|
+ # Return canonical format including algorithm and iteration count
|
|
|
|
|
+ return f"pbkdf2_sha256${iterations}${salt}${derived.hex()}"
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_secure_password(length: int = 32) -> str:
|
|
def generate_secure_password(length: int = 32) -> str:
|
|
|
"""Generate a secure password"""
|
|
"""Generate a secure password"""
|
|
|
import string
|
|
import string
|
|
|
|
|
+
|
|
|
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
|
- return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
|
|
|
|
|
+ return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_config(config_path: str):
|
|
def load_config(config_path: str):
|
|
|
"""Load configuration file"""
|
|
"""Load configuration file"""
|
|
|
- with open(config_path, 'r') as f:
|
|
|
|
|
|
|
+ with open(config_path, "r") as f:
|
|
|
return json.load(f)
|
|
return json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_config(config, config_path: str):
|
|
def save_config(config, config_path: str):
|
|
|
"""Save configuration file"""
|
|
"""Save configuration file"""
|
|
|
- with open(config_path, 'w') as f:
|
|
|
|
|
|
|
+ with open(config_path, "w") as f:
|
|
|
json.dump(config, f, indent=2)
|
|
json.dump(config, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -57,8 +61,9 @@ def copy_key_file(source_path: str, dest_path: str):
|
|
|
if os.geteuid() == 0: # Running as root
|
|
if os.geteuid() == 0: # Running as root
|
|
|
try:
|
|
try:
|
|
|
import pwd, grp
|
|
import pwd, grp
|
|
|
- user = pwd.getpwnam('emergency-access')
|
|
|
|
|
- group = grp.getgrnam('emergency-access')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ user = pwd.getpwnam("emergency-access")
|
|
|
|
|
+ group = grp.getgrnam("emergency-access")
|
|
|
os.chown(dest_path, user.pw_uid, group.gr_gid)
|
|
os.chown(dest_path, user.pw_uid, group.gr_gid)
|
|
|
print(f"✅ Copied key file with emergency-access ownership: {dest_path}")
|
|
print(f"✅ Copied key file with emergency-access ownership: {dest_path}")
|
|
|
except (KeyError, OSError):
|
|
except (KeyError, OSError):
|
|
@@ -71,13 +76,15 @@ def copy_key_file(source_path: str, dest_path: str):
|
|
|
|
|
|
|
|
def main():
|
|
def main():
|
|
|
parser = argparse.ArgumentParser(description="Add a key to emergency access server")
|
|
parser = argparse.ArgumentParser(description="Add a key to emergency access server")
|
|
|
- parser.add_argument('key_id', help='Key identifier (e.g., backup, master)')
|
|
|
|
|
- parser.add_argument('key_file', help='Path to existing key file')
|
|
|
|
|
- parser.add_argument('backends', help='Comma-separated notification backends')
|
|
|
|
|
- parser.add_argument('--config', default='config.json', help='Config file path')
|
|
|
|
|
- parser.add_argument('--dest-dir', default='/etc/emergency-access', help='Destination directory')
|
|
|
|
|
- parser.add_argument('--password', help='Password (generated if not provided)')
|
|
|
|
|
- parser.add_argument('--message', help='Custom notification message')
|
|
|
|
|
|
|
+ parser.add_argument("key_id", help="Key identifier (e.g., backup, master)")
|
|
|
|
|
+ parser.add_argument("key_file", help="Path to existing key file")
|
|
|
|
|
+ parser.add_argument("backends", help="Comma-separated notification backends")
|
|
|
|
|
+ parser.add_argument("--config", default="config.json", help="Config file path")
|
|
|
|
|
+ parser.add_argument(
|
|
|
|
|
+ "--dest-dir", default="/etc/emergency-access", help="Destination directory"
|
|
|
|
|
+ )
|
|
|
|
|
+ parser.add_argument("--password", help="Password (generated if not provided)")
|
|
|
|
|
+ parser.add_argument("--message", help="Custom notification message")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
@@ -94,7 +101,7 @@ def main():
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
# Check if key already exists
|
|
# Check if key already exists
|
|
|
- if args.key_id in config.get('keys', {}):
|
|
|
|
|
|
|
+ if args.key_id in config.get("keys", {}):
|
|
|
print(f"❌ Key '{args.key_id}' already exists")
|
|
print(f"❌ Key '{args.key_id}' already exists")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
|
|
|
|
|
@@ -110,23 +117,23 @@ def main():
|
|
|
route = f"/emergency-key-{args.key_id}"
|
|
route = f"/emergency-key-{args.key_id}"
|
|
|
username = f"emergency_{args.key_id}"
|
|
username = f"emergency_{args.key_id}"
|
|
|
password_hash = generate_password_hash(password)
|
|
password_hash = generate_password_hash(password)
|
|
|
- backends = [b.strip() for b in args.backends.split(',')]
|
|
|
|
|
|
|
+ backends = [b.strip() for b in args.backends.split(",")]
|
|
|
message = args.message or f"🚨 EMERGENCY: Key {args.key_id} accessed from server"
|
|
message = args.message or f"🚨 EMERGENCY: Key {args.key_id} accessed from server"
|
|
|
|
|
|
|
|
# Copy key file
|
|
# Copy key file
|
|
|
copy_key_file(args.key_file, dest_file)
|
|
copy_key_file(args.key_file, dest_file)
|
|
|
|
|
|
|
|
# Add to config
|
|
# Add to config
|
|
|
- if 'keys' not in config:
|
|
|
|
|
- config['keys'] = {}
|
|
|
|
|
-
|
|
|
|
|
- config['keys'][args.key_id] = {
|
|
|
|
|
- 'route': route,
|
|
|
|
|
- 'file': dest_file,
|
|
|
|
|
- 'username': username,
|
|
|
|
|
- 'password_hash': password_hash,
|
|
|
|
|
- 'backends': backends,
|
|
|
|
|
- 'message': message
|
|
|
|
|
|
|
+ if "keys" not in config:
|
|
|
|
|
+ config["keys"] = {}
|
|
|
|
|
+
|
|
|
|
|
+ config["keys"][args.key_id] = {
|
|
|
|
|
+ "route": route,
|
|
|
|
|
+ "file": dest_file,
|
|
|
|
|
+ "username": username,
|
|
|
|
|
+ "password_hash": password_hash,
|
|
|
|
|
+ "backends": backends,
|
|
|
|
|
+ "message": message,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Save config
|
|
# Save config
|
|
@@ -139,5 +146,5 @@ def main():
|
|
|
print(f" Backends: {', '.join(backends)}")
|
|
print(f" Backends: {', '.join(backends)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
-if __name__ == '__main__':
|
|
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
main()
|
|
main()
|