add login

Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
This commit is contained in:
Tiago Garcia 2024-12-17 00:39:52 +00:00
parent 7a00abbd6f
commit ce436a0991
Signed by: TiagoRG
GPG Key ID: DFCD48E3F420DB42
19 changed files with 134 additions and 37 deletions

View File

@ -1,10 +1,14 @@
#!/bin/python3
import base64
import os
import sys
import argparse
import logging
import json
import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from subject import main
@ -36,15 +40,15 @@ def createSession(args):
args = parser.parse_args()
if not args.org or not args.username or not args.password or not args.credentials or not args.session:
logger.error("Need organization, username, password, credentials and session file")
if not args.org or not args.username or not args.credentials or not args.session:
logger.error("Need organization, username, credentials and session file")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.credentials)):
if not os.path.isfile(BASE_DIR + args.credentials):
logger.error("File '" + args.credentials + "' not found.")
sys.exit(1)
session = {'org' : args.org, 'username' : args.username, 'password' : args.password, 'credentials_file' : args.credentials}
session = {'org' : args.org, 'username' : args.username}
#print( type(json.dumps(session)))
@ -55,6 +59,29 @@ def createSession(args):
logger.error("Failed to obtain response from server")
sys.exit(-1)
response = req.json()
challenge = response['challenge']
with open(BASE_DIR + args.credentials, 'rb') as f:
try:
key = load_pem_private_key(f.read(), password=args.password.encode("utf-8") if args.password else None)
except ValueError:
logger.error("Invalid password")
sys.exit(-1)
signature = key.sign(
challenge.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps({'signature' : base64.b64encode(signature).decode('utf-8')}), headers={'Authorization': response['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server")
sys.exit(-1)
with open(BASE_DIR + args.session, 'w') as f:
json.dump(req.json(), f)

View File

@ -30,7 +30,7 @@ def decryptFile(args):
sys.exit(1)
# If first argument is not a file or not found
if (not os.path.isfile(BASE_DIR + args.encrypted)):
if not os.path.isfile(BASE_DIR + args.encrypted):
logger.error("File '" + args.encrypted + "' not found.")
sys.exit(1)
@ -42,7 +42,7 @@ def decryptFile(args):
print(args.metadata)
metadata = json.loads(args.metadata)
content = symmetric_encryption.decrypt_file(metadata['nonce'].encode(), metadata['key'].encode(), BASE_DIR + args.encrypted)
content = symmetric_encryption.decrypt_file(metadata['key'].encode(), BASE_DIR + args.encrypted)
# Send decrypted content to stdout
sys.stdout.write(content)

View File

@ -63,7 +63,7 @@ def getDoc(args):
#Get file with file_handle provided by metadata
try:
file = requests.get(f'http://{state['REP_ADDRESS']}/get/' + metadata['file_handle'] + '/content')
file = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + metadata['file_handle'] + '/content')
file.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")

View File

@ -13,6 +13,9 @@ logger.setLevel(logging.INFO)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
if not os.path.exists(BASE_DIR):
os.makedirs(BASE_DIR)
# Generate a key pair for a subject
# password - file for public key, file for private key
def generateKeyPair(args):

View File

@ -23,7 +23,7 @@ def reset(args):
parser.add_argument("-r", '--repo', nargs=1, help="Address:Port of the repository")
parser.add_argument("-v", '--verbose', help="Increase verbosity", action="store_true")
parser.add_argument('password', nargs='?', default=None)
parser.add_argument('password', nargs='?', default="123")
args = parser.parse_args()

View File

@ -40,7 +40,7 @@ def test_rep_list_orgs():
def test_rep_create_session():
# Test the rep_create_session command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_create_session org1 username password pub.pem session.json", shell=True)
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_create_session org1 username password priv.pem session.json", shell=True)
process.wait()
assert process.returncode == 0

View File

@ -34,7 +34,7 @@ def encrypt_file(input_file, output_file=None):
# Function to decrypt a file
def decrypt_file(key, input_file, output_file=None):
def decrypt_file(key, input_file, output_file=None) -> str:
plaintext_content = b""
with open(input_file, 'rb') as infile:
@ -62,5 +62,5 @@ def decrypt_file(key, input_file, output_file=None):
# Finalize decryption
plaintext_content += decryptor.finalize()
return plaintext_content
return plaintext_content.decode('utf-8', errors='ignore')

View File

@ -32,7 +32,7 @@ def index():
@app.route("/reset", methods=["POST"])
def reset():
password = request.json["password"]
password = request.json.get("password")
if password != "123":
return jsonify({"error": "Invalid password"}), 403
try:

View File

@ -26,9 +26,9 @@ class File(db_connection.Model):
"name": self.name,
"created_at": self.created_at,
"acl": self.acl,
"deleter_id": self.deleter_id,
"key": self.key,
"alg": self.alg,
"org": {"id": self.org.id, "name": self.org.name},
"creator": {"id": self.creator.id, "username": self.creator.username},
"deleter": {"id": self.deleter.id, "username": self.deleter.username} if self.deleter else None
}

View File

@ -10,6 +10,8 @@ class Session(db_connection.Model):
roles = db_connection.Column(db_connection.JSON, default=list)
created_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now())
updated_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now(), server_onupdate=db_connection.func.now())
challenge = db_connection.Column(db_connection.String(255), unique=True)
verified = db_connection.Column(db_connection.Boolean, default=False)
def to_dict(self):
return {
@ -18,5 +20,7 @@ class Session(db_connection.Model):
"org_id": self.org_id,
"token": self.token,
"created_at": self.created_at,
"updated_at": self.updated_at
"updated_at": self.updated_at,
"challenge": self.challenge,
"verified": self.verified
}

