Security
mydborm provides two security-focused field types:
- PasswordField — one-way bcrypt hashing for user passwords
- EncryptedField — two-way AES encryption for sensitive data
Installation
This installs bcrypt and cryptography as extra dependencies.
PasswordField — bcrypt hashing
Use PasswordField for user passwords. Passwords are hashed using bcrypt
and cannot be decrypted — you can only verify them.
Define the field
from mydborm import db, BaseModel, IntField, StrField, BoolField
from mydborm import PasswordField
class User(BaseModel):
__tablename__ = "users"
id = IntField(primary_key=True)
username = StrField(max_length=50, nullable=False, unique=True)
email = StrField(max_length=255, nullable=False, unique=True)
password = PasswordField(nullable=False)
active = BoolField(default=True)
User.create_table()
SQL generated:
Create a user
The password is automatically hashed on create() — you never store plain text:
uid = User.create(
username = "alice",
email = "alice@example.com",
password = "mysecretpassword", # plain text in
)
user = User.get(id=uid)
print(user["password"])
# $2b$12$K9L7bPzQx5uF3Yw8RtJm8... ← bcrypt hash stored
print(user["password"] == "mysecretpassword") # False — never plain text
Verify a password (login)
def login(username, plain_password):
user = User.query().where("username", username).first()
if not user:
return False, "User not found"
if PasswordField.verify(plain_password, user["password"]):
return True, "Login successful"
else:
return False, "Wrong password"
ok, msg = login("alice", "mysecretpassword")
print(ok, msg) # True Login successful
ok, msg = login("alice", "wrongpassword")
print(ok, msg) # False Wrong password
Change password
def change_password(user_id, old_password, new_password):
user = User.get(id=user_id)
if not user:
raise ValueError("User not found")
if not PasswordField.verify(old_password, user["password"]):
raise ValueError("Current password is incorrect")
# Update — new password is auto-hashed
User.update({"password": new_password}, id=user_id)
return True
change_password(uid, "mysecretpassword", "newstrongerpassword!")
Hash password manually
# Hash without storing
hashed = PasswordField.hash("mysecret", rounds=12)
print(hashed) # $2b$12$...
# Verify later
ok = PasswordField.verify("mysecret", hashed)
print(ok) # True
Configure work factor (rounds)
Higher rounds = slower hashing = more secure. Default is 12.
class User(BaseModel):
__tablename__ = "users"
id = IntField(primary_key=True)
username = StrField(max_length=50, nullable=False)
# rounds=12 is production default
# rounds=4 for tests (much faster)
password = PasswordField(rounds=12, nullable=False)
| Rounds | Hash time | Use case |
|---|---|---|
| 4 | ~1ms | Tests only |
| 10 | ~100ms | Low-security apps |
| 12 | ~400ms | Recommended |
| 14 | ~1.5s | High-security |
How bcrypt works
- Each hash includes a random salt — same password hashes differently each time
- The rounds parameter controls how many iterations are run
- Stored hash includes the rounds and salt — no extra columns needed
- Cannot be reversed — only verification is possible
EncryptedField — AES encryption
Use EncryptedField for data you need to retrieve — API keys, tokens,
SSNs, credit card numbers, personal data.
Keep your key safe
If you lose your encryption key, all encrypted data is unrecoverable. Store keys in environment variables or a secrets manager — never in code.
Generate a key
from mydborm import EncryptedField
# Generate once — store securely
key = EncryptedField.generate_key()
print(key)
# dBF_6PJ5hRkzGjQ8N9TmY2w4sIoXcVeA3nKuLbEZWp0=
# Store in .env file:
# ENCRYPTION_KEY=dBF_6PJ5hRkzGjQ8N9TmY2w4sIoXcVeA3nKuLbEZWp0=
Define the model
import os
from mydborm import db, BaseModel, IntField, StrField
from mydborm import EncryptedField
KEY = os.environ["ENCRYPTION_KEY"] # never hardcode!
class APICredential(BaseModel):
__tablename__ = "api_credentials"
id = IntField(primary_key=True)
service = StrField(max_length=50, nullable=False)
api_key = EncryptedField(secret_key=KEY, nullable=False)
api_secret = EncryptedField(secret_key=KEY, nullable=True)
webhook = EncryptedField(secret_key=KEY, nullable=True)
APICredential.create_table()
SQL generated:
-- MySQL
api_key TEXT NOT NULL
api_secret TEXT
webhook TEXT
-- YugabyteDB
api_key TEXT NOT NULL
api_secret TEXT
webhook TEXT
Store credentials
Values are automatically encrypted on create():
cid = APICredential.create(
service = "stripe",
api_key = "sk_live_51abc123xyz", # plain text in
api_secret = "whsec_webhook_secret_456",
webhook = "https://api.myapp.com/webhook",
)
cred = APICredential.get(id=cid)
print(cred["api_key"])
# gAAAAABqNiQh8G-CriEJg6VvaewyJH... ← ciphertext stored
print(cred["api_key"] == "sk_live_51abc123xyz") # False — encrypted in DB
Decrypt values
# Method 1 — static method
plain = EncryptedField.decrypt(cred["api_key"], secret_key=KEY)
print(plain) # sk_live_51abc123xyz
# Method 2 — field instance
field = APICredential._fields["api_key"]
plain = field.decrypt_value(cred["api_key"])
print(plain) # sk_live_51abc123xyz
# Method 3 — encrypt/decrypt directly
cipher = EncryptedField.encrypt("my_secret_value", secret_key=KEY)
plain = EncryptedField.decrypt(cipher, secret_key=KEY)
Full workflow example
import os
from mydborm import db, BaseModel, IntField, StrField, EncryptedField
KEY = os.environ.get("ENCRYPTION_KEY") or EncryptedField.generate_key()
class OAuthToken(BaseModel):
__tablename__ = "oauth_tokens"
id = IntField(primary_key=True)
user_id = IntField(nullable=False)
provider = StrField(max_length=20, nullable=False)
access_token = EncryptedField(secret_key=KEY, nullable=False)
refresh_token = EncryptedField(secret_key=KEY, nullable=True)
expires_in = IntField(nullable=True)
OAuthToken.create_table()
# Store after OAuth flow
def save_oauth_token(user_id, provider, access, refresh, expires):
# Delete existing token for this user+provider
existing = OAuthToken.filter(user_id=user_id, provider=provider)
for t in existing:
OAuthToken.delete(id=t["id"])
# Store new token (auto-encrypted)
return OAuthToken.create(
user_id = user_id,
provider = provider,
access_token = access,
refresh_token = refresh,
expires_in = expires,
)
# Retrieve and decrypt for API calls
def get_access_token(user_id, provider):
token = OAuthToken.query().where("user_id", user_id).where("provider", provider).first()
if not token:
return None
return EncryptedField.decrypt(token["access_token"], secret_key=KEY)
# Usage
save_oauth_token(1, "google", "ya29.abc...", "1//def...", 3600)
access = get_access_token(1, "google")
print(access) # ya29.abc...
How Fernet encryption works
- Uses Fernet — a safe, authenticated encryption scheme
- Each encryption uses a random IV — same value encrypts differently each time
- Includes HMAC authentication — tampered ciphertext raises an error
- Ciphertext is base64 — safe to store in TEXT columns
Security best practices
Use environment variables for keys
# .env file (never commit this!)
# ENCRYPTION_KEY=your-key-here
# DB_PASSWORD=your-db-password
import os
from dotenv import load_dotenv
load_dotenv()
KEY = os.environ["ENCRYPTION_KEY"]
Never log plain passwords or decrypted values
# BAD
print(f"User logged in with password: {plain_password}")
logger.info(f"API key: {decrypted_key}")
# GOOD
print(f"User {user_id} logged in successfully")
logger.info(f"API key for service {service} retrieved (length={len(decrypted_key)})")
Rotate encryption keys periodically
def rotate_encryption_key(old_key, new_key):
"""Re-encrypt all credentials with a new key."""
creds = APICredential.all()
for cred in creds:
# Decrypt with old key
plain_key = EncryptedField.decrypt(cred["api_key"], secret_key=old_key)
plain_secret = EncryptedField.decrypt(cred["api_secret"], secret_key=old_key) if cred["api_secret"] else None
# Re-encrypt with new key
new_key_enc = EncryptedField.encrypt(plain_key, secret_key=new_key)
new_secret_enc = EncryptedField.encrypt(plain_secret, secret_key=new_key) if plain_secret else None
APICredential.update({
"api_key": new_key_enc,
"api_secret": new_secret_enc,
}, id=cred["id"])
print(f"Re-encrypted {len(creds)} credentials")
PasswordField vs EncryptedField — choosing the right one
| Use case | Field | Why |
|---|---|---|
| User login passwords | PasswordField |
One-way — even you can't read it |
| API keys / tokens | EncryptedField |
Need to retrieve and use them |
| OAuth access tokens | EncryptedField |
Need to send to external APIs |
| Credit card numbers | EncryptedField |
Need to display last 4 digits |
| SSN / passport | EncryptedField |
Need to verify identity |
| Admin PINs | PasswordField |
Only need to verify |
| Webhook secrets | EncryptedField |
Need to sign payloads |
| Recovery codes | PasswordField |
One-time verify, never show again |
Field reference
PasswordField
| Property | Value |
|---|---|
| MySQL type | VARCHAR(255) |
| YugabyteDB type | VARCHAR(255) |
| Algorithm | bcrypt |
| Default rounds | 12 |
| Reversible | No — verify only |
Methods:
PasswordField.verify(plain, hashed) # → bool
PasswordField.hash(plain, rounds=12) # → hash string
field.needs_rehash(hashed) # → bool
EncryptedField
| Property | Value |
|---|---|
| MySQL type | TEXT |
| YugabyteDB type | TEXT |
| Algorithm | Fernet (AES-128-CBC + HMAC-SHA256) |
| Key size | 32 bytes (base64-encoded) |
| Reversible | Yes — decrypt with same key |
Methods: