04/16 Upload codebase

This commit is contained in:
2026-04-16 08:27:07 -04:00
parent 91be5c1f5c
commit 457016b8d4
43 changed files with 6241 additions and 0 deletions
View File
+65
View File
@@ -0,0 +1,65 @@
from datetime import datetime, timedelta
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class EmergencyAccess(db.Model):
"""
Emergency access grant from a vault owner (grantor) to a trusted contact (grantee).
Status flow:
invited → grantee calls /accept → accepted
accepted → grantor calls /provide → ready (enc_vault stored)
ready → grantee calls /request → pending (wait timer starts)
pending → grantor calls /deny → ready (reset, grantee can request again)
pending (wait_days elapsed) → grantable (grantee fetches vault)
Zero-knowledge: enc_vault is a JSON array of vault items re-encrypted by the grantor
using the ECDH shared secret (grantor private key + grantee public key).
"""
__tablename__ = 'emergency_access'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
grantor_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
)
grantee_email = db.Column(db.String(255), nullable=False)
grantee_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('users.id', ondelete='SET NULL'),
nullable=True,
)
wait_days = db.Column(db.Integer, default=7, nullable=False)
# invited | accepted | ready | pending | denied
status = db.Column(db.String(20), default='invited', nullable=False)
request_initiated_at = db.Column(db.DateTime, nullable=True)
# JSON string: [{ id, name, item_type, enc_data, iv }, ...]
enc_vault = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
@property
def wait_elapsed(self):
"""True if the wait period has passed since the access request."""
if self.status != 'pending' or not self.request_initiated_at:
return False
return datetime.utcnow() >= self.request_initiated_at + timedelta(days=self.wait_days)
def to_dict(self, grantor_email=None):
return {
'id': self.id,
'grantor_id': self.grantor_id,
'grantor_email': grantor_email,
'grantee_email': self.grantee_email,
'grantee_id': self.grantee_id,
'wait_days': self.wait_days,
'status': self.status,
'wait_elapsed': self.wait_elapsed,
'request_initiated_at': (
self.request_initiated_at.isoformat() if self.request_initiated_at else None
),
'created_at': self.created_at.isoformat() if self.created_at else None,
}
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class Folder(db.Model):
__tablename__ = 'folders'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
user_id = db.Column(INTEGER(unsigned=True), db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
name = db.Column(db.String(128), nullable=False)
vault_items = db.relationship('VaultItem', backref='folder', lazy='dynamic')
def to_dict(self):
return {'id': self.id, 'name': self.name}
def __repr__(self):
return f'<Folder {self.name}>'
+58
View File
@@ -0,0 +1,58 @@
from datetime import datetime
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class SharedItem(db.Model):
"""
A vault item shared by one user with another.
Zero-knowledge: enc_data is re-encrypted by the SENDER using the ECDH shared
secret derived from sender's private key + recipient's P-256 public key.
The server stores only the ciphertext — it cannot decrypt it.
"""
__tablename__ = 'shared_items'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
item_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('vault_items.id', ondelete='CASCADE'),
nullable=False,
)
owner_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
)
recipient_email = db.Column(db.String(255), nullable=False)
recipient_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('users.id', ondelete='SET NULL'),
nullable=True,
)
# Plaintext metadata — not sensitive, used for display before decryption
item_name = db.Column(db.String(255), nullable=False)
item_type = db.Column(db.String(20), nullable=False, default='password')
# ECDH-encrypted payload
enc_data = db.Column(db.Text, nullable=False)
iv = db.Column(db.String(64), nullable=False)
accepted = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
def to_dict(self):
return {
'id': self.id,
'item_id': self.item_id,
'owner_id': self.owner_id,
'recipient_email': self.recipient_email,
'recipient_id': self.recipient_id,
'item_name': self.item_name,
'item_type': self.item_type,
'enc_data': self.enc_data,
'iv': self.iv,
'accepted': self.accepted,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
+29
View File
@@ -0,0 +1,29 @@
from datetime import datetime
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class TokenBlacklist(db.Model):
"""Revoked JWT identifiers. Access and refresh tokens are added on logout."""
__tablename__ = 'token_blacklist'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
jti = db.Column(db.String(36), unique=True, nullable=False, index=True)
user_id = db.Column(INTEGER(unsigned=True), nullable=False)
expires_at = db.Column(db.DateTime, nullable=False)
@classmethod
def is_blacklisted(cls, jti: str) -> bool:
entry = cls.query.filter_by(jti=jti).first()
if not entry:
return False
# Automatically ignore expired entries (they can be cleaned up later)
return entry.expires_at > datetime.utcnow()
@classmethod
def cleanup_expired(cls):
"""Delete entries that have already expired — call occasionally to keep table small."""
cls.query.filter(cls.expires_at <= datetime.utcnow()).delete()
db.session.commit()
+45
View File
@@ -0,0 +1,45 @@
from datetime import datetime
from flask_login import UserMixin
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
# master_hash: Argon2id hash of the client-derived PBKDF2 auth_hash
# The raw master password is NEVER sent to or stored on the server.
master_hash = db.Column(db.String(255), nullable=False)
# enc_key_salt: random 16-byte salt (base64) generated at registration.
# Returned to the client on login so it can re-derive the AES-256-GCM vault key.
# The server never uses this for decryption — it is opaque to us.
enc_key_salt = db.Column(db.String(64), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
last_login = db.Column(db.DateTime, nullable=True)
# TOTP / MFA
totp_secret = db.Column(db.String(64), nullable=True)
totp_enabled = db.Column(db.Boolean, default=False, nullable=False)
# ECDH P-256 sharing keypair
# Public key: raw uncompressed point (65 bytes), base64-encoded, stored plaintext
sharing_public_key = db.Column(db.Text, nullable=True)
# Private key: JWK, AES-256-GCM encrypted with the user's vault key
sharing_private_key_enc = db.Column(db.Text, nullable=True)
sharing_private_key_iv = db.Column(db.String(64), nullable=True)
folders = db.relationship('Folder', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
vault_items = db.relationship('VaultItem', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
def check_password(self, auth_hash: str) -> bool:
ph = PasswordHasher()
try:
return ph.verify(self.master_hash, auth_hash)
except (VerifyMismatchError, VerificationError, InvalidHashError):
return False
def __repr__(self):
return f'<User {self.email}>'
+52
View File
@@ -0,0 +1,52 @@
from datetime import datetime
import enum
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class ItemType(str, enum.Enum):
PASSWORD = 'password'
NOTE = 'note'
ADDRESS = 'address'
CARD = 'card'
BANK = 'bank'
SSN = 'ssn'
PASSKEY = 'passkey'
class VaultItem(db.Model):
__tablename__ = 'vault_items'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
user_id = db.Column(INTEGER(unsigned=True), db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
folder_id = db.Column(INTEGER(unsigned=True), db.ForeignKey('folders.id', ondelete='SET NULL'), nullable=True)
item_type = db.Column(db.String(20), nullable=False, default=ItemType.PASSWORD.value)
# name is stored in plaintext for display in the vault list.
# All other sensitive fields (username, password, URL, notes, etc.)
# are inside enc_data and are encrypted client-side with AES-256-GCM.
name = db.Column(db.String(255), nullable=False)
enc_data = db.Column(db.Text, nullable=False) # base64-encoded AES-256-GCM ciphertext
iv = db.Column(db.String(64), nullable=False) # base64-encoded 12-byte GCM nonce
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def to_dict(self):
# item_type is stored as a plain string; handle both str and enum safely
item_type = self.item_type
if isinstance(item_type, ItemType):
item_type = item_type.value
return {
'id': self.id,
'item_type': item_type,
'name': self.name,
'folder_id': self.folder_id,
'enc_data': self.enc_data,
'iv': self.iv,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
def __repr__(self):
return f'<VaultItem {self.name}>'