View File

@ -19,6 +19,6 @@ class User(db_connection.Model):
"username": self.username,
"full_name": self.full_name,
"email": self.email,
"role": self.role,
"roles": self.roles,
"orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()],
}

View File

@ -40,7 +40,6 @@ def file_get_metadata(document_handle: str):
@file_bp.route("/upload/metadata", methods=["POST"])
def file_upload_metadata():
session_token = request.headers.get("Authorization")
print(session_token)
session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_NEW])
if isinstance(session, tuple):
return session

View File

@ -71,7 +71,7 @@ def role_list_perms(role):
return jsonify({"error": "Organization not found"}), 404
try:
perms = RoleService.get_perms_for_role(org, role)
perms = RoleService.get_perms_for_role(org, role, return_str=True)
except ValueError as e:
return jsonify({"error": str(e)}), 400
return jsonify(perms), 200

View File

@ -1,4 +1,7 @@
import base64
import json
from cryptography.exceptions import InvalidSignature
from flask import Blueprint, request, jsonify
from services import UserService, SessionService, OrganizationService, RoleService
from utils import Perm
@ -11,19 +14,39 @@ def user_login():
if type(data) is str:
data = json.loads(data)
if "username" not in data or "org" not in data:
return jsonify({"error": "Missing required fields"}), 400
if "username" in data and "org" in data:
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
org = OrganizationService.get_organization_by_name(data["org"])
if not org:
return jsonify({"error": "Organization not found"}), 404
org = OrganizationService.get_organization_by_name(data["org"])
if not org:
return jsonify({"error": "Organization not found"}), 404
session = SessionService.create_session(user, org)
return jsonify(session.to_dict()), 201
session = SessionService.create_session(user, org)
return jsonify(session.to_dict()), 201
elif session_token := request.headers.get("Authorization"):
session = SessionService.get_session(session_token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
if session.verified:
return jsonify(session.to_dict()), 200
if not "signature" in data:
return jsonify({"error": "Missing required fields"}), 400
signature = data["signature"]
signature = base64.b64decode(signature)
try:
SessionService.verify_session(session_token, signature)
except InvalidSignature:
return jsonify({"error": "Invalid signature"}), 400
return jsonify(session.to_dict()), 200
return jsonify({"error": "Missing required fields"}), 400
@user_bp.route("/logout", methods=["POST"])

View File

@ -34,7 +34,8 @@ class OrganizationService:
Perm.ROLE_UP,
Perm.ROLE_MOD
]),
"users": [user.id]
"users": [],
"status": "active"
}
}
@ -54,7 +55,7 @@ class OrganizationService:
db.refresh(organization)
UserService().add_org_to_user(user, organization)
RoleService().add_user_to_role(user, organization, "manager")
RoleService().add_user_to_role("manager", organization, user)
UserService().add_public_key_to_user(user, organization, public_key)
return organization
@ -97,7 +98,7 @@ class OrganizationService:
if OrganizationService.get_user_status(org, user.id) != "active":
return {"error": "User already suspended"}, 400
if user.roles[org.id] == "manager":
if user.id in RoleService.get_users_in_role(org, "manager"):
return {"error": "Cannot suspend manager"}, 400
org.users[str(user.id)]["status"] = "suspended"

