Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,69 @@
// monorepo/cloud/maplefile-backend/internal/domain/crypto/domain/keys/kdf.go
package crypto
import (
"fmt"
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/crypto"
)
// KDFParams stores the key derivation function parameters
type KDFParams struct {
Algorithm string `json:"algorithm" bson:"algorithm"` // "argon2id", "pbkdf2", "scrypt"
Version string `json:"version" bson:"version"` // "1.0", "1.1", etc.
Iterations uint32 `json:"iterations" bson:"iterations"` // For PBKDF2 or Argon2 time cost
Memory uint32 `json:"memory" bson:"memory"` // For Argon2 memory in KB
Parallelism uint8 `json:"parallelism" bson:"parallelism"` // For Argon2 threads
SaltLength uint32 `json:"salt_length" bson:"salt_length"` // Salt size in bytes
KeyLength uint32 `json:"key_length" bson:"key_length"` // Output key size in bytes
}
// DefaultKDFParams returns the current recommended KDF parameters
func DefaultKDFParams() KDFParams {
return KDFParams{
Algorithm: crypto.Argon2IDAlgorithm,
Version: "1.0", // Always starts at 1.0
Iterations: crypto.Argon2OpsLimit, // Time cost
Memory: crypto.Argon2MemLimit,
Parallelism: crypto.Argon2Parallelism,
SaltLength: crypto.Argon2SaltSize,
KeyLength: crypto.Argon2KeySize,
}
}
// Validate checks if KDF parameters are valid
func (k KDFParams) Validate() error {
switch k.Algorithm {
case crypto.Argon2IDAlgorithm:
if k.Iterations < 1 {
return fmt.Errorf("argon2id time cost must be >= 1")
}
if k.Memory < 1024 {
return fmt.Errorf("argon2id memory must be >= 1024 KB")
}
if k.Parallelism < 1 {
return fmt.Errorf("argon2id parallelism must be >= 1")
}
default:
return fmt.Errorf("unsupported KDF algorithm: %s", k.Algorithm)
}
if k.SaltLength < 8 {
return fmt.Errorf("salt length must be >= 8 bytes")
}
if k.KeyLength < 16 {
return fmt.Errorf("key length must be >= 16 bytes")
}
return nil
}
// KDFUpgradePolicy defines when to upgrade KDF parameters
type KDFUpgradePolicy struct {
MinimumParams KDFParams `json:"minimum_params" bson:"minimum_params"`
RecommendedParams KDFParams `json:"recommended_params" bson:"recommended_params"`
MaxPasswordAge time.Duration `json:"max_password_age" bson:"max_password_age"`
UpgradeOnNextLogin bool `json:"upgrade_on_next_login" bson:"upgrade_on_next_login"`
LastUpgradeCheck time.Time `json:"last_upgrade_check" bson:"last_upgrade_check"`
}

View file

