package appexport import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "os" "golang.org/x/crypto/scrypt" ) const ( magicHeader = "FABE" // Felhom App Bundle Encrypted scryptN = 1 << 15 // 32768 scryptR = 8 scryptP = 1 saltSize = 32 aesKeySize = 32 hmacKeySize = 32 ivSize = aes.BlockSize // 16 ) // deriveKeys derives an AES-256 key and HMAC-SHA256 key from password + salt. func deriveKeys(password string, salt []byte) (aesKey, hmacKey []byte, err error) { derived, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, aesKeySize+hmacKeySize) if err != nil { return nil, nil, err } return derived[:aesKeySize], derived[aesKeySize:], nil } // IsEncryptedFAB checks if a file starts with the "FABE" magic header. func IsEncryptedFAB(path string) (bool, error) { f, err := os.Open(path) if err != nil { return false, err } defer f.Close() magic := make([]byte, 4) n, err := f.Read(magic) if err != nil || n < 4 { return false, nil } return string(magic) == magicHeader, nil } // EncryptFile encrypts a plaintext file with a password. // Uses AES-256-CTR + HMAC-SHA256 with scrypt key derivation. // Format: "FABE" (4) || salt (32) || IV (16) || encrypted_data || HMAC-SHA256 (32) func EncryptFile(inputPath, outputPath, password string) error { in, err := os.Open(inputPath) if err != nil { return fmt.Errorf("open input: %w", err) } defer in.Close() out, err := os.Create(outputPath) if err != nil { return fmt.Errorf("create output: %w", err) } defer out.Close() salt := make([]byte, saltSize) if _, err := rand.Read(salt); err != nil { return fmt.Errorf("generating salt: %w", err) } iv := make([]byte, ivSize) if _, err := rand.Read(iv); err != nil { return fmt.Errorf("generating IV: %w", err) } aesKey, hmKey, err := deriveKeys(password, salt) if err != nil { return fmt.Errorf("deriving keys: %w", err) } block, err := aes.NewCipher(aesKey) if err != nil { return err } stream := cipher.NewCTR(block, iv) mac := hmac.New(sha256.New, hmKey) // Write header (magic is NOT in HMAC; salt + IV are) if _, err := out.Write([]byte(magicHeader)); err != nil { return err } mac.Write(salt) if _, err := out.Write(salt); err != nil { return err } mac.Write(iv) if _, err := out.Write(iv); err != nil { return err } // Encrypt and stream data buf := make([]byte, 64*1024) for { n, readErr := in.Read(buf) if n > 0 { encrypted := make([]byte, n) stream.XORKeyStream(encrypted, buf[:n]) mac.Write(encrypted) if _, err := out.Write(encrypted); err != nil { return err } } if readErr == io.EOF { break } if readErr != nil { return fmt.Errorf("read: %w", readErr) } } // Append HMAC tag if _, err := out.Write(mac.Sum(nil)); err != nil { return err } return out.Sync() } // DecryptFile decrypts an encrypted .fab file with a password. // Returns a clear error if the password is wrong or the file is corrupted. func DecryptFile(inputPath, outputPath, password string) error { in, err := os.Open(inputPath) if err != nil { return fmt.Errorf("open input: %w", err) } defer in.Close() // Verify magic header magic := make([]byte, 4) if _, err := io.ReadFull(in, magic); err != nil { return fmt.Errorf("reading header: %w", err) } if string(magic) != magicHeader { return errors.New("not an encrypted FAB file") } // Read salt and IV salt := make([]byte, saltSize) if _, err := io.ReadFull(in, salt); err != nil { return fmt.Errorf("reading salt: %w", err) } iv := make([]byte, ivSize) if _, err := io.ReadFull(in, iv); err != nil { return fmt.Errorf("reading IV: %w", err) } // Calculate data size (total - header - HMAC tag) stat, err := in.Stat() if err != nil { return err } headerSize := int64(4 + saltSize + ivSize) tagSize := int64(sha256.Size) dataSize := stat.Size() - headerSize - tagSize if dataSize < 0 { return errors.New("file too small to be valid") } aesKey, hmKey, err := deriveKeys(password, salt) if err != nil { return fmt.Errorf("deriving keys: %w", err) } mac := hmac.New(sha256.New, hmKey) mac.Write(salt) mac.Write(iv) block, err := aes.NewCipher(aesKey) if err != nil { return err } stream := cipher.NewCTR(block, iv) out, err := os.Create(outputPath) if err != nil { return fmt.Errorf("create output: %w", err) } defer out.Close() // Decrypt data section buf := make([]byte, 64*1024) remaining := dataSize for remaining > 0 { toRead := int64(len(buf)) if toRead > remaining { toRead = remaining } n, readErr := in.Read(buf[:toRead]) if n > 0 { mac.Write(buf[:n]) decrypted := make([]byte, n) stream.XORKeyStream(decrypted, buf[:n]) if _, err := out.Write(decrypted); err != nil { return err } remaining -= int64(n) } if readErr == io.EOF { break } if readErr != nil { return fmt.Errorf("read: %w", readErr) } } // Verify HMAC tag storedMAC := make([]byte, sha256.Size) if _, err := io.ReadFull(in, storedMAC); err != nil { return fmt.Errorf("reading HMAC: %w", err) } if !hmac.Equal(mac.Sum(nil), storedMAC) { os.Remove(outputPath) return errors.New("jelszó hibás vagy a fájl sérült") } return out.Sync() }