diff --git a/LICENSE b/LICENSE index 0519ecb..8e4b016 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,21 @@ - \ No newline at end of file +MIT License + +Copyright (c) 2024 Go File Encryption + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index ff2cfc3..40d7cd0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - ### Makefile ```Makefile @@ -27,5 +26,16 @@ clean: @echo "Cleaning up..." rm -f $(BINARY_NAME) -# Default target: build the binary -all: build +# Run all tests +test: + @echo "Running tests..." + go test -v ./... + +# Run tests with coverage +test-coverage: + @echo "Running tests with coverage..." + go test -v -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +# Default target: build the binary and run tests +all: build test diff --git a/README.md b/README.md index ce2138a..d351e19 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,39 @@ This project provides a robust implementation of file encryption and decryption ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. + +## Testing + +### Running Tests + +1. **Run all tests:** + + ```sh + make test + ``` + +2. **Run tests with coverage:** + + ```sh + make test-coverage + ``` + + This will generate a coverage report in HTML format (coverage.html) + +### Test Coverage + +The test suite includes: + +- Unit tests for each encryption algorithm (AES, DES, RC4) +- Key size validation tests +- Encryption/decryption round-trip tests +- Error handling tests + +### Writing Tests + +When contributing new features, please ensure: + +- All new code is thoroughly tested +- Tests are placed in the appropriate `_test.go` file +- Test coverage is maintained or improved +- Tests are clear and well-documented diff --git a/main.go b/main.go index e9d811e..fb189a9 100644 --- a/main.go +++ b/main.go @@ -1,171 +1,14 @@ package main import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/des" - "crypto/rand" - "crypto/rc4" "fmt" - "io" "log" "os" - "strings" "github.com/joho/godotenv" + "github.com/theognis1002/go-encrypt/test" ) -type EncryptionAlgorithm interface { - Encrypt(data []byte, key []byte) ([]byte, error) - Decrypt(data []byte, key []byte) ([]byte, error) - GetKeySize() int -} - -type AESAlgorithm struct{} -type DESAlgorithm struct{} -type RC4Algorithm struct{} - -// AES implementation -func (a *AESAlgorithm) Encrypt(data []byte, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - return gcm.Seal(nonce, nonce, data, nil), nil -} - -func (a *AESAlgorithm) Decrypt(data []byte, key []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, fmt.Errorf("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - return gcm.Open(nil, nonce, ciphertext, nil) -} - -func (a *AESAlgorithm) GetKeySize() int { - return 16 // AES-128 -} - -// DES implementation -func (d *DESAlgorithm) Encrypt(data []byte, key []byte) ([]byte, error) { - block, err := des.NewCipher(key) - if err != nil { - return nil, err - } - - blockSize := block.BlockSize() - data = pkcs7Padding(data, blockSize) - - ciphertext := make([]byte, len(data)) - iv := make([]byte, blockSize) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - - mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(ciphertext, data) - - // Prepend IV to ciphertext - return append(iv, ciphertext...), nil -} - -func (d *DESAlgorithm) Decrypt(data []byte, key []byte) ([]byte, error) { - block, err := des.NewCipher(key) - if err != nil { - return nil, err - } - - blockSize := block.BlockSize() - if len(data) < blockSize { - return nil, fmt.Errorf("ciphertext too short") - } - - iv := data[:blockSize] - data = data[blockSize:] - - mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(data, data) - - return pkcs7Unpadding(data) -} - -func (d *DESAlgorithm) GetKeySize() int { - return 8 // DES uses 8-byte keys -} - -// RC4 implementation -func (r *RC4Algorithm) Encrypt(data []byte, key []byte) ([]byte, error) { - cipher, err := rc4.NewCipher(key) - if err != nil { - return nil, err - } - - result := make([]byte, len(data)) - cipher.XORKeyStream(result, data) - return result, nil -} - -func (r *RC4Algorithm) Decrypt(data []byte, key []byte) ([]byte, error) { - return r.Encrypt(data, key) // RC4 encryption and decryption are the same operation -} - -func (r *RC4Algorithm) GetKeySize() int { - return 16 // Using 16 bytes for RC4 key -} - -// Helper functions for padding -func pkcs7Padding(data []byte, blockSize int) []byte { - padding := blockSize - len(data)%blockSize - padText := bytes.Repeat([]byte{byte(padding)}, padding) - return append(data, padText...) -} - -func pkcs7Unpadding(data []byte) ([]byte, error) { - length := len(data) - if length == 0 { - return nil, fmt.Errorf("invalid padding") - } - padding := int(data[length-1]) - return data[:length-padding], nil -} - -func getAlgorithm(name string) (EncryptionAlgorithm, error) { - switch strings.ToUpper(name) { - case "AES": - return &AESAlgorithm{}, nil - case "DES": - return &DESAlgorithm{}, nil - case "RC4": - return &RC4Algorithm{}, nil - default: - return nil, fmt.Errorf("unsupported algorithm: %s", name) - } -} - func main() { err := godotenv.Load() if err != nil { @@ -178,25 +21,28 @@ func main() { encryptedFile := os.Getenv("ENCRYPTED_FILE") decryptedFile := os.Getenv("DECRYPTED_FILE") - algorithm, err := getAlgorithm(algorithmName) - if err != nil { - log.Fatalf("Error: %v", err) - } - - if len(key) != algorithm.GetKeySize() { - log.Fatalf("Key length must be %d bytes for %s", algorithm.GetKeySize(), algorithmName) - } - // Read input file data, err := os.ReadFile(inputFile) if err != nil { log.Fatalf("Error reading input file: %v", err) } - // Encrypt - encrypted, err := algorithm.Encrypt(data, key) - if err != nil { - log.Fatalf("Error encrypting: %v", err) + var encrypted, decrypted []byte + var errEncrypt error + + switch algorithmName { + case "AES": + encrypted, errEncrypt = test.EncryptAES(data, key) + case "DES": + encrypted, errEncrypt = test.EncryptDES(data, key) + case "RC4": + encrypted, errEncrypt = test.EncryptRC4(data, key) + default: + log.Fatalf("Unsupported algorithm: %s", algorithmName) + } + + if errEncrypt != nil { + log.Fatalf("Error encrypting: %v", errEncrypt) } err = os.WriteFile(encryptedFile, encrypted, 0644) if err != nil { @@ -205,7 +51,7 @@ func main() { fmt.Println("File encrypted successfully") // Decrypt - decrypted, err := algorithm.Decrypt(encrypted, key) + decrypted, err = test.Decrypt(encrypted, key) if err != nil { log.Fatalf("Error decrypting: %v", err) } diff --git a/test/encryption.go b/test/encryption.go new file mode 100644 index 0000000..5133f49 --- /dev/null +++ b/test/encryption.go @@ -0,0 +1,126 @@ +package test + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/des" + "crypto/rand" + "crypto/rc4" + "fmt" + "io" +) + +// AES encryption/decryption +func EncryptAES(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func DecryptAES(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} + +// DES encryption/decryption +func EncryptDES(data []byte, key []byte) ([]byte, error) { + block, err := des.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + data = pkcs7Padding(data, blockSize) + + ciphertext := make([]byte, len(data)) + iv := make([]byte, blockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, data) + + return append(iv, ciphertext...), nil +} + +func DecryptDES(data []byte, key []byte) ([]byte, error) { + block, err := des.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + if len(data) < blockSize { + return nil, fmt.Errorf("ciphertext too short") + } + + iv := data[:blockSize] + data = data[blockSize:] + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(data, data) + + return pkcs7Unpadding(data) +} + +// RC4 encryption/decryption +func EncryptRC4(data []byte, key []byte) ([]byte, error) { + cipher, err := rc4.NewCipher(key) + if err != nil { + return nil, err + } + + result := make([]byte, len(data)) + cipher.XORKeyStream(result, data) + return result, nil +} + +func DecryptRC4(data []byte, key []byte) ([]byte, error) { + return EncryptRC4(data, key) // RC4 encryption and decryption are the same +} + +// Helper functions +func pkcs7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +func pkcs7Unpadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, fmt.Errorf("invalid padding") + } + padding := int(data[length-1]) + return data[:length-padding], nil +} diff --git a/test/encryption_test.go b/test/encryption_test.go new file mode 100644 index 0000000..2983657 --- /dev/null +++ b/test/encryption_test.go @@ -0,0 +1,70 @@ +package test + +import ( + "bytes" + "testing" +) + +// Test data +var ( + testKey = []byte("1234567890123456") // 16-byte key for AES + testData = []byte("Hello, World!") +) + +func TestAESEncryption(t *testing.T) { + encrypted, err := EncryptAES(testData, testKey) + if err != nil { + t.Fatalf("Encryption failed: %v", err) + } + + decrypted, err := DecryptAES(encrypted, testKey) + if err != nil { + t.Fatalf("Decryption failed: %v", err) + } + + if !bytes.Equal(testData, decrypted) { + t.Error("Decrypted data doesn't match original") + } +} + +func TestDESEncryption(t *testing.T) { + desKey := []byte("12345678") // 8-byte key for DES + + encrypted, err := EncryptDES(testData, desKey) + if err != nil { + t.Fatalf("Encryption failed: %v", err) + } + + decrypted, err := DecryptDES(encrypted, desKey) + if err != nil { + t.Fatalf("Decryption failed: %v", err) + } + + if !bytes.Equal(testData, decrypted) { + t.Error("Decrypted data doesn't match original") + } +} + +func TestRC4Encryption(t *testing.T) { + encrypted, err := EncryptRC4(testData, testKey) + if err != nil { + t.Fatalf("Encryption failed: %v", err) + } + + decrypted, err := DecryptRC4(encrypted, testKey) + if err != nil { + t.Fatalf("Decryption failed: %v", err) + } + + if !bytes.Equal(testData, decrypted) { + t.Error("Decrypted data doesn't match original") + } +} + +func TestInvalidKeySize(t *testing.T) { + invalidKey := []byte("tooshort") + _, err := EncryptAES(testData, invalidKey) + if err == nil { + t.Error("Expected error for invalid AES key size") + } +}