203 lines
4.1 KiB
Go
203 lines
4.1 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
envelope.go
|
|
|
|
Pack and unpack the crypha payload envelope
|
|
|
|
Layout: magic(4) ver(1) flags(1) [cipher(1) params(9) salt(16) nonce(12) if encrypted]
|
|
len(4) body(N) crc32(4). The header up to nonce is the AEAD additional data.
|
|
*/
|
|
|
|
package payload
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"hash/crc32"
|
|
)
|
|
|
|
func Pack(data []byte, opts Options) ([]byte, error) {
|
|
if len(data) == 0 {
|
|
return nil, ErrEmptyPayload
|
|
}
|
|
|
|
var flags byte
|
|
body := data
|
|
|
|
if opts.Compress {
|
|
c, err := compress(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body = c
|
|
flags |= flagCompressed
|
|
}
|
|
|
|
header := new(bytes.Buffer)
|
|
header.Write(magic[:])
|
|
header.WriteByte(currentVersion)
|
|
|
|
if len(opts.Passphrase) > 0 {
|
|
flags |= flagEncrypted
|
|
header.WriteByte(flags)
|
|
|
|
salt := make([]byte, saltLen)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return nil, err
|
|
}
|
|
params := paramsForStrength(opts.Strength)
|
|
key := deriveKey(opts.Passphrase, salt, params)
|
|
aead, cipherID, err := newAEAD(opts.Cipher, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nonce := make([]byte, aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
header.WriteByte(cipherID)
|
|
var pbuf [paramsLen]byte
|
|
binary.BigEndian.PutUint32(pbuf[0:4], params.time)
|
|
binary.BigEndian.PutUint32(pbuf[4:8], params.memory)
|
|
pbuf[8] = params.threads
|
|
header.Write(pbuf[:])
|
|
header.Write(salt)
|
|
header.Write(nonce)
|
|
|
|
body = aead.Seal(nil, nonce, body, header.Bytes())
|
|
} else {
|
|
header.WriteByte(flags)
|
|
}
|
|
|
|
out := new(bytes.Buffer)
|
|
out.Write(header.Bytes())
|
|
var meta [lenField]byte
|
|
binary.BigEndian.PutUint32(meta[:], uint32(len(body)))
|
|
out.Write(meta[:])
|
|
out.Write(body)
|
|
binary.BigEndian.PutUint32(meta[:], crc32.ChecksumIEEE(body))
|
|
out.Write(meta[:])
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
type parsed struct {
|
|
flags byte
|
|
aad []byte
|
|
cipherID byte
|
|
params kdfParams
|
|
salt []byte
|
|
nonce []byte
|
|
body []byte
|
|
}
|
|
|
|
func parse(env []byte) (*parsed, error) {
|
|
off := 0
|
|
remaining := func(n int) bool { return off+n <= len(env) }
|
|
|
|
if !remaining(len(magic)) || !bytes.Equal(env[0:len(magic)], magic[:]) {
|
|
return nil, ErrBadMagic
|
|
}
|
|
off = len(magic)
|
|
|
|
if !remaining(1) {
|
|
return nil, ErrTruncated
|
|
}
|
|
if env[off] != currentVersion {
|
|
return nil, ErrUnsupportedVersion
|
|
}
|
|
off++
|
|
|
|
if !remaining(1) {
|
|
return nil, ErrTruncated
|
|
}
|
|
p := &parsed{flags: env[off]}
|
|
off++
|
|
|
|
if p.flags&flagEncrypted != 0 {
|
|
if !remaining(1 + paramsLen + saltLen + nonceLen) {
|
|
return nil, ErrTruncated
|
|
}
|
|
p.cipherID = env[off]
|
|
off++
|
|
p.params = kdfParams{
|
|
time: binary.BigEndian.Uint32(env[off : off+4]),
|
|
memory: binary.BigEndian.Uint32(env[off+4 : off+8]),
|
|
threads: env[off+8],
|
|
}
|
|
off += paramsLen
|
|
if !p.params.valid() {
|
|
return nil, ErrBadParams
|
|
}
|
|
p.salt = env[off : off+saltLen]
|
|
off += saltLen
|
|
p.nonce = env[off : off+nonceLen]
|
|
off += nonceLen
|
|
p.aad = env[0:off]
|
|
}
|
|
|
|
if !remaining(lenField) {
|
|
return nil, ErrTruncated
|
|
}
|
|
bodyLen := int(binary.BigEndian.Uint32(env[off : off+lenField]))
|
|
off += lenField
|
|
if bodyLen < 0 || !remaining(bodyLen) {
|
|
return nil, ErrTruncated
|
|
}
|
|
p.body = env[off : off+bodyLen]
|
|
off += bodyLen
|
|
|
|
if !remaining(crcField) {
|
|
return nil, ErrTruncated
|
|
}
|
|
if crc32.ChecksumIEEE(p.body) != binary.BigEndian.Uint32(env[off:off+crcField]) {
|
|
return nil, ErrChecksumMismatch
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func Unpack(env, passphrase []byte) ([]byte, error) {
|
|
p, err := parse(env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body := p.body
|
|
if p.flags&flagEncrypted != 0 {
|
|
if len(passphrase) == 0 {
|
|
return nil, ErrPassphraseRequired
|
|
}
|
|
key := deriveKey(passphrase, p.salt, p.params)
|
|
aead, err := aeadByID(p.cipherID, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plain, err := aead.Open(nil, p.nonce, body, p.aad)
|
|
if err != nil {
|
|
return nil, ErrDecrypt
|
|
}
|
|
body = plain
|
|
}
|
|
|
|
if p.flags&flagCompressed != 0 {
|
|
return decompress(body)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func Validate(env []byte) error {
|
|
_, err := parse(env)
|
|
return err
|
|
}
|
|
|
|
func IsEncrypted(env []byte) (bool, error) {
|
|
p, err := parse(env)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return p.flags&flagEncrypted != 0, nil
|
|
}
|