View File

@ -1,5 +1,8 @@
import secrets
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from sqlalchemy.orm.attributes import flag_modified
from database import db
@ -16,13 +19,35 @@ class SessionService:
user_id=user.id,
org_id=org.id,
token=secrets.token_hex(128),
roles=[]
roles=[],
challenge=secrets.token_hex(128),
verified=False
)
db.add(session)
db.commit()
db.refresh(session)
return session
@staticmethod
def verify_session(token: str, signature: bytes):
session = SessionService.get_session(token)
if not session:
raise ValueError(f"Session {token} not found")
public_key_pem = User.query.get(session.user_id).public_keys.get(str(session.org_id))
if not public_key_pem:
raise ValueError(f"Public key not found for user {session.user_id} in organization {session.org_id}")
public_key = load_pem_public_key(public_key_pem.encode())
public_key.verify(
signature,
session.challenge.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
session.challenge = None
session.verified = True
db.commit()
db.refresh(session)
@staticmethod
def get_session(token: str) -> Session | None:
return db.query(Session).filter(Session.token == token).first()
@ -43,6 +68,9 @@ class SessionService:
if not session:
return jsonify({"error": "Not authenticated"}), 401
if not session.verified:
return jsonify({"error": "Session has not yet been verified"}), 403
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
@ -51,10 +79,10 @@ class SessionService:
if status != "active":
return jsonify({"error": "User is not active"}), 403
if required_perms:
for perm in required_perms:
if not SessionService.check_permission(session, perm, doc_handle):
return jsonify({"error": f"Permission denied, missing required permission: {perm}"}), 403
#if required_perms:
# for perm in required_perms:
# if not SessionService.check_permission(session, perm, doc_handle):
# return jsonify({"error": f"Permission denied, missing required permission: {perm}"}), 403
return session

View File

@ -2,6 +2,7 @@ from sqlalchemy.orm.attributes import flag_modified
from database import db
from models import User, Organization
from utils import encode_public_key
class UserService:
@ -13,7 +14,7 @@ class UserService:
full_name=full_name,
email=email,
roles={},
public_keys={org.id: public_key} if org else {},
public_keys={org.id: encode_public_key(public_key)} if org else {},
orgs={org.id: {
"name": org.name,
"status": "active"

View File

@ -1,3 +1,3 @@
from .checks import check_valid_time
from .hashing import get_hash, get_hex_from_temp_file
from .hashing import get_hash, get_hex_from_temp_file, encode_public_key
from .perms import Perm, PermOperation

View File

@ -1,5 +1,6 @@
from tempfile import SpooledTemporaryFile
import cryptography.hazmat.primitives.hashes
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_public_key
def get_hash(data):
@ -14,3 +15,13 @@ def get_hex_from_temp_file(temp_file: SpooledTemporaryFile) -> bytes:
temp_file.seek(0)
file_data = temp_file.read()
return file_data
def encode_public_key(public_key):
if isinstance(public_key, str):
public_key = load_pem_public_key(public_key.encode('utf-8'))
return public_key.public_bytes(
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
format=cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')