@ -0,0 +1,355 @@
package crypto
import (
"encoding/base64"
"encoding/json"
"fmt"
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/crypto"
)
// tryDecodeBase64 attempts to decode a base64 string using multiple encodings.
// It tries URL-safe without padding first (libsodium's URLSAFE_NO_PADDING),
// then standard base64 with padding, then standard without padding.
func tryDecodeBase64(s string) ([]byte, error) {
var lastErr error
// Try URL-safe base64 without padding (libsodium's URLSAFE_NO_PADDING)
if data, err := base64.RawURLEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try standard base64 with padding (Go's default for []byte)
if data, err := base64.StdEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try standard base64 without padding
if data, err := base64.RawStdEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try URL-safe base64 with padding
if data, err := base64.URLEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
return nil, fmt.Errorf("failed to decode base64 with any encoding: %w", lastErr)
}
// MasterKey represents the root encryption key for a user
type MasterKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedMasterKey is the master key encrypted with the key encryption key
type EncryptedMasterKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (emk *EncryptedMasterKey) GetCurrentVersion() int {
return emk.KeyVersion
}
func (emk *EncryptedMasterKey) GetKeyByVersion(version int) *EncryptedHistoricalKey {
if version == emk.KeyVersion {
// Return current key as historical format
return &EncryptedHistoricalKey{
KeyVersion: emk.KeyVersion,
Ciphertext: emk.Ciphertext,
Nonce: emk.Nonce,
Algorithm: crypto.ChaCha20Poly1305Algorithm, // ✅ Updated to ChaCha20-Poly1305
}
}
for _, key := range emk.PreviousKeys {
if key.KeyVersion == version {
return &key
}
}
return nil
}
// KeyEncryptionKey derived from user password
type KeyEncryptionKey struct {
Key []byte `json:"key" bson:"key"`
Salt []byte `json:"salt" bson:"salt"`
}
// PublicKey for asymmetric encryption
type PublicKey struct {
Key []byte `json:"key" bson:"key"`
VerificationID string `json:"verification_id" bson:"verification_id"`
}
// PrivateKey for asymmetric decryption
type PrivateKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedPrivateKey is the private key encrypted with the master key
type EncryptedPrivateKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}
// RecoveryKey for account recovery
type RecoveryKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedRecoveryKey is the recovery key encrypted with the master key
type EncryptedRecoveryKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}
// CollectionKey encrypts files in a collection
type CollectionKey struct {
Key []byte `json:"key" bson:"key"`
CollectionID string `json:"collection_id" bson:"collection_id"`
}
// EncryptedCollectionKey is the collection key encrypted with master key
type EncryptedCollectionKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (eck *EncryptedCollectionKey) NeedsRotation(policy KeyRotationPolicy) bool {
if eck.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*eck.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedCollectionKey to serialize bytes as base64 strings.
func (eck *EncryptedCollectionKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(eck.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(eck.Nonce),
KeyVersion: eck.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedCollectionKey to handle URL-safe base64 strings.
func (eck *EncryptedCollectionKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedCollectionKey into alias: %w", err)
}
// Set KeyVersion
eck.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedCollectionKey.Ciphertext: %w", err)
}
eck.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedCollectionKey.Nonce: %w", err)
}
eck.Nonce = nonceBytes
}
return nil
}
// FileKey encrypts a specific file
type FileKey struct {
Key []byte `json:"key" bson:"key"`
FileID string `json:"file_id" bson:"file_id"`
}
// EncryptedFileKey is the file key encrypted with collection key
type EncryptedFileKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (eck *EncryptedFileKey) NeedsRotation(policy KeyRotationPolicy) bool {
if eck.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*eck.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedFileKey to serialize bytes as base64 strings.
func (efk *EncryptedFileKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(efk.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(efk.Nonce),
KeyVersion: efk.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedFileKey to handle URL-safe base64 strings.
func (efk *EncryptedFileKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedFileKey into alias: %w", err)
}
// Set KeyVersion
efk.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedFileKey.Ciphertext: %w", err)
}
efk.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedFileKey.Nonce: %w", err)
}
efk.Nonce = nonceBytes
}
return nil
}
// TagKey encrypts tag data (name and color)
type TagKey struct {
Key []byte `json:"key" bson:"key"`
TagID string `json:"tag_id" bson:"tag_id"`
}
// EncryptedTagKey is the tag key encrypted with user's master key
type EncryptedTagKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (etk *EncryptedTagKey) NeedsRotation(policy KeyRotationPolicy) bool {
if etk.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*etk.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedTagKey to serialize bytes as base64 strings.
func (etk *EncryptedTagKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(etk.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(etk.Nonce),
KeyVersion: etk.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedTagKey to handle URL-safe base64 strings.
func (etk *EncryptedTagKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedTagKey into alias: %w", err)
}
// Set KeyVersion
etk.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedTagKey.Ciphertext: %w", err)
}
etk.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedTagKey.Nonce: %w", err)
}
etk.Nonce = nonceBytes
}
return nil
}
// MasterKeyEncryptedWithRecoveryKey allows account recovery
type MasterKeyEncryptedWithRecoveryKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}

View file

@ -0,0 +1,39 @@
// monorepo/cloud/maplefile-backend/internal/domain/crypto/domain/keys/rotation.go
package crypto
import (
"time"
"github.com/gocql/gocql"
)
// EncryptedHistoricalKey represents a previous version of a key
type EncryptedHistoricalKey struct {
KeyVersion int `json:"key_version" bson:"key_version"`
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
RotatedAt time.Time `json:"rotated_at" bson:"rotated_at"`
RotatedReason string `json:"rotated_reason" bson:"rotated_reason"`
// Algorithm used for this key version
Algorithm string `json:"algorithm" bson:"algorithm"`
}
// KeyRotationPolicy defines when and how to rotate keys
type KeyRotationPolicy struct {
MaxKeyAge time.Duration `json:"max_key_age" bson:"max_key_age"`
MaxKeyUsageCount int64 `json:"max_key_usage_count" bson:"max_key_usage_count"`
ForceRotateOnBreach bool `json:"force_rotate_on_breach" bson:"force_rotate_on_breach"`
}
// KeyRotationRecord tracks rotation events
type KeyRotationRecord struct {
ID gocql.UUID `bson:"_id" json:"id"`
EntityType string `bson:"entity_type" json:"entity_type"` // "user", "collection", "file"
EntityID gocql.UUID `bson:"entity_id" json:"entity_id"`
FromVersion int `bson:"from_version" json:"from_version"`
ToVersion int `bson:"to_version" json:"to_version"`
RotatedAt time.Time `bson:"rotated_at" json:"rotated_at"`
RotatedBy gocql.UUID `bson:"rotated_by" json:"rotated_by"`
Reason string `bson:"reason" json:"reason"`
AffectedItems int64 `bson:"affected_items" json:"affected_items"`
}