rewritten after codex recommendations

This commit is contained in:
2026-07-18 20:27:19 +02:00
parent f9dc885937
commit 399f539b4f
22 changed files with 1448 additions and 996 deletions
+303 -245
View File
@@ -1,269 +1,327 @@
from fernet import Fernet
from Log import Log
from Config import *
"""Database and encryption services for the legacy Flowers data format.
The public ``Flower`` API intentionally remains compatible with the original
application. Existing tables and Fernet ciphertext require no migration.
"""
from __future__ import annotations
import re
from contextlib import contextmanager
from typing import Callable, Iterator
import pymysql as mdb
from fernet import Fernet
#encryption stuff
SECRET = b'XBxB603cX_mULEXxfavOg3FDc0Ox3gChwYEY-Uxd3tE='
from Config import (
DB_ADMIN_PASSWORD,
DB_ADMIN_USER,
DB_HOST,
DB_NAME,
DB_PORT,
MINFIELDLEN,
)
from Log import Log
class Flower():
_USERNAME = re.compile(r"^[A-Za-z0-9_]{1,31}$")
def __init__(self, user, pwd):
self.db = 'Flowers'
self.db_username = user + '_'
self.db_password = (pwd*10)[:43]+"="
class InvalidCredentials(ValueError):
"""The supplied username/password cannot address a Flowers vault."""
class StorageError(RuntimeError):
"""A database operation failed."""
def legacy_key(password: str) -> str:
"""Return the exact DB password/Fernet key used by historical releases."""
if not isinstance(password, str) or not password:
raise InvalidCredentials("A password is required")
key = (password * 10)[:43] + "="
try:
Fernet(key.encode("ascii"))
except (UnicodeEncodeError, ValueError) as exc:
raise InvalidCredentials(
"Password must use Base64-compatible characters"
) from exc
return key
def _identifier(username: str) -> str:
if not isinstance(username, str) or not _USERNAME.fullmatch(username):
raise InvalidCredentials(
"Username must contain only letters, numbers, and underscores"
)
return f"{username}_"
class Flower:
"""Read and write one user's existing Flowers table."""
def __init__(
self,
user: str,
pwd: str,
connection_factory: Callable[..., object] | None = None,
) -> None:
self.db = DB_NAME
self.db_username = _identifier(user)
self.db_password = legacy_key(pwd)
self.customer_table = self.db_username
self.fernet = Fernet(bytes(self.db_password, 'ascii'))
self._table = f"`{self.customer_table}`"
self.fernet = Fernet(self.db_password.encode("ascii"))
self._connect = connection_factory or mdb.connect
def encode(self, s):
if len(s)==0:
return ''
return self.fernet.encrypt(s.encode()).decode()
def decode(self, s):
if len(s)==0:
return ''
return self.fernet.decrypt(s.encode()).decode()
def add(self, organization, myname, myid, secret, deleted=0, timestamp=None):
''' POST a new flower
'''
if len(organization)>MINFIELDLEN and len(myid)>MINFIELDLEN and len(secret)>MINFIELDLEN:
sql = 'INSERT INTO {} (organization ,myID ,myName ,mySecret, deleted) VALUES ("{}", "{}", "{}", "{}", {})'.format(self.customer_table, organization, self.encode(myid), self.encode(myname), self.encode(secret), deleted)
if timestamp is not None:
sql = 'INSERT INTO {} (organization ,myID ,myName ,mySecret, deleted, dateCreated) VALUES ("{}", "{}", "{}", "{}", {}, "{}")'.format(self.customer_table, organization, self.encode(myid), self.encode(myname), self.encode(secret), deleted, timestamp)
result, data = self.do_db_commit(sql)
Log.debug(result)
if result==1:
return {'organization': organization, 'dateCreated': 'STORED', 'myID': myid, 'myName': myname,'mySecret': secret}
return {'organization': organization, 'dateCreated': 'FAILED', 'myID': myid, 'myName': myname,'mySecret': secret}
def all(self, isdeleted=0):
''' GET flowers list
'''
#sql = "SELECT organization, myID, myName, dateCreated FROM %s " % (self.customer_table)
sql = """
select uniqueflowers.organization, uniqueflowers.myID, uniqueflowers.dateCreated from {0} as uniqueflowers
inner join (
select organization, max(dateCreated) as lastCreated from {0} group by organization order by dateCreated DESC) allflowers
on allflowers.organization=uniqueflowers.organization and lastCreated=uniqueflowers.dateCreated where uniqueflowers.deleted={1}
""".format(self.customer_table, isdeleted)
result, data = self.do_db_allrows(sql)
#Log.debug(sql_result)
all_flowers=[]
if result==1:
for row in data:
all_flowers.append({'organization': str(row[0]), 'myID': self.decode(row[1]), 'dateCreated': str(row[2])})
return all_flowers
def one(self, organization, datetimestamp ):
''' flower for user, org and datetime
'''
result = -1
secret = ':-('
sql = "SELECT mySecret, myID, myName FROM %s where organization='%s' and dateCreated='%s' order by dateCreated DESC limit 1" % (self.customer_table, organization, datetimestamp)
result, data = self.do_db_1row(sql)
if result == 1:
return {'organization': organization, 'dateCreated': datetimestamp, 'myID': self.decode(data[1]), 'myName': self.decode(data[2]),'mySecret': self.decode(data[0])}
return {'organization': organization, 'dateCreated': datetimestamp, 'myID': '', 'myName': '','mySecret': '-- Not found --'}
def deactivate(self, organization, datetimestamp ):
''' flower for user, org and datetime
'''
result = -1
secret = ':-('
sql = "update {} set deleted=1 where organization='{}' and dateCreated='{}' ".format(self.customer_table, organization, datetimestamp)
result, data = self.do_db_commit(sql)
if result == 1:
secret = '- DELETED -'
return {'organization': organization, 'dateCreated': datetimestamp, 'myID': '', 'myName': '','mySecret': secret}
def empty(self):
''' empty flower
'''
return {'organization': '', 'dateCreated': '', 'myID': '', 'myName': '','mySecret': ''}
def numberOfEntries(self):
''' GET login info, check if its valid
'''
sql = "SELECT count(*) FROM {};".format(self.customer_table)
result, data = self.do_db_1row(sql)
if result == 1:
return(data[0])
return -1
def update_pwd(self, new_pwd):
long_newpwd = (new_pwd*10)[:43]+"="
self.new_fernet = Fernet(bytes(long_newpwd, 'ascii'))
def new_encode(s):
if len(s)==0:
return ''
return self.new_fernet.encrypt(s.encode()).decode()
end_result = "ERROR updating hushes\n"
sql = """
select myID, myName, mySecret, organization, dateCreated from {0}""".format(self.customer_table)
result, data = self.do_db_allrows(sql)
# Log.debug(sql_result)
if result == 1:
# convert all hushes
sql = ""
for row in data:
sql += "update {} set myID='{}', myName='{}', mySecret='{}' where organization='{}' and dateCreated='{}';\n".\
format(self.customer_table,
new_encode(self.decode(row[0])),
new_encode(self.decode(row[1])),
new_encode(self.decode(row[2])),row[3], row[4])
result, data = self.do_db_commit(sql)
if result == 1:
end_result = "ERROR updating user-password\n"
# update the password of the user
sql = "SET PASSWORD FOR '{}'@'localhost' = PASSWORD('{}'); ".format(self.db_username, long_newpwd)
result, data = self.do_db_commit(sql)
if result == 1:
end_result = "SUCCESSFULLY updated, now login again\n"
return end_result
def do_db_commit(self, sql):
Log.debug('INSc: '+sql)
result = 0
data = ''
con = None
@contextmanager
def _connection(self) -> Iterator[object]:
connection = None
try:
con = mdb.connect(host='localhost', passwd=self.db_password, user=self.db_username, db=self.db);
cur = con.cursor()
if ";\n" in sql:
for s in sql.split(";\n"):
if len(s)>1:
cur.execute(s)
else:
cur.execute(sql)
cur.close()
con.commit()
result = 1
except mdb.Error as e:
m = "Error %d: %s" % (e.args[0],e.args[1])
data = {'message': m}
Log.debug(m)
result = -1
connection = self._connect(
host=DB_HOST,
port=DB_PORT,
user=self.db_username,
passwd=self.db_password,
db=self.db,
charset="latin1",
)
yield connection
except mdb.Error as exc:
Log.debug(f"Database error: {exc}")
raise StorageError("The vault database is unavailable") from exc
finally:
if con:
con.close()
return result, data
if connection is not None:
connection.close()
def encode(self, value: str) -> str:
if not value:
return ""
return self.fernet.encrypt(value.encode("utf-8")).decode("ascii")
def do_db_1row(self, sql):
Log.debug('1row sql: '+sql)
result = 0
con = None
def decode(self, value: str) -> str:
if not value:
return ""
return self.fernet.decrypt(value.encode("ascii")).decode("utf-8")
def authenticate(self) -> bool:
try:
con = mdb.connect(host='localhost', passwd=self.db_password, user=self.db_username, db=self.db);
cur = con.cursor()
cur.execute(sql)
data = cur.fetchone()
if data:
result = 1
except mdb.Error as e:
m = "Error %d: %s" % (e.args[0],e.args[1])
data = {'message': m}
Log.debug(m)
result = -1
finally:
if con:
con.close()
return result, data
with self._connection() as connection:
with connection.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM {self._table}")
cursor.fetchone()
return True
except (StorageError, ValueError):
return False
def do_db_allrows(self, sql):
Log.debug('all rows sql: '+sql)
result = 0
con = None
def numberOfEntries(self) -> int:
"""Compatibility method used by older clients and scripts."""
try:
con = mdb.connect(host='localhost', passwd=self.db_password, user=self.db_username, db=self.db);
cur = con.cursor()
cur.execute(sql)
data = cur.fetchall()
if data:
result = 1
except mdb.Error as e:
m = "Error %d: %s" % (e.args[0],e.args[1])
data = {'message': m}
Log.debug(m)
result = -1
finally:
if con:
con.close()
return result, data
with self._connection() as connection:
with connection.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM {self._table}")
row = cursor.fetchone()
return int(row[0]) if row else 0
except StorageError:
return -1
def add(
self,
organization: str,
myname: str,
myid: str,
secret: str,
deleted: int = 0,
timestamp=None,
) -> dict:
item = self._item(organization, "FAILED", myid, myname, secret)
if not all(isinstance(v, str) for v in (organization, myname, myid, secret)):
return item
if not (
len(organization) > MINFIELDLEN
and len(myid) > MINFIELDLEN
and len(secret) > MINFIELDLEN
):
return item
columns = "organization, myID, myName, mySecret, deleted"
values = [
organization,
self.encode(myid),
self.encode(myname),
self.encode(secret),
int(bool(deleted)),
]
placeholders = "%s, %s, %s, %s, %s"
if timestamp is not None:
columns += ", dateCreated"
placeholders += ", %s"
values.append(timestamp)
sql = f"INSERT INTO {self._table} ({columns}) VALUES ({placeholders})"
self._execute_write(sql, values)
return self._item(organization, "STORED", myid, myname, secret)
def all(self, isdeleted: int = 0) -> list[dict]:
sql = f"""
SELECT latest.organization, latest.myID, latest.dateCreated
FROM {self._table} AS latest
INNER JOIN (
SELECT organization, MAX(dateCreated) AS lastCreated
FROM {self._table}
GROUP BY organization
) AS versions
ON versions.organization = latest.organization
AND versions.lastCreated = latest.dateCreated
WHERE latest.deleted = %s
ORDER BY latest.dateCreated DESC, latest.organization ASC
"""
with self._connection() as connection:
with connection.cursor() as cursor:
cursor.execute(sql, (int(bool(isdeleted)),))
rows = cursor.fetchall()
return [
{
"organization": str(row[0]),
"myID": self.decode(row[1]),
"dateCreated": str(row[2]),
}
for row in rows
]
def one(self, organization: str, datetimestamp) -> dict:
sql = f"""
SELECT mySecret, myID, myName
FROM {self._table}
WHERE organization = %s AND dateCreated = %s
ORDER BY dateCreated DESC
LIMIT 1
"""
with self._connection() as connection:
with connection.cursor() as cursor:
cursor.execute(sql, (organization, datetimestamp))
row = cursor.fetchone()
if row:
return self._item(
organization,
str(datetimestamp),
self.decode(row[1]),
self.decode(row[2]),
self.decode(row[0]),
)
return self._item(organization, str(datetimestamp), "", "", "-- Not found --")
def deactivate(self, organization: str, datetimestamp) -> dict:
sql = f"UPDATE {self._table} SET deleted = 1 WHERE organization = %s AND dateCreated = %s"
changed = self._execute_write(sql, (organization, datetimestamp))
message = "- DELETED -" if changed else "-- Not found --"
return self._item(organization, str(datetimestamp), "", "", message)
def empty(self) -> dict:
return self._item("", "", "", "", "")
def update_pwd(self, new_pwd: str) -> str:
"""Re-encrypt every value and update the legacy per-user DB password."""
new_db_password = legacy_key(new_pwd)
new_fernet = Fernet(new_db_password.encode("ascii"))
try:
with self._connection() as connection:
try:
with connection.cursor() as cursor:
cursor.execute(
f"SELECT myID, myName, mySecret, organization, dateCreated FROM {self._table}"
)
rows = cursor.fetchall()
update = f"""
UPDATE {self._table}
SET myID = %s, myName = %s, mySecret = %s
WHERE organization = %s AND dateCreated = %s
"""
for row in rows:
encrypted = [
new_fernet.encrypt(self.decode(value).encode("utf-8")).decode("ascii")
if value else ""
for value in row[:3]
]
cursor.execute(update, (*encrypted, row[3], row[4]))
# This form is supported by the MariaDB versions used by
# historical Flowers installations.
cursor.execute("SET PASSWORD = PASSWORD(%s)", (new_db_password,))
connection.commit()
except Exception:
connection.rollback()
raise
except Exception as exc:
Log.debug(f"Password update failed: {exc}")
return "ERROR updating password"
return "SUCCESSFULLY updated, now login again"
def _execute_write(self, sql: str, parameters) -> int:
with self._connection() as connection:
try:
with connection.cursor() as cursor:
changed = cursor.execute(sql, parameters)
connection.commit()
return int(changed)
except Exception:
connection.rollback()
raise
@staticmethod
def _item(organization, timestamp, myid, myname, secret) -> dict:
return {
"organization": organization,
"dateCreated": timestamp,
"myID": myid,
"myName": myname,
"mySecret": secret,
}
class SuperFlower(Flower):
"""Provision a legacy per-user table and database account."""
def __init__(self, customer, pwd):
super().__init__(customer, pwd)
self.customer_name = customer + '_'
self.customer_pwd = (pwd*10)[:43]+"="
def __init__(self, customer: str, pwd: str, connection_factory=None) -> None:
super().__init__(customer, pwd, connection_factory)
self.customer_name = self.customer_table
self.customer_pwd = self.db_password
self.db_username = DB_ADMIN_USER
self.db_password = DB_ADMIN_PASSWORD
self.db_username = 'flower'
self.db_password = '608f0b988db4a96066af7dd8870de96c'
def createNewTable(self):
# create new table and user
sql = """
CREATE TABLE `{}` (
`organization` varchar(80) NOT NULL,
`myID` varchar(255) NOT NULL,
`myName` varchar(255) NOT NULL,
`mySecret` varchar(255) NOT NULL,
`dateCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`deleted` int(11) DEFAULT '0',
PRIMARY KEY (`organization`,`dateCreated`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;""".format(self.customer_table)
result, data = self.do_db_1row(sql)
if result < 0:
Log.info("SQL ERROR:")
Log.info(data)
def createNewTable(self) -> int:
statements = [
f"""CREATE TABLE {self._table} (
organization varchar(80) NOT NULL,
myID varchar(255) NOT NULL,
myName varchar(255) NOT NULL,
mySecret varchar(255) NOT NULL,
dateCreated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted int(11) DEFAULT 0,
PRIMARY KEY (organization, dateCreated)
) ENGINE=InnoDB DEFAULT CHARSET=latin1""",
f"CREATE USER '{self.customer_name}'@'localhost' IDENTIFIED BY %s",
f"GRANT SELECT, UPDATE, INSERT ON `{self.db}`.{self._table} TO '{self.customer_name}'@'localhost'",
]
try:
with self._connection() as connection:
with connection.cursor() as cursor:
cursor.execute(statements[0])
cursor.execute(statements[1], (self.customer_pwd,))
cursor.execute(statements[2])
connection.commit()
return 1
except StorageError as exc:
Log.info(f"Could not create vault: {exc}")
return 0
sql = """create user {}@localhost identified by '{}';""".format(self.customer_name, self.customer_pwd)
result, data = self.do_db_1row(sql)
if result < 0:
Log.info("SQL ERROR:")
Log.info(data)
def flush_privs(self) -> int:
try:
self._execute_write("FLUSH PRIVILEGES", ())
return 1
except StorageError:
return 0
sql = """grant select,update,insert on Flowers.{} to {}@localhost;""".format(self.customer_table, self.customer_name)
result, data = self.do_db_1row(sql)
if result < 0:
Log.info("SQL ERROR:")
Log.info(data)
return 0
self.flush_privs()
return 1
def flush_privs(self):
sql = """flush privileges;"""
result, data = self.do_db_1row(sql)
if result < 0:
Log.info("SQL ERROR:")
Log.info(data)
return 0
return 1
if __name__ == '__main__':
print("this is a library, sorry dude")
if __name__ == "__main__":
print("This module is a library.")