-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from angelsolaorbaiceta/feat/encryption
feat: Encrypt/Decrypt archives
- Loading branch information
Showing
12 changed files
with
512 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.