Skip to content

Commit

Permalink
Merge pull request #2 from angelsolaorbaiceta/feat/encryption
Browse files Browse the repository at this point in the history
feat: Encrypt/Decrypt archives
  • Loading branch information
angelsolaorbaiceta authored Sep 5, 2024
2 parents bc88c95 + 18e7b00 commit 2787c37
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 33 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ Listing the contents of an archive:
$ aar list -f archive.aarch
```

Encrypting an archive:

```bash
$ aar encrypt -f archive.aarch
Password: <password>
Confirm password: <password>
```

Where `<password>` is the password you want to use to encrypt the archive, with a minimum length of 8 characters.
It removes the original _.aarch_ file and creates a new one with the encrypted data, with extension _.aarch.enc_.

> [!NOTE]
> The encryption is done using the AES-256-GCM algorithm, and it only works for angel archives.
Decrypting an archive:

```bash
$ aar decrypt -f archive.aarch.enc
Password: <password>
Confirm password: <password>
```

Where `<password>` is the password you used to encrypt the archive.
It removes the encrypted _.aarch.enc_ file and creates a new one with the decrypted data, with extension _.aarch_.

> [!NOTE]
> The decryption is done using the AES-256-GCM algorithm, and it only works for encrypted angel archives.
## File Format

### Archive Header
Expand Down
15 changes: 14 additions & 1 deletion archive/archive.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package archive

import "io"
import (
"bytes"
"io"
)

// An Archive represents a collection of files stored in a single file.
type Archive struct {
Expand All @@ -20,6 +23,16 @@ func (a *Archive) TotalSize() uint64 {
return total
}

// GetBytes returns the archive as a byte slice.
func (a *Archive) GetBytes() ([]byte, error) {
data := new(bytes.Buffer)
if err := a.Write(data); err != nil {
return nil, err
}

return data.Bytes(), nil
}

// Write writes the archive into the provided writer.
func (a *Archive) Write(w io.Writer) error {
if err := a.Header.Write(w); err != nil {
Expand Down
150 changes: 150 additions & 0 deletions archive/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package archive

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"io"

"golang.org/x/crypto/pbkdf2"
)

const (
saltSize = 16
nonceSize = 12
)

// An EncryptedArchive represents an encrypted archive.
type EncryptedArchive struct {
bytes []byte
salt []byte
nonce []byte
}

// Write writes the encrypted archive into the provided writer.
// The encrypted archive is serialized as follows:
//
// 1. The magic field is serialized as a 4-byte sequence.
// 2. The salt field is serialized as a 16-byte sequence.
// 3. The nonce field is serialized as a sequence of bytes.
// 4. The encrypted data is serialized as a sequence of bytes.
func (a *EncryptedArchive) Write(w io.Writer) error {
// Write the magic (4 bytes)
if _, err := w.Write(encMagic); err != nil {
return err
}

// Write the salt (16 bytes)
if _, err := w.Write(a.salt); err != nil {
return err
}

// Write the nonce
if _, err := w.Write(a.nonce); err != nil {
return err
}

// Write the encrypted data
if _, err := w.Write(a.bytes); err != nil {
return err
}

return nil
}

// ReadEncryptedArchive reads an encrypted archive from the provided reader.
func ReadEncryptedArchive(r io.Reader) (*EncryptedArchive, error) {
if err := mustReadEncryptedMagic(r); err != nil {
return nil, err
}

// Read the salt (16 bytes)
salt := make([]byte, saltSize)
if _, err := io.ReadFull(r, salt); err != nil {
return nil, err
}

// Read the nonce
nonce := make([]byte, nonceSize)
if _, err := io.ReadFull(r, nonce); err != nil {
return nil, err
}

// Read the encrypted data
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}

return &EncryptedArchive{
bytes: data,
salt: salt,
nonce: nonce,
}, nil
}

// Encrypt encrypts the archive using AES-GCM with the provided password.
func (a *Archive) Encrypt(password string) (*EncryptedArchive, error) {
// Generate a salt for key derivation (PBKDF2)
salt := make([]byte, saltSize)
if _, err := rand.Read(salt); err != nil {
return nil, err
}

aesGCM, err := newCipher(password, salt)
if err != nil {
return nil, err
}

// Generate a nonce for AES-GCM (random IV)
// aesGCM.NonceSize() returns 12 bytes
nonce := make([]byte, nonceSize)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}

// Get the plaintext data
plaintext, err := a.GetBytes()
if err != nil {
return nil, err
}

// Encrypt the data using AES-GCM
ciphertext := aesGCM.Seal(nil, nonce, plaintext, nil)

return &EncryptedArchive{
bytes: ciphertext,
salt: salt,
nonce: nonce,
}, nil
}

// Decrypt decrypts the encrypted archive using AES-GCM with the provided password.
// If the password is incorrect, the process will fail as the Archive data will be
// corrupted.
func (a *EncryptedArchive) Decrypt(password string) (*Archive, error) {
aesGCM, err := newCipher(password, a.salt)
if err != nil {
return nil, err
}

plaintext, err := aesGCM.Open(nil, a.nonce, a.bytes, nil)
if err != nil {
return nil, err
}

return ReadArchive(bytes.NewReader(plaintext))
}

// newCipher creates a new AES-GCM cipher with the provided password and salt.
func newCipher(password string, salt []byte) (cipher.AEAD, error) {
key := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

return cipher.NewGCM(block)
}
76 changes: 76 additions & 0 deletions archive/encryption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package archive

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEncryptDecryptArchive(t *testing.T) {
archive := makeTestArchive()

encrypted, err := archive.Encrypt("password")
assert.Nil(t, err)

decrypted, err := encrypted.Decrypt("password")
assert.Nil(t, err)

assert.Equal(t, archive, decrypted)
}

func TestWriteAndReadEncryptedArchive(t *testing.T) {
var (
archive = makeTestArchive()
encrypted, _ = archive.Encrypt("password")
w = new(bytes.Buffer)
)

err := encrypted.Write(w)
assert.Nil(t, err)

r := bytes.NewReader(w.Bytes())
readArchive, err := ReadEncryptedArchive(r)
assert.Nil(t, err)

assert.Equal(t, encrypted, readArchive)
}

func makeTestArchive() *Archive {
return &Archive{
Header: &Header{
HeaderLength: 46,
Entries: []*HeaderFileEntry{
{
Name: "file1.txt",
Size: 12,
Offset: 28,
},
{
Name: "file2.txt",
Size: 16,
Offset: 40,
},
},
},
Files: []*ArchiveFile{
{
FileName: "file1.txt",
CompressedBytes: []byte{
0x78, 0x9c, 0x4b, 0x4c,
0x4f, 0x49, 0x2d, 0x2e,
0x01, 0x00, 0x00, 0xff,
},
},
{
FileName: "file2.txt",
CompressedBytes: []byte{
0x78, 0x9c, 0x4b, 0x4c,
0x4f, 0x49, 0x2d, 0x2e,
0x01, 0x00, 0x00, 0xff,
0x78, 0x9c, 0x4b, 0x4c,
},
},
},
}
}
7 changes: 0 additions & 7 deletions archive/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ import (
"io"
)

// magic is a unique identifier for the archive format.
// It's the ASCII representation of "AAR?".
var magic = []byte{0x41, 0x41, 0x52, 0x3F}

// magicLen is the length of the magic field in bytes.
const magicLen = uint32(4)

// byteOrder is the byte order used to serialize integers.
var byteOrder = binary.LittleEndian

Expand Down
59 changes: 59 additions & 0 deletions archive/magic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package archive

import (
"bytes"
"fmt"
"io"
)

// magic is a unique identifier for the archive format.
// It's the ASCII representation of "AAR?".
var magic = []byte{0x41, 0x41, 0x52, 0x3F}

// encMagic is a unique identifier for the encrypted archive format.
// It's the ASCII representation of "AARX".
var encMagic = []byte{0x41, 0x41, 0x52, 0x58}

// magicLen is the length of the magic field in bytes.
const magicLen = uint32(4)

// ErrInvalidMagic is returned when the magic field is not correct.
var ErrInvalidMagic = fmt.Errorf("invalid magic, expected %v", magic)

// ErrInvalidEncMagic is returned when the magic field is not correct.
var ErrInvalidEncMagic = fmt.Errorf("invalid magic, expected %v", encMagic)

// mustReadMagic reads the magic field from the provided reader.
// If the magic field is not correct, it returns an error.
func mustReadMagic(r io.Reader) error {
readMagic := make([]byte, 4)

// Read the magic (4 bytes)
if _, err := io.ReadFull(r, readMagic); err != nil {
return err
}

// Check if the magic is correct
if !bytes.Equal(magic, readMagic) {
return ErrInvalidMagic
}

return nil
}

// mustReadEncryptedMagic reads the magic field from the provided reader.
func mustReadEncryptedMagic(r io.Reader) error {
readMagic := make([]byte, 4)

// Read the magic (4 bytes)
if _, err := io.ReadFull(r, readMagic); err != nil {
return err
}

// Check if the magic is correct
if !bytes.Equal(encMagic, readMagic) {
return ErrInvalidEncMagic
}

return nil
}
25 changes: 0 additions & 25 deletions archive/utils.go

This file was deleted.

Loading

0 comments on commit 2787c37

Please sign in to comment.