Init delivery2

Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
This commit is contained in:
Tiago Garcia 2024-12-11 16:18:12 +00:00
parent e00c032df8
commit 511458f770
Signed by: TiagoRG
GPG Key ID: DFCD48E3F420DB42
48 changed files with 2934 additions and 0 deletions

View File

@ -1 +1,126 @@
# SIO 2024 - Projeto 1
## Group
- João Pedro Fonseca Bastos - 113470 - joaop.bastos@ua.pt
- Rúben da Loura Cristóvão Gomes - 113435 - rlcg@ua.pt
- Tiago Rocha Garcia - 114184 - tiago.rgarcia@ua.pt
## API
### Usage
The API run as a RESTful service using the flask framework. A test API is hosted on the following URL: `https://sio.tiagorg.pt`.
To run, first create the virtual environment and install the dependencies:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
Then, run the API:
```bash
flask run --port <port>
```
*Note: The API is hosted on port 5000 by default.*
*Note: You can also run the API in debug mode with the flag `--debug`.*
### Endpoints
The API has a list of endpoints that require different permission levels to access.
Mainly, it's divided into 3 categories:
- Anonymous: No authentication required.
- Authenticated: Authentication required.
- Authorized: Authentication and permissions required.
#### Anonymous Endpoints
- `GET /`: Returns a ping message.
- `POST /reset`: Resets the database and deletes all data.
- Required headers:
- `Content-Type: application/json`
- Required payload fields:
- `password`: The reset password. *Note: The reset password is `123`.*
- `GET /org/list`: Returns a list of organizations.
- `POST /org/create`: Creates a new organization.
- Required headers:
- `Content-Type: application/json`
- Required payload fields:
- `name`: Organization name.
- `username`: Manager username.
- `full_name`: Manager full name.
- `email`: Manager email.
- `public_key`: Manager public key.
- `GET /file/get/<file_handle>/content`: Downloads the file content.
- `POST /user/login`: Logs in a user.
- Required headers:
- `Content-Type: application/json`
- Required payload fields:
- `org`: Organization name.
- `username`: User username.
- `password`: User password.
- `credentials_file`: User credentials file.
#### Authenticated Endpoints
- `GET /user/list`: Returns a list of users.
- Required headers:
- `Authorization: token`
- Optional payload parameters:
- `username`: Filter by username.
- `GET /file/list`: Returns a list of files.
- Required headers:
- `Authorization: token`
- Optional payload parameters:
- `username`: Filter by username.
- `datetime`: Filter by datetime. The datetime filter has the following fields:
- `value`: Epoch time in seconds.
- `relation`: `ot` | `eq` | `nt`. (One of the following: older than, equal to, newer than)
- `POST /user/logout`: Logs out a user.
- Required headers:
- `Authorization: token`
#### Authorized Endpoints
- `POST /user/create`: Creates a new user.
- Required headers:
- `Authorization: token`
- `Content-Type: application/json`
- Required payload fields:
- `username`: User username.
- `name`: User name.
- `email`: User email.
- `public_key`: User public key.
- `POST /user/<username>/suspend`: Suspends a user.
- Required headers:
- `Authorization: token`
- `POST /user/<username>/activate`: Activates a user.
- Required headers:
- `Authorization: token`
- `POST /file/upload/metadata`: Uploads file metadata.
- Required headers:
- `Authorization: token`
- `Content-Type: application/json`
- Required payload fields:
- `document_name`: Document name.
- `key`: Document key.
- `alg`: Document algorithm.
- `nonce`: Document nonce.
- `POST /file/upload/content`: Uploads file content, content-type must be `multipart/form-data`.
- Required headers:
- `Authorization: token`
- `Content-Type: multipart/form-data`
- Required payload fields:
- `content`: Document content.
- `GET /file/get/<document_handle>/metadata`: Downloads file metadata.
- Required headers:
- `Authorization: token`
- `POST /file/delete/<document_handle>`: Deletes a file.
- Required headers:
- `Authorization: token

View File

@ -0,0 +1,54 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
#session file - username
def activateSubject(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('username', nargs='?',default=None)
args = parser.parse_args()
if not args.session or not args.username:
logger.error("Need session file and username.")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/' + args.username + '/activate', headers={'Authorization': args.session['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
sys.exit(0)
if __name__ == '__main__':
activateSubject(sys.argv[1:])

View File

@ -0,0 +1,88 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
from subject import main
sys.path.append(os.path.abspath("../../"))
from lib.symmetric_encryption import *
from lib import digest
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - document name - file
def addDoc(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
parser.add_argument('file', nargs='?', default=None)
args = parser.parse_args()
if not args.session or not args.name or not args.file:
logger.error("Need session file, document's name and file to upload.")
sys.exit(1)
if (not os.path.isfile(os.path.join(BASE_DIR, args.session))):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
if (not os.path.isfile(os.path.join(BASE_DIR, args.file))):
logger.error("File '" + args.file + "' not found")
sys.exit(1)
#Get session file content
with open(os.path.join(BASE_DIR, args.session), 'r') as f:
args.session = json.load(f)
#Encrypt content
key, content, nonce = encrypt_file(BASE_DIR + args.file, BASE_DIR + 'encryptedText')
#Upload document metadata
doc = {'document_name' : args.name, 'key' : key.hex(), 'alg' : 'AES-CFB', 'nonce' : nonce.hex() }
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/file/upload/metadata', json=json.dumps(doc),
headers={'Authorization': args.session['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
#Upload Document content
file = {'file' : (BASE_DIR + args.file, content)}
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/file/upload/content',
files=file,
headers={'Authorization': args.session['token'], 'File-Checksum' : digest.get_hash(content)})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
os.remove(BASE_DIR + 'encryptedText')
sys.exit(1)
#Delete temporary file
os.remove(BASE_DIR + 'encryptedText')
sys.exit(0)
if __name__ == '__main__':
addDoc(sys.argv[1:])

View File

@ -0,0 +1,69 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
from subject import main
sys.path.append(os.path.abspath('../../'))
from lib import asymmetric_functs
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - username - name - email - credentials file
def addSubject(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('username', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
parser.add_argument('email', nargs='?', default=None)
parser.add_argument('credentials', nargs='?', default=None)
args = parser.parse_args()
if not args.session or not args.username or not args.name or not args.email or not args.credentials:
logger.error("Need session file, username, name, email and credentials file.")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
if (not os.path.isfile(BASE_DIR + args.credentials)):
logger.error("File '" + args.file + "' not found")
sys.exit(1)
pubKey = asymmetric_functs.load_public_key(BASE_DIR + args.credentials)
subject = {'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey}
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/create', json=json.dumps(subject), headers={'Authorization': args.session['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
sys.exit(0)
if __name__ == '__main__':
addSubject(sys.argv[1:])

View File

@ -0,0 +1,71 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import re
import argparse
sys.path.append(os.path.abspath("../"))
from subject import main
sys.path.append(os.path.abspath('../../'))
from lib import asymmetric_functs
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# Create organization
# organization - username - name - email - public key file
def createOrganization(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('org', nargs='?', default=None)
parser.add_argument('username', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
parser.add_argument('email', nargs='?', default=None)
parser.add_argument('pubkey', nargs='?', default=None)
args = parser.parse_args()
if not args.org or not args.username or not args.name or not args.email or not args.pubkey:
logger.error("Need organization, username, name, email and key file.")
sys.exit(1)
# Validate email
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', args.email):
logger.error("Need a valid email.")
sys.exit(1)
# Validate key file
if (not os.path.isfile(BASE_DIR + args.pubkey)):
logger.error("File '" + args.pubkey + "' not found.")
sys.exit(1)
# load public key from file
pubKey = asymmetric_functs.load_public_key(BASE_DIR + args.pubkey)
input = {'name' : args.org, 'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey}
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/org/create', json=json.dumps(input))
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
sys.exit(0)
if __name__ == '__main__':
createOrganization(sys.argv[1:])

View File

@ -0,0 +1,64 @@
#!/bin/python3
import os
import sys
import argparse
import logging
import json
import requests
from subject import main
# Identity attributes
# {'username' : '', 'full_name' : '', 'email' : '', public_key : '' }
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# org - username - password - credentials file - session file
def createSession(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('org', nargs='?', default=None)
parser.add_argument('username', nargs='?', default=None)
parser.add_argument('password', nargs='?', default=None)
parser.add_argument('credentials', nargs='?', default=None)
parser.add_argument('session', nargs='?', default=None)
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")
sys.exit(1)
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}
#print( type(json.dumps(session)))
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps(session))
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)
sys.exit(0)
if __name__ == '__main__':
createSession(sys.argv[1:])

View File

@ -0,0 +1,53 @@
#!/bin/python3
import os
import sys
import logging
import argparse
import json
sys.path.append(os.path.abspath("../../"))
from lib import symmetric_encryption
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
#send to stdout contents of decrypted file
# encrypted file - encryption metadata
def decryptFile(args):
parser = argparse.ArgumentParser()
parser.add_argument('encrypted', nargs='?', default=None)
parser.add_argument('metadata', nargs='?', default=None)
args = parser.parse_args()
if not args.encrypted or not args.metadata:
logger.error("Need encrypted file and it's metadata.")
sys.exit(1)
# If first argument is not a file or not found
if (not os.path.isfile(BASE_DIR + args.encrypted)):
logger.error("File '" + args.encrypted + "' not found.")
sys.exit(1)
# if (not os.path.isfile(BASE_DIR + args.metadata)):
# logger.error("File '" + args.metadata + "' not found.")
# sys.exit(1)
#Decrypt file
print(args.metadata)
metadata = json.loads(args.metadata)
content = symmetric_encryption.decrypt_file(metadata['nonce'].encode(), metadata['key'].encode(), BASE_DIR + args.encrypted)
# Send decrypted content to stdout
sys.stdout.write(content)
sys.exit(0)
if __name__ == '__main__':
decryptFile(sys.argv[1:])

View File

@ -0,0 +1,60 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
sys.path.append(os.path.abspath("../../"))
from lib import digest
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - document name
def delDoc(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
args = parser.parse_args()
if not args.session or not args.name:
logger.error("Need session file and document's name.")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
doc_name = digest.get_hash(bytes(args.name, encoding='utf-8'))
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/file/delete/' + doc_name, headers={'Authorization': args.session['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
sys.exit(0)
if __name__ == '__main__':
delDoc(sys.argv[1:])

View File

@ -0,0 +1,92 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
from subject import main
sys.path.append(os.path.abspath("../../"))
from lib import digest
from lib import symmetric_encryption
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - document name - output file(optional)
def getDoc(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
parser.add_argument('output', nargs='?', default=None)
args = parser.parse_args()
if not args.session or not args.name:
logger.error("Need session file and document's name.")
sys.exit(-1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(-1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
# Get Document metadata
doc_name = digest.get_hash(bytes(args.name, encoding='utf-8'))
try:
metadata = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + doc_name + '/metadata', headers={'Authorization': args.session['token']})
metadata.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(1)
metadata = metadata.json()
#Get file with file_handle provided by metadata
try:
file = requests.get(f'http://{state['REP_ADDRESS']}/get/' + metadata['file_handle'] + '/content')
file.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
file = file.json()
if not digest.get_hash(file) == metadata['file_handle']:
logger.error("Files integrity is lost.")
sys.exit(-1)
content = symmetric_encryption.decrypt_file(file)
if args.output:
with open(BASE_DIR + args.output, 'w') as f:
f.write(content)
else:
sys.stdout.write(content)
sys.exit(0)
if __name__ == '__main__':
getDoc(sys.argv[1:])

View File

@ -0,0 +1,64 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from lib import digest
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - document name
def getDocMetadata(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('name', nargs='?', default=None)
args = parser.parse_args()
if not args.session or not args.name:
logger.error("Need session file and document's name.")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
doc_name = digest.get_hash(bytes(args.name, encoding='utf-8'))
try:
metadata = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + doc_name + '/metadata', headers={'Authorization': args.session['token']})
metadata.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
metadata = metadata.json()
sys.stdout.write(json.dumps(metadata))
sys.exit(0)
if __name__ == '__main__':
getDocMetadata(sys.argv[1:])

View File

@ -0,0 +1,64 @@
#!/bin/python3
import os
import sys
import logging
import json
import requests
import argparse
from subject import main
# Identity attributes
# {'username' : '', 'full_name' : '', 'email' : '', public_key : '' }
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
#get file
#file handle - file(optonal)
def getFile(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('filehandle', nargs='?', default=None)
parser.add_argument('file', nargs='?', default=None)
args = parser.parse_args()
if not args.filehandle:
logger.error("Need a file handle.")
sys.exit(1)
#else:
# if not os.path.isfile(BASE_DIR + args.filehandle):
# logger.error("File '" + args.filehandle + "' not found" )
# sys.exit(1)
#Get file
try:
file = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + args.filehandle + '/content')
file.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
print(file)
if not args.file:
sys.stdout.write(file.text)
else:
with open(BASE_DIR + args.file, "wb") as f:
f.write(file.content)
sys.exit(0)
if __name__ == '__main__':
getFile(sys.argv[1:])

View File

@ -0,0 +1,128 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
import datetime
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
def validDate(input):
try:
date = datetime.datetime.strptime(input,"%d/%m/%Y")
except ValueError as err:
logger.error("Date is invalid. Input format: DD/MM/YYYY")
sys.exit(-1)
return date
#session file - [-s username] [-d nt/ot/et date]
def list_docs(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument("-s", '--username', nargs=1, help="Username")
parser.add_argument("-dnt", '--newerThan', help="Date new than")
parser.add_argument("-dot", '--olderThan', help="Date older than")
parser.add_argument("-deq", '--equalTo', help="Date equal to")
args = parser.parse_args()
# Check if session file is valid
if args.session:
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
else:
logger.error("Need session file.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
if args.newerThan:
#Convert date str to datetime in seconds
args.newerThan = validDate(args.newerThan)
args.newerThan = int(args.newerThan.timestamp())
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list',
json=json.dumps({'username' : args.username,'datetime' : {'value' : args.newerThan, 'relation' : 'nt'}}),
headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
subjects = subjects.json()
elif args.equalTo:
#Convert date str to datetime in seconds
args.equalTo = validDate(args.equalTo)
args.equalTo = int(args.equalTo.timestamp())
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list',
json=json.dumps({'username' : args.username,
'datetime' : {'value' : args.equalTo, 'relation' : 'eq'}}),
headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
subjects = subjects.json()
elif args.olderThan:
#Convert date str to datetime in seconds
args.olderThan = validDate(args.olderThan)
args.olderThan = int(args.olderThan.timestamp())
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list',
json=json.dumps({'username' : args.username,
'datetime' : {'value' : args.olderThan, 'relation' : 'ot'}}),
headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
subjects = subjects.json()
else:
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list', json=json.dumps({}), headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
subjects = subjects.json()
for s in subjects:
sys.stdout.write(s['id'] + " - " + s['username'])
sys.exit(0)
if __name__ == '__main__':
list_docs(sys.argv[1:])

View File

@ -0,0 +1,34 @@
#!/bin/python3
import os
import sys
import logging
import json
import requests
from subject import main
# Identity attributes
# {'username' : '', 'full_name' : '', 'email' : '', public_key : '' }
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
def listOrganizations():
try:
orgs = requests.get(f'http://{state['REP_ADDRESS']}/org/list')
orgs.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
for org in orgs.json():
sys.stdout.write(str(org['id']) + " - " + org['name'])
sys.exit(0)
if __name__ == '__main__':
listOrganizations()

View File

@ -0,0 +1,74 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - username(optional)
def list_subjects(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('username', nargs='?', default=None)
args = parser.parse_args()
# Check if session file is valid
if args.session:
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
else:
logger.error("Need session file.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
if args.username:
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/user/list',
json=json.dumps({'username' : args.username}),
headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
else:
try:
subjects = requests.get(f'http://{state['REP_ADDRESS']}/user/list',
json=json.dumps({}),
headers={'Authorization': args.session['token']})
subjects.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
for s,d in subjects.json().items():
sys.stdout.write(s + " - " + d['username'] + "\n")
sys.exit(0)
if __name__ == '__main__':
list_subjects(sys.argv[1:])

View File

@ -0,0 +1,40 @@
#!/bin/python3
import os
import sys
import logging
import argparse
sys.path.append(os.path.abspath("../../"))
from lib import key_pair
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# Generate a key pair for a subject
# password - file for public key, file for private key
def generateKeyPair(args):
parser = argparse.ArgumentParser()
parser.add_argument('password', nargs='?', default=None)
parser.add_argument('pubfile', nargs='?', default=None)
parser.add_argument('privfile', nargs='?', default=None)
args = parser.parse_args()
if not args.password or not args.pubfile or not args.privfile:
logger.error("Need password and file to store keys")
sys.exit(1)
#Generate the key pair
key_pair.generate_key_pair(BASE_DIR + args.pubfile, BASE_DIR + args.privfile, 2048, args.password)
sys.exit(0)
if __name__ == '__main__':
generateKeyPair(sys.argv[1:])

View File

@ -0,0 +1,55 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
sys.path.append(os.path.abspath("../"))
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
# session file - username
def suspendSubject(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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('session', nargs='?', default=None)
parser.add_argument('username', nargs='?',default=None)
args = parser.parse_args()
if not args.session or not args.username:
logger.error("Need session file and username.")
sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.session)):
logger.error("File '" + args.session + "' not found.")
sys.exit(1)
# Get session file content
with open(BASE_DIR + args.session, 'r') as f:
args.session = json.load(f)
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/' + args.username + '/suspend', headers={'Authorization': args.session['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
sys.exit(0)
if __name__ == '__main__':
suspendSubject(sys.argv[1:])

View File

@ -0,0 +1,42 @@
#!/bin/python3
import os
import sys
import logging
import requests
import json
import argparse
sys.path.append(os.path.abspath("../"))
from subject import main
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
state = main(sys.argv)
def reset(args):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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)
args = parser.parse_args()
if not args.password:
logger.error("Need password")
sys.exit(1)
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/reset',json=json.dumps({'password' : args.password}))
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.")
sys.exit(-1)
if __name__ == '__main__':
reset(sys.argv[1:])

116
delivery2/client/bin/subject.py Executable file
View File

@ -0,0 +1,116 @@
import os
import sys
import argparse
import logging
import json
import requests
# Identity attributes
# {'username' : '', 'full_name' : '', 'email' : '', public_key : '' }
logging.basicConfig(format='%(levelname)s\t- %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def load_state():
state = {}
state_dir = os.path.join(os.path.expanduser('~'), '.sio')
state_file = os.path.join(state_dir, 'state.json')
logger.debug('State folder: ' + state_dir)
logger.debug('State file: ' + state_file)
if os.path.exists(state_file):
logger.debug('Loading state')
with open(state_file,'r') as f:
state = json.loads(f.read())
if state is None:
state = {}
return state
def parse_env(state):
if 'REP_ADDRESS' in os.environ:
state['REP_ADDRESS'] = os.getenv('REP_ADDRESS')
logger.debug('Setting REP_ADDRESS from Environment to: ' + state['REP_ADDRESS'])
if 'REP_PUB_KEY' in os.environ:
rep_pub_key = os.getenv('REP_PUB_KEY')
logger.debug('Loading REP_PUB_KEY fron: ' + state['REP_PUB_KEY'])
if os.path.exists(rep_pub_key):
with open(rep_pub_key, 'r') as f:
state['REP_PUB_KEY'] = f.read()
logger.debug('Loaded REP_PUB_KEY from Environment')
return state
def parse_args(state):
parser = argparse.ArgumentParser()
parser.add_argument("-k", '--key', nargs=1, help="Path to the key file")
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("-c", "--command", help="Command to execute")
parser.add_argument('arg0', nargs='?', default=None)
parser.add_argument('arg1', nargs='?', default=None)
parser.add_argument('arg2', nargs='?', default=None)
parser.add_argument('arg3', nargs='?', default=None)
parser.add_argument('arg4', nargs='?', default=None)
parser.add_argument('arg5', nargs='?', default=None)
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
logger.info('Setting log level to DEBUG')
if args.key:
if not os.path.exists(args.key[0]) or not os.path.isfile(args.key[0]):
logger.error(f'Key file not found or invalid: {args.key[0]}')
sys.exit(1)
with open(args.key[0], 'r') as f:
state['REP_PUB_KEY'] = f.read()
logger.info('Overriding REP_PUB_KEY from command line')
if args.repo:
state['REP_ADDRESS'] = args.repo[0]
logger.info('Overriding REP_ADDRESS from command line')
#if args.command:
# logger.info("Command: " + args.command)
return state#, {'command': args.command, 'arg0': args.arg0, 'arg1': args.arg1, 'arg2': args.arg2, 'arg3': args.arg3, 'arg4': args.arg4, 'arg5': args.arg5}
def save(state):
state_dir = os.path.join(os.path.expanduser('~'), '.sio')
state_file = os.path.join(state_dir, 'state.json')
if not os.path.exists(state_dir):
logger.debug('Creating state folder')
os.mkdir(state_dir)
with open(state_file, 'w') as f:
f.write(json.dumps(state, indent=4))
#Main function for checking commands
def main(args):
state = load_state()
state = parse_env(state)
#state, args = parse_args(state)
state = parse_args(state)
if 'REP_ADDRESS' not in state:
logger.error("Must define Repository Address")
sys.exit(1)
# if 'REP_PUB_KEY' not in state:
# logger.error("Must set the Repository Public Key")
# sys.exit(1)
save(state)
return state
if __name__ == '__main__':
main(sys.argv)

View File

@ -0,0 +1,128 @@
import json
import os, subprocess, sys
import requests
DELIVERY_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
FILES_PATH = os.path.join(os.path.expanduser('~'), '.sio/')
# !!! database.db must be deleted/reset before running the tests !!!
requests.post("http://localhost:5000/reset", json={"password": "123"})
os.system(f"rm {FILES_PATH}*")
def test_address_set():
# Initialize the server path on state.json
process = subprocess.Popen(f"python3 {DELIVERY_PATH}/client/bin/subject.py -r localhost:5000 ", shell=True)
process.wait()
assert os.path.exists(os.path.join(FILES_PATH, 'state.json'))
def test_rep_subject_credentials():
# Test the rep_subject_create command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_subject_credentials password pub.pem priv.pem ", shell=True)
process.wait()
assert os.path.exists(os.path.join(FILES_PATH, 'pub.pem')) and os.path.exists(os.path.join(FILES_PATH, 'priv.pem'))
def test_rep_create_org():
# Test the rep_create_org command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_create_org org1 username name email@org.com pub.pem", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_list_orgs():
# Test the list_orgs command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_list_orgs", shell=True)
process.wait()
assert process.returncode == 0
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.wait()
assert process.returncode == 0
def test_rep_list_subjects():
#Test the rep_list_subjects command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_list_subjects session.json", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_add_subject():
# Test the rep_subject_create command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_subject_credentials password pub_extra.pem priv_extra.pem ", shell=True)
process.wait()
assert os.path.exists(os.path.join(FILES_PATH, 'pub_extra.pem')) and os.path.exists(os.path.join(FILES_PATH, 'priv_extra.pem'))
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_add_subject session.json username2 name2 name2@any.com pub_extra.pem", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_suspend_subject():
# Test the rep_suspend_subject command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_suspend_subject session.json username2", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_activate_subject():
# Test the rep_activate_subject command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_activate_subject session.json username2", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_add_doc():
# Test the rep_add_doc command
process = subprocess.Popen(f"dd if=/dev/urandom of={FILES_PATH}test.txt bs=1024 count=1000 ", shell=True)
process.wait()
assert process.returncode == 0
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_add_doc session.json doc test.txt ", shell=True)
process.wait()
assert process.returncode == 0
metadata = {}
def test_rep_get_doc_metadata():
# Test the rep_get_doc_metadata command
global metadata
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_get_doc_metadata session.json doc", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
process.wait()
stdout, stderr = process.communicate()
metadata = json.loads(stdout)
assert process.returncode == 0
def test_rep_get_file():
# Test the rep_get_file command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_get_file {metadata['file_handle']} file.txt ", shell=True)
process.wait()
assert process.returncode == 0
def test_decrypt_file():
# Test the rep_decrypt_file command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_decrypt_file file.txt '{json.dumps(metadata)}'", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_get_doc_file():
# Test the rep_get_doc_file
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_get_doc_file session.json doc ", shell=True)
process.wait()
assert process.returncode == 0
def test_rep_delete_doc():
# Test the rep_get_doc_file
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_delete_doc session.json doc ", shell=True)
process.wait()
assert process.returncode == 0

View File

@ -0,0 +1,142 @@
import sys, os
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# -----------------
# encrypt functions
# -----------------
# function to generate a 256-bit symmetric key
def generate_symmetric_key():
return os.urandom(32)
# function to encrypt data using a symmetric key
def encrypt_symmetric(key, plain_text):
# generate a random IV
iv = os.urandom(16)
# cipher the data using AES in CFB mode
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plain_text) + encryptor.finalize()
return iv + ciphertext
# function that calls and combines the symmetric and asymmetric encryption
def encrypt_hybrid(public_key, plaintext):
# generate a random symmetric key
symmetric_key = generate_symmetric_key()
encrypted_data = encrypt_symmetric(symmetric_key, plaintext)
# encrypt the symmetric key with the public key
encrypted_symmetric_key = public_key.encrypt(
symmetric_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# combine the symmetric key and the encrypted data
return encrypted_symmetric_key + encrypted_data
# main function to encrypt the file
def encrypt_file(public_key, original_file, encrypted_file):
with open(original_file, 'rb') as f:
plaintext = f.read()
encrypted_content = encrypt_hybrid(public_key, plaintext)
with open(encrypted_file, 'wb') as f:
f.write(encrypted_content)
# function to load a public key from a file
def load_public_key(file):
with open(file, 'rb') as key_file:
public_key = serialization.load_pem_public_key(
key_file.read(),
)
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
return public_key_pem
# -----------------
# decrypt functions
# -----------------
# function to decrypt data using a symmetric key
def decrypt_symmetric(key, ciphertext):
# generate a random IV
iv = ciphertext[:16]
# decipher the data using AES in CFB mode
ciphertext = ciphertext[16:]
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend())
decryptor = cipher.decryptor()
return decryptor.update(ciphertext) + decryptor.finalize()
# function that calls and combines the symmetric and asymmetric decryption
def decrypt_hybrid(private_key, encrypted_data):
# extract the encrypted symmetric key and the encrypted data (remember that the data is symmetric + asymmetric)
encrypted_symmetric_key = encrypted_data[:private_key.key_size // 8]
encrypted_data = encrypted_data[private_key.key_size // 8:]
# decrypt the symmetric key using the RSA private key
symmetric_key = private_key.decrypt(
encrypted_symmetric_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# decrypt the data using the decrypted symmetric key
return decrypt_symmetric(symmetric_key, encrypted_data)
# main function to decrypt the file
def decrypt_file(private_key, encrypted_file, decrypted_file=None):
with open(encrypted_file, 'rb') as f:
encrypted_content = f.read()
decrypted_content = decrypt_hybrid(private_key, encrypted_content)
if decrypted_file is None:
return decrypted_content
else:
with open(decrypted_file, 'wb') as f:
f.write(decrypted_content)
# function to load a private key from a file
def load_private_key(file, passwd=None):
if passwd is not None:
passwd = passwd.encode('utf-8')
try:
with open(file, 'rb') as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=passwd,
)
except ValueError as e:
raise ValueError("Error: The password is not valid.") from e
return private_key

7
delivery2/lib/digest.py Normal file
View File

@ -0,0 +1,7 @@
import cryptography.hazmat.primitives.hashes
def get_hash(data):
digest = cryptography.hazmat.primitives.hashes.Hash(cryptography.hazmat.primitives.hashes.SHA256())
digest.update(data)
return digest.finalize().hex()

34
delivery2/lib/key_pair.py Normal file
View File

@ -0,0 +1,34 @@
import sys
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
def generate_key_pair(pub_name, priv_name, key_size, passwd=None):
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=int(key_size)
)
public_key = private_key.public_key()
with open(pub_name, 'wb') as f:
f.write(public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
))
if not passwd:
with open(priv_name, 'wb') as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
else:
with open(priv_name, 'wb') as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.BestAvailableEncryption(passwd.encode())
))

View File

@ -0,0 +1,46 @@
import os
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# Function to encrypt a file using a salt
def encrypt_file(input_file, output_file=None):
key = os.urandom(16)
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CFB(iv))
encryptor = cipher.encryptor()
with open(input_file, 'rb') as f:
plaintext = f.read()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
ciphertext = iv + ciphertext
if output_file is not None:
with open(output_file, 'wb') as f:
f.write(ciphertext)
print(iv.hex())
return key, ciphertext, iv
# Function to decrypt a file
def decrypt_file(nonce, key, input_file, output_file=None):
with open(input_file, 'rb') as f:
encrypted_data = f.read()
ciphertext = encrypted_data
cipher = Cipher(algorithms.AES(key), modes.CFB(nonce))
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
if output_file is not None:
with open(output_file, 'wb') as f:
f.write(plaintext)
return plaintext.hex()

View File

@ -0,0 +1,36 @@
import os
from digest import *
def test_equal_string():
string_one = "Hello, World!"
string_two = "Hello, World!"
assert get_hash(bytes(string_one, 'utf-8')) == get_hash(bytes(string_two, 'utf-8'))
def test_diff_string():
string_one = "Hello, World!"
string_two = "Hello, World"
assert get_hash(bytes(string_one, 'utf-8')) != get_hash(bytes(string_two, 'utf-8'))
def test_equal_file():
# create equal files
os.system("dd if=/dev/zero of=test.txt bs=1024 count=1000 >/dev/null 2>&1")
os.system("dd if=/dev/zero of=test2.txt bs=1024 count=1000 >/dev/null 2>&1")
assert get_hash(open("test.txt", "rb").read()) == get_hash(open("test2.txt", "rb").read())
os.remove("test.txt")
os.remove("test2.txt")
def test_diff_file():
# create different files
os.system("dd if=/dev/urandom of=test.txt bs=1024 count=1000 >/dev/null 2>&1")
os.system("dd if=/dev/urandom of=test2.txt bs=1024 count=1000 >/dev/null 2>&1")
assert get_hash(open("test.txt", "rb").read()) != get_hash(open("test2.txt", "rb").read())
os.remove("test.txt")
os.remove("test2.txt")

View File

@ -0,0 +1,186 @@
import os
from asymmetric_functs import *
from key_pair import *
def test_encryption_no_pwd():
# create a file to encrypt
with open("test.txt", "w") as f:
f.write("Hello, World!")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem")
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
with open("test.dec", "r") as f:
assert f.read() == "Hello, World!"
# cleanup
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")
def test_encryption_with_pwd():
# create a file to encrypt
with open("test.txt", "w") as f:
f.write("Hello, World!")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048', 'password')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem", 'password')
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
with open("test.dec", "r") as f:
assert f.read() == "Hello, World!"
# remove the files
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")
def test_load_private_key_wrong_pwd():
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048', 'password')
# try to load the private key with the wrong password
try:
load_private_key("private.pem", 'wrong_password')
except ValueError as e:
assert str(e) == "Error: The password is not valid."
def test_1mb_file_with_pwd():
# create a 1mb file to encrypt
os.system("dd if=/dev/urandom of=test.txt bs=1024 count=1000 >/dev/null 2>&1")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048', 'password')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem", 'password')
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
assert open("test.txt", "rb").read() == open("test.dec", "rb").read()
# remove the files
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")
def test_1mb_file_no_pwd():
# create a 1mb file to encrypt
os.system("dd if=/dev/urandom of=test.txt bs=1024 count=1000 >/dev/null 2>&1")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem")
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
assert open("test.txt", "rb").read() == open("test.dec", "rb").read()
# remove the files
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")
def test_100mb_file_with_pwd():
# create a 100mb file to encrypt
os.system("dd if=/dev/urandom of=test.txt bs=1024 count=100000 >/dev/null 2>&1")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048', 'password')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem", 'password')
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
assert open("test.txt", "rb").read() == open("test.dec", "rb").read()
# remove the files
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")
def test_100mb_file_no_pwd():
# create a 100mb file to encrypt
os.system("dd if=/dev/urandom of=test.txt bs=1024 count=100000 >/dev/null 2>&1")
# generate a key pair
generate_key_pair('public.pem', 'private.pem', '2048')
# load the public and private keys
public_key = load_public_key("public.pem")
private_key = load_private_key("private.pem")
# encrypt the file
encrypt_file(public_key, "test.txt", "test.enc")
# decrypt the file
decrypt_file(private_key, "test.enc", "test.dec")
# check that the decrypted file is the same as the original
assert open("test.txt", "rb").read() == open("test.dec", "rb").read()
# remove the files
os.remove("test.txt")
os.remove("test.enc")
os.remove("test.dec")
os.remove("public.pem")
os.remove("private.pem")

52
delivery2/server/app.py Normal file
View File

@ -0,0 +1,52 @@
import os
import sqlalchemy.exc
from flask import Flask, request, jsonify
from routes import org_bp, user_bp, file_bp
from database import db_connection, db
from models import Organization, User, File, Session
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
app.config["SQLALCHEMY_AUTOCOMMIT"] = False
app.config["SQLALCHEMY_AUTOFLUSH"] = False
db_connection.init_app(app)
with app.app_context():
try:
db_connection.session.query(Session).delete()
db_connection.session.commit()
except sqlalchemy.exc.OperationalError:
pass
db_connection.create_all()
app.register_blueprint(org_bp, url_prefix="/org")
app.register_blueprint(user_bp, url_prefix="/user")
app.register_blueprint(file_bp, url_prefix="/file")
@app.route("/", methods=["GET"])
def index():
return jsonify({"message": "Welcome to the API"}), 200
@app.route("/reset", methods=["POST"])
def reset():
password = request.json["password"]
if password != "123":
return jsonify({"error": "Invalid password"}), 403
try:
with app.app_context():
db_connection.drop_all()
db_connection.create_all()
repos = os.path.join(os.path.dirname(os.path.abspath(__file__)), "repository")
for repo in os.listdir(repos):
if os.path.isdir(os.path.join(repos, repo)):
for file in os.listdir(os.path.join(repos, repo)):
os.remove(os.path.join(repos, repo, file))
os.rmdir(os.path.join(repos, repo))
except sqlalchemy.exc.OperationalError:
return jsonify({"error": "Database error"}), 500
return jsonify({"message": "Database reset"}), 200
if __name__ == "__main__":
app.run(debug=True)

View File

@ -0,0 +1,4 @@
from flask_sqlalchemy import SQLAlchemy
db_connection = SQLAlchemy()
db = db_connection.session

View File

@ -0,0 +1,4 @@
from .user import *
from .org import *
from .file import *
from .session import *

View File

@ -0,0 +1,32 @@
from database import db_connection
class File(db_connection.Model):
__tablename__ = 'files'
id = db_connection.Column(db_connection.Integer, primary_key=True, index=True)
file_handle = db_connection.Column(db_connection.String, unique=True, nullable=True)
document_handle = db_connection.Column(db_connection.String, unique=True, nullable=False)
name = db_connection.Column(db_connection.String, nullable=False)
created_at = db_connection.Column(db_connection.Integer, nullable=False)
key = db_connection.Column(db_connection.String, nullable=False)
alg = db_connection.Column(db_connection.String, nullable=False)
nonce = db_connection.Column(db_connection.String, nullable=False)
org_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('organizations.id'), nullable=False)
creator_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id'), nullable=False)
org = db_connection.relationship('Organization', backref=db_connection.backref('org_files', uselist=False))
creator = db_connection.relationship('User', backref=db_connection.backref('created_files', uselist=False))
def to_dict(self):
return {
"id": self.id,
"file_handle": self.file_handle,
"document_handle": self.document_handle,
"name": self.name,
"created_at": self.created_at,
"key": self.key,
"alg": self.alg,
"nonce": self.nonce,
"org": {"id": self.org.id, "name": self.org.name},
"creator": {"id": self.creator.id, "username": self.creator.username}
}

View File

@ -0,0 +1,23 @@
from database import db_connection
class Organization(db_connection.Model):
__tablename__ = 'organizations'
id = db_connection.Column(db_connection.Integer, primary_key=True, index=True)
name = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False)
users = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
users_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id'))
manager = db_connection.relationship('User', backref=db_connection.backref('owned_organization', uselist=False))
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"manager": self.manager.to_dict(),
"users": [{"id": user_id, "user_data": {
"username": user_data["username"],
"full_name": user_data["full_name"],
"email": user_data["email"],
"status": user_data["status"]
}} for user_id, user_data in self.users.items()],
}

View File

@ -0,0 +1,21 @@
from database import db_connection
class Session(db_connection.Model):
__tablename__ = 'sessions'
id = db_connection.Column(db_connection.Integer, primary_key=True)
user_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id'))
org_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('organizations.id'))
token = db_connection.Column(db_connection.String(255), unique=True)
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())
def to_dict(self):
return {
"id": self.id,
"user_id": self.user_id,
"org_id": self.org_id,
"token": self.token,
"created_at": self.created_at,
"updated_at": self.updated_at
}

View File

@ -0,0 +1,22 @@
from flask_sqlalchemy import SQLAlchemy
from database import db_connection
class User(db_connection.Model):
__tablename__ = 'users'
id = db_connection.Column(db_connection.Integer, primary_key=True, index=True)
username = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False)
full_name = db_connection.Column(db_connection.String, nullable=False)
email = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False)
public_keys = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
orgs = db_connection.Column(db_connection.JSON, nullable=False, default=dict)
def to_dict(self):
return {
"id": self.id,
"username": self.username,
"full_name": self.full_name,
"email": self.email,
"orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()],
}

View File

View File

@ -0,0 +1,3 @@
cryptography
flask
flask_sqlalchemy

View File

@ -0,0 +1,3 @@
from .org import org_bp
from .user import user_bp
from .file import file_bp

View File

@ -0,0 +1,190 @@
import json
from flask import Blueprint, request, jsonify, send_file, Response
import utils
from services import FileService, OrganizationService, UserService, SessionService
file_bp = Blueprint("file", __name__)
upload_service = FileService()
@file_bp.route("/get/<string:file_handle>/content", methods=["GET"])
def file_get_content(file_handle: str):
file = FileService.get_file_by_file_handle(file_handle)
if not file:
return jsonify({"error": "File not found"}), 404
file_content = FileService.get_file_content(file)
return send_file(file_content, as_attachment=True, download_name=file.name)
@file_bp.route("/get/<string:document_handle>/metadata", methods=["GET"])
def file_get_metadata(document_handle: str):
session_token = request.headers.get("Authorization")
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
file = FileService.get_file_by_document_handle(document_handle)
if not file:
return jsonify({"error": "File not found"}), 404
return jsonify(file.to_dict())
@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)
if isinstance(session, tuple):
return session
data = request.json
if type(data) is str:
data = json.loads(data)
if "document_name" not in data or "key" not in data or "alg" not in data or "nonce" not in data:
return jsonify({"error": "Missing required fields"}), 400
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
user = UserService.get_user(session.user_id)
if not user:
return jsonify({"error": "User not found"}), 404
file = upload_service.create_file(session.token, org, user, data["document_name"], data["key"], data["alg"], data["nonce"])
return jsonify(file.to_dict()), 201
@file_bp.route("/upload/content", methods=["POST"])
def file_upload_content():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
if "file" not in request.files:
return jsonify({"error": "No file data"}), 400
file = request.files.get("file")
if file.filename == "":
return jsonify({"error": "No file selected for uploading"}), 400
if not file:
return jsonify({"error": "Invalid file data"}), 400
file_data = utils.get_hex_from_temp_file(file.stream)
file_sum = request.headers.get("File-Checksum")
if not file_sum:
return jsonify({"error": "No file checksum provided"}), 400
if file_sum != str(utils.get_hash(file_data)):
return jsonify({"error": "File checksum mismatch"}), 400
file = upload_service.write_file(session_token, file_sum, file_data)
if isinstance(file, tuple):
return file
return jsonify(file.to_dict()), 201
@file_bp.route("/list", methods=["GET"])
def file_list():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
data = request.json
if type(data) is str:
data = json.loads(data)
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if "datetime" in data:
try:
datetime_value = int(data["datetime"]["value"])
datetime_relation = data["datetime"]["relation"]
except ValueError:
return jsonify({"error": "Invalid datetime value"}), 400
if "username" in data:
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
files = FileService.list_files_in_org(org)
return jsonify([file.to_dict() for file in files if file.creator_id == user.id and (
utils.check_valid_time(file.created_at, datetime_value, datetime_relation)
if "datetime" in data else True
)])
files = FileService.list_files_in_org(org)
return jsonify([file.to_dict() for file in files if (utils.check_valid_time(file.created_at, datetime_value, datetime_relation) if "datetime" in data else True)])
@file_bp.route("/delete/<string:document_handle>", methods=["POST"])
def file_delete(document_handle: str):
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
file = FileService.get_file_by_document_handle(document_handle)
if not file:
return jsonify({"error": "File not found"}), 404
if file.creator_id != session.user_id:
return jsonify({"error": "Not authorized to delete file"}), 403
file = FileService.delete_file(file)
return jsonify(file.to_dict())
################################################
@file_bp.route("/create_dummy", methods=["POST"])
def file_create_dummy():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
user = UserService.get_user(session.user_id)
if not user:
return jsonify({"error": "User not found"}), 404
file = FileService.create_dummy_file(org, user)
return jsonify(file.to_dict()), 201

View File

@ -0,0 +1,32 @@
import json
from flask import Blueprint, request, jsonify
from services import OrganizationService
org_bp = Blueprint("org", __name__)
@org_bp.route("/create", methods=["POST"])
def org_create():
data = request.json
if type(data) is str:
data = json.loads(data)
if "name" not in data or "username" not in data or "full_name" not in data or "email" not in data or "public_key" not in data:
return jsonify({"error": "Missing required fields"}), 400
existing_org = OrganizationService.get_organization_by_name(data["name"])
if existing_org:
return jsonify({"error": "Organization already exists"}), 400
org = OrganizationService.create_organization(
name=data["name"],
username=data["username"],
full_name=data["full_name"],
email=data["email"],
public_key=data["public_key"]
)
return jsonify(org.to_dict()), 201
@org_bp.route("/list", methods=["GET"])
def org_list():
orgs = OrganizationService.list_organizations()
return jsonify([org.to_dict() for org in orgs])

View File

@ -0,0 +1,152 @@
import json
import utils
from flask import Blueprint, request, jsonify
from services import UserService, SessionService, OrganizationService
user_bp = Blueprint("user", __name__)
@user_bp.route("/login", methods=["POST"])
def user_login():
data = request.json
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
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
session = SessionService.create_session(user, org)
return jsonify(session.to_dict()), 201
@user_bp.route("/logout", methods=["POST"])
def user_logout():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.get_session(session_token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
SessionService.delete_session(session)
return jsonify({"message": "Logged out"}), 200
@user_bp.route("/list", methods=["GET"])
def user_list():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
data = request.json
if type(data) is str:
data = json.loads(data)
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if "username" in data:
user = UserService.get_user_by_username(data["username"])
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user.to_dict()), 200
users = OrganizationService.get_users_in_organization(org)
return jsonify(users), 200
@user_bp.route("/create", methods=["POST"])
def user_create():
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
data = request.json
if type(data) is str:
data = json.loads(data)
if "username" not in data or "full_name" not in data or "email" not in data or "public_key" not in data:
return jsonify({"error": "Missing required fields"}), 400
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.manager.id != session.user_id:
return jsonify({"error": "Not authorized to create users"}), 403
user = UserService.get_user_by_username(data["username"])
if not user:
user = UserService.create_user(
username=data["username"],
full_name=data["full_name"],
email=data["email"],
public_key=data["public_key"],
org=org
)
return jsonify(user.to_dict()), 201
@user_bp.route("/<string:username>/suspend", methods=["POST"])
def user_suspend(username):
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.manager.id != session.user_id:
return jsonify({"error": "Not authorized to suspend users"}), 403
user = UserService.get_user_by_username(username)
if not user:
return jsonify({"error": "User not found"}), 404
return OrganizationService.suspend_user(org, user)
@user_bp.route("/<string:username>/activate", methods=["POST"])
def user_unsuspend(username):
session_token = request.headers.get("Authorization")
if not session_token:
return jsonify({"error": "No session token"}), 400
session = SessionService.validate_session(session_token)
if isinstance(session, tuple):
return session
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
if org.manager.id != session.user_id:
return jsonify({"error": "Not authorized to unsuspend users"}), 403
user = UserService.get_user_by_username(username)
if not user:
return jsonify({"error": "User not found"}), 404
return OrganizationService.activate_user(org, user)

View File

@ -0,0 +1,4 @@
from .orgs import OrganizationService
from .users import UserService
from .files import FileService
from .sessions import SessionService

View File

@ -0,0 +1,111 @@
import os
import io
from datetime import datetime
from typing import List, Type
from flask import jsonify
from database import db
from models import File, Organization, User
from utils import get_hash
class FileService:
def __init__(self):
self.current_requests = {}
def create_file(self, session_token: str, org: Organization, user: User, file_name: str, key: str, alg: str, nonce: str) -> File:
file = File(
file_handle = None,
document_handle = get_hash(file_name),
name = file_name,
created_at = int(datetime.now().timestamp()),
key = key,
alg = alg,
nonce = nonce,
org_id = org.id,
creator_id = user.id,
org = org,
creator = user
)
db.add(file)
db.commit()
db.refresh(file)
self.current_requests[session_token] = file.id
return file
def write_file(self, session_token: str, file_handle: str, file_data: bytes) -> File | tuple:
if session_token not in self.current_requests:
return jsonify({"error": "No file upload request found"}), 400
file = db.query(File).filter(File.id == self.current_requests[session_token]).first()
file_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "repository"), file.org.name, file.document_handle)
with open(file_path, "wb") as f:
f.write(file_data)
file.file_handle = file_handle
db.commit()
db.refresh(file)
del self.current_requests[session_token]
return file
@staticmethod
def create_dummy_file(org: Organization, user: User) -> File:
file = File(
file_handle = "dummy_file",
document_handle = "org/dummy_file.txt",
name = "dummy_file",
created_at = int(datetime.now().timestamp()),
org_id = 1,
creator_id = 1,
org = org,
creator = user
)
file_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "repository"), file.document_handle)
with open(file_path, "w") as f:
f.write("Dummy file content")
db.add(file)
db.commit()
db.refresh(file)
return file
@staticmethod
def get_file(file_id: int) -> File | None:
return db.query(File).filter(File.id == file_id).first()
@staticmethod
def get_file_by_document_handle(document_handle: str) -> File | None:
return db.query(File).filter(File.document_handle == document_handle).first()
@staticmethod
def get_file_by_file_handle(file_handle: str) -> File | None:
return db.query(File).filter(File.file_handle == file_handle).first()
@staticmethod
def get_file_content(file: File) -> io.BytesIO:
file_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "repository"), file.org.name, file.document_handle)
with open(file_path, "rb") as f:
return io.BytesIO(f.read())
@staticmethod
def list_files() -> list[Type[File]]:
return db.query(File).all()
@staticmethod
def list_files_in_org(org: Organization) -> list[Type[File]]:
return db.query(File).filter(File.org_id == org.id).all()
@staticmethod
def delete_file(file: File) -> File:
file_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "repository"), file.org.name, file.document_handle)
os.remove(file_path)
file.file_handle = None
db.commit()
db.refresh(file)
return file

View File

@ -0,0 +1,99 @@
import os.path
from database import db
from models import Organization, User
from sqlalchemy.orm.attributes import flag_modified
class OrganizationService:
@staticmethod
def create_organization(name: str, username: str, full_name: str, email: str, public_key: str) -> Organization:
from services import UserService
user = UserService().get_user_by_username(username)
if not user:
user = UserService().create_user(username, full_name, email, public_key)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
repos = os.path.join(project_root, "repository")
if not os.path.exists(os.path.join(repos, name)):
os.mkdir(os.path.join(repos, name))
organization = Organization(
name=name,
manager=user,
users={user.id: {
"username": user.username,
"full_name": user.full_name,
"email": user.email,
"status": "active"
}}
)
db.add(organization)
db.commit()
db.refresh(organization)
UserService().add_org_to_user(user, organization)
UserService().add_public_key_to_user(user, organization, public_key)
return organization
@staticmethod
def list_organizations():
return db.query(Organization).all()
@staticmethod
def get_organization(org_id: int) -> Organization | None:
return db.query(Organization).filter(Organization.id == org_id).first()
@staticmethod
def get_organization_by_name(name: str) -> Organization | None:
return db.query(Organization).filter(Organization.name == name).first()
@staticmethod
def get_users_in_organization(org: Organization) -> list[User]:
return db.query(Organization).filter(Organization.id == org.id).first().users
@staticmethod
def get_user_status(org: Organization, user_id: int) -> str:
return db.query(Organization).filter(Organization.id == org.id).first().users[str(user_id)]["status"]
@staticmethod
def add_user_to_organization(org: Organization, user: User) -> Organization:
org.users[str(user.id)] = {
"username": user.username,
"full_name": user.full_name,
"email": user.email,
"status": "active"
}
flag_modified(org, "users")
db.commit()
db.refresh(org)
return org
@staticmethod
def suspend_user(org: Organization, user: User) -> tuple:
if OrganizationService.get_user_status(org, user.id) != "active":
return {"error": "User already suspended"}, 400
if org.manager.id == user.id:
return {"error": "Cannot suspend manager"}, 400
org.users[str(user.id)]["status"] = "suspended"
flag_modified(org, "users")
db.commit()
db.refresh(org)
return {"message": "User suspended"}, 200
@staticmethod
def activate_user(org: Organization, user: User) -> tuple:
if OrganizationService.get_user_status(org, user.id) != "suspended":
return {"error": "User already active"}, 400
org.users[str(user.id)]["status"] = "active"
flag_modified(org, "users")
db.commit()
db.refresh(org)
return {"message": "User activated"}, 200

View File

@ -0,0 +1,48 @@
import secrets
from database import db
from models import Session, User, Organization
from flask import jsonify
class SessionService:
@staticmethod
def create_session(user: User, org: Organization) -> Session:
session = Session(
user_id=user.id,
org_id=org.id,
token=secrets.token_hex(128)
)
db.add(session)
db.commit()
db.refresh(session)
return session
@staticmethod
def get_session(token: str) -> Session | None:
return db.query(Session).filter(Session.token == token).first()
@staticmethod
def delete_session(session: Session) -> None:
db.delete(session)
db.commit()
@staticmethod
def validate_session(token: str) -> tuple | Session:
from services import OrganizationService
if "Bearer" in token:
token = token.split(" ")[1]
session = SessionService.get_session(token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
org = OrganizationService.get_organization(session.org_id)
if not org:
return jsonify({"error": "Organization not found"}), 404
status = OrganizationService.get_user_status(org, session.user_id)
if status != "active":
return jsonify({"error": "User is not active"}), 403
return session

View File

@ -0,0 +1,53 @@
from database import db
from models import User, Organization
class UserService:
@staticmethod
def create_user(username: str, full_name: str, email: str, public_key: str, org: Organization = None) -> User:
from services import OrganizationService
user = User(
username=username,
full_name=full_name,
email=email,
public_keys={org.id: public_key} if org else {},
orgs={org.id: {
"name": org.name,
"status": "active"
}} if org else {}
)
db.add(user)
db.commit()
db.refresh(user)
if org:
OrganizationService.add_user_to_organization(org, user)
return user
@staticmethod
def get_user(user_id: int) -> User | None:
return db.query(User).filter(User.id == user_id).first()
@staticmethod
def get_user_by_username(username: str) -> User | None:
return db.query(User).filter(User.username == username).first()
@staticmethod
def add_org_to_user(user: User, org: Organization) -> User:
orgs = user.orgs.copy()
orgs[org.id] = {
"name": org.name,
"status": "active"
}
user.orgs = orgs
db.commit()
db.refresh(user)
return user
@staticmethod
def add_public_key_to_user(user: User, org: Organization, public_key: str) -> User:
public_keys = user.public_keys.copy()
public_keys[org.id] = public_key
user.public_keys = public_keys
db.commit()
db.refresh(user)
return user

View File

@ -0,0 +1,106 @@
### Reset database
POST http://localhost:5000/reset
Content-Type: application/json
{
"password": "123"
}
### Create a new organization
POST http://localhost:5000/org/create
Content-Type: application/json
{
"name": "org",
"username": "username",
"full_name": "Full Name",
"email": "user@mail.com",
"public_key": "null"
}
### Login
POST http://localhost:5000/user/login
Content-Type: application/json
{
"username": "username",
"org": "org"
}
> {% client.global.set("token", response.body["token"]) %}
### Upload dummy file metadata
POST http://localhost:5000/file/upload/metadata
Content-Type: application/json
Authorization: {{token}}
{
"document_name": "dummy_file.txt",
"key": "arfarf",
"alg": "ftgtrg"
}
> {% client.global.set("document_handle", response.body["document_handle"]) %}
#### Upload dummy file content, through send file
#POST http://localhost:5000/file/upload/content
#Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
#Authorization: {{token}}
#
#----WebKitFormBoundary7MA4YWxkTrZu0gW
#Content-Disposition: form-data; name="file"; filename="dummy_file.txt"
#Content-Type: text/plain
#
#This is a dummy file
#----WebKitFormBoundary7MA4YWxkTrZu0gW
### List files (with no data)
GET http://localhost:5000/file/list
Content-Type: application/json
Authorization: {{token}}
{}
### List files by user
GET http://localhost:5000/file/list
Content-Type: application/json
Authorization: {{token}}
{
"username": "username"
}
### List files by datetime
GET http://localhost:5000/file/list
Content-Type: application/json
Authorization: {{token}}
{
"datetime": {
"relation": "ot",
"value": "1731863876"
}
}
### Get file metadata
GET http://localhost:5000/file/get/{{document_handle}}/metadata
Content-Type: application/json
Authorization: {{token}}
> {% client.global.set("file_handle", response.body["file_handle"]) %}
### Get file content
GET http://localhost:5000/file/get/{{file_handle}}/content
Content-Type: application/json
Authorization: {{token}}
### Delete dummy file
POST http://localhost:5000/file/delete/{{document_handle}}
Content-Type: application/json
Authorization: {{token}}
### Logout
POST http://localhost:5000/user/logout
Content-Type: application/json
Authorization: {{token}}

View File

@ -0,0 +1,76 @@
### Reset database
POST http://localhost:5000/reset
Content-Type: application/json
{
"password": "123"
}
### Create a new organization
POST http://localhost:5000/org/create
Content-Type: application/json
{
"name": "org",
"username": "username",
"full_name": "Full Name",
"email": "user@mail.com",
"public_key": "null"
}
### Login
POST http://localhost:5000/user/login
Content-Type: application/json
{
"username": "username",
"org": "org",
"public_key": "null"
}
> {% client.global.set("token", response.body["token"]) %}
### List organizations
GET http://localhost:5000/org/list
### Create a new user
POST http://localhost:5000/user/create
Content-Type: application/json
Authorization: {{token}}
{
"username": "newuser",
"full_name": "Full Name",
"email": "newuser@mail.com",
"public_key": "null2"
}
### List users
GET http://localhost:5000/user/list
Content-Type: application/json
Authorization: {{token}}
{}
### Suspend user
POST http://localhost:5000/user/suspend
Content-Type: application/json
Authorization: {{token}}
{
"username": "newuser"
}
### Activate user
POST http://localhost:5000/user/activate
Content-Type: application/json
Authorization: {{token}}
{
"username": "newuser"
}
### Logout
POST http://localhost:5000/user/logout
Content-Type: application/json
Authorization: {{token}}

View File

@ -0,0 +1,2 @@
from .checks import check_valid_time
from .hashing import get_hash, get_hex_from_temp_file

View File

@ -0,0 +1,9 @@
def check_valid_time(time: int, relation_time: int, relation: str) -> bool:
if relation == 'ot':
return time < relation_time
elif relation == 'nt':
return time > relation_time
elif relation == 'eq':
return time == relation_time
else:
raise ValueError('Invalid relation: {}'.format(relation))

View File

@ -0,0 +1,16 @@
from tempfile import SpooledTemporaryFile
import cryptography.hazmat.primitives.hashes
def get_hash(data):
if isinstance(data, str):
data = data.encode('utf-8')
digest = cryptography.hazmat.primitives.hashes.Hash(cryptography.hazmat.primitives.hashes.SHA256())
digest.update(data)
return digest.finalize().hex()
def get_hex_from_temp_file(temp_file: SpooledTemporaryFile) -> bytes:
temp_file.seek(0)
file_data = temp_file.read()
return file_data