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"` }