2487681396
gofmt -w across the controller tree (46 files) so gofmt -l is empty — disarms the
formatting landmine where a targeted edit + accidental gofmt -w swept ~46 unrelated
files. Pure formatting: whitespace + gofmt's optional-semicolon removal in reflowed
inline closures. One doc comment reworded ('' -> 'the empty string') to avoid gofmt's
Go-1.19 doc-comment typographic substitition ('' -> curly quote) muddying its meaning.
No build/vet/test behavior change.
229 lines
5.2 KiB
Go
229 lines
5.2 KiB
Go
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()
|
|
}
|