repository: move crypto package to internal/repository/crypto

This commit is contained in:
Michael Eischer
2026-06-05 13:26:29 +02:00
parent 35e60dc2c7
commit c8a0bc2c5e
22 changed files with 16 additions and 16 deletions
+19
View File
@@ -0,0 +1,19 @@
package crypto
// NewBlobBuffer returns a buffer that is large enough to hold a blob of size
// plaintext bytes, including the crypto overhead.
func NewBlobBuffer(size int) []byte {
return make([]byte, size, size+Extension)
}
// PlaintextLength returns the plaintext length of a blob with ciphertextSize
// bytes.
func PlaintextLength(ciphertextSize int) int {
return ciphertextSize - Extension
}
// CiphertextLength returns the encrypted length of a blob with plaintextSize
// bytes.
func CiphertextLength(plaintextSize int) int {
return plaintextSize + Extension
}
+328
View File
@@ -0,0 +1,328 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"github.com/restic/restic/internal/errors"
"golang.org/x/crypto/poly1305"
)
const (
aesKeySize = 32 // for AES-256
macKeySizeK = 16 // for AES-128
macKeySizeR = 16 // for Poly1305
macKeySize = macKeySizeK + macKeySizeR // for Poly1305-AES128
ivSize = aes.BlockSize
macSize = poly1305.TagSize
// Extension is the number of bytes a plaintext is enlarged by encrypting it.
Extension = ivSize + macSize
)
var (
// ErrUnauthenticated is returned when ciphertext verification has failed.
ErrUnauthenticated = fmt.Errorf("ciphertext verification failed")
)
// Key holds encryption and message authentication keys for a repository. It is stored
// encrypted and authenticated as a JSON data structure in the Data field of the Key
// structure.
type Key struct {
MACKey `json:"mac"`
EncryptionKey `json:"encrypt"`
}
// EncryptionKey is key used for encryption
type EncryptionKey [32]byte
// MACKey is used to sign (authenticate) data.
type MACKey struct {
K [16]byte // for AES-128
R [16]byte // for Poly1305
}
func poly1305MAC(msg []byte, nonce []byte, key *MACKey) []byte {
k := poly1305PrepareKey(nonce, key)
var out [16]byte
poly1305.Sum(&out, msg, &k)
return out[:]
}
// construct mac key from slice (k||r), with masking
func macKeyFromSlice(mk *MACKey, data []byte) {
copy(mk.K[:], data[:16])
copy(mk.R[:], data[16:32])
}
// prepare key for low-level poly1305.Sum(): r||n
func poly1305PrepareKey(nonce []byte, key *MACKey) [32]byte {
var k [32]byte
cipher, err := aes.NewCipher(key.K[:])
if err != nil {
panic(err)
}
cipher.Encrypt(k[16:], nonce[:])
copy(k[:16], key.R[:])
return k
}
func poly1305Verify(msg []byte, nonce []byte, key *MACKey, mac []byte) bool {
k := poly1305PrepareKey(nonce, key)
var m [16]byte
copy(m[:], mac)
return poly1305.Verify(&m, msg, &k)
}
// NewRandomKey returns new encryption and message authentication keys.
func NewRandomKey() *Key {
k := &Key{}
n, err := rand.Read(k.EncryptionKey[:])
if n != aesKeySize || err != nil {
panic("unable to read enough random bytes for encryption key")
}
n, err = rand.Read(k.MACKey.K[:])
if n != macKeySizeK || err != nil {
panic("unable to read enough random bytes for MAC encryption key")
}
n, err = rand.Read(k.MACKey.R[:])
if n != macKeySizeR || err != nil {
panic("unable to read enough random bytes for MAC key")
}
return k
}
// NewRandomNonce returns a new random nonce. It panics on error so that the
// program is safely terminated.
func NewRandomNonce() []byte {
iv := make([]byte, ivSize)
n, err := rand.Read(iv)
if n != ivSize || err != nil {
panic("unable to read enough random bytes for iv")
}
return iv
}
type jsonMACKey struct {
K []byte `json:"k"`
R []byte `json:"r"`
}
// MarshalJSON converts the MACKey to JSON.
func (m *MACKey) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonMACKey{K: m.K[:], R: m.R[:]})
}
// UnmarshalJSON fills the key m with data from the JSON representation.
func (m *MACKey) UnmarshalJSON(data []byte) error {
j := jsonMACKey{}
err := json.Unmarshal(data, &j)
if err != nil {
return errors.Wrap(err, "Unmarshal")
}
copy(m.K[:], j.K)
copy(m.R[:], j.R)
return nil
}
// Valid tests whether the key k is valid (i.e. not zero).
func (m *MACKey) Valid() bool {
nonzeroK := false
for i := 0; i < len(m.K); i++ {
if m.K[i] != 0 {
nonzeroK = true
}
}
if !nonzeroK {
return false
}
for i := 0; i < len(m.R); i++ {
if m.R[i] != 0 {
return true
}
}
return false
}
// MarshalJSON converts the EncryptionKey to JSON.
func (k *EncryptionKey) MarshalJSON() ([]byte, error) {
return json.Marshal(k[:])
}
// UnmarshalJSON fills the key k with data from the JSON representation.
func (k *EncryptionKey) UnmarshalJSON(data []byte) error {
d := make([]byte, aesKeySize)
err := json.Unmarshal(data, &d)
if err != nil {
return errors.Wrap(err, "Unmarshal")
}
copy(k[:], d)
return nil
}
// Valid tests whether the key k is valid (i.e. not zero).
func (k *EncryptionKey) Valid() bool {
for i := 0; i < len(k); i++ {
if k[i] != 0 {
return true
}
}
return false
}
// validNonce checks that nonce is not all zero.
func validNonce(nonce []byte) bool {
var sum byte
for _, b := range nonce {
sum |= b
}
return sum > 0
}
// statically ensure that *Key implements crypto/cipher.AEAD
var _ cipher.AEAD = &Key{}
// NonceSize returns the size of the nonce that must be passed to Seal
// and Open.
func (k *Key) NonceSize() int {
return ivSize
}
// Overhead returns the maximum difference between the lengths of a
// plaintext and its ciphertext.
func (k *Key) Overhead() int {
return macSize
}
// sliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
//
// taken from the stdlib, crypto/aes/aes_gcm.go
func sliceForAppend(in []byte, n int) (head, tail []byte) {
if total := len(in) + n; cap(in) >= total {
head = in[:total]
} else {
head = make([]byte, total)
copy(head, in)
}
tail = head[len(in):]
return
}
// Seal encrypts and authenticates plaintext, authenticates the
// additional data and appends the result to dst, returning the updated
// slice. The nonce must be NonceSize() bytes long and unique for all
// time, for a given key.
//
// The plaintext and dst may alias exactly or not at all. To reuse
// plaintext's storage for the encrypted output, use plaintext[:0] as dst.
func (k *Key) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if !k.Valid() {
panic("key is invalid")
}
if len(additionalData) > 0 {
panic("additional data is not supported")
}
if len(nonce) != ivSize {
panic("incorrect nonce length")
}
if !validNonce(nonce) {
panic("nonce is invalid")
}
ret, out := sliceForAppend(dst, len(plaintext)+k.Overhead())
c, err := aes.NewCipher(k.EncryptionKey[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
e := cipher.NewCTR(c, nonce)
e.XORKeyStream(out, plaintext)
mac := poly1305MAC(out[:len(plaintext)], nonce, &k.MACKey)
copy(out[len(plaintext):], mac)
return ret
}
// Open decrypts and authenticates ciphertext, authenticates the
// additional data and, if successful, appends the resulting plaintext
// to dst, returning the updated slice. The nonce must be NonceSize()
// bytes long and both it and the additional data must match the
// value passed to Seal.
//
// The ciphertext and dst may alias exactly or not at all. To reuse
// ciphertext's storage for the decrypted output, use ciphertext[:0] as dst.
//
// Even if the function fails, the contents of dst, up to its capacity,
// may be overwritten.
func (k *Key) Open(dst, nonce, ciphertext, _ []byte) ([]byte, error) {
if !k.Valid() {
return nil, errors.New("invalid key")
}
// check parameters
if len(nonce) != ivSize {
panic("incorrect nonce length")
}
if !validNonce(nonce) {
return nil, errors.New("nonce is invalid")
}
// check for plausible length
if len(ciphertext) < k.Overhead() {
return nil, errors.Errorf("trying to decrypt invalid data: ciphertext too short")
}
l := len(ciphertext) - macSize
ct, mac := ciphertext[:l], ciphertext[l:]
// verify mac
if !poly1305Verify(ct, nonce, &k.MACKey, mac) {
return nil, ErrUnauthenticated
}
ret, out := sliceForAppend(dst, len(ct))
c, err := aes.NewCipher(k.EncryptionKey[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
e := cipher.NewCTR(c, nonce)
e.XORKeyStream(out, ct)
return ret, nil
}
// Valid tests if the key is valid.
func (k *Key) Valid() bool {
return k.EncryptionKey.Valid() && k.MACKey.Valid()
}
@@ -0,0 +1,192 @@
package crypto
import (
"bytes"
"encoding/hex"
"testing"
)
// test vectors from http://cr.yp.to/mac/poly1305-20050329.pdf
var poly1305Tests = []struct {
msg []byte
r []byte
k []byte
nonce []byte
mac []byte
}{
{
[]byte("\xf3\xf6"),
[]byte("\x85\x1f\xc4\x0c\x34\x67\xac\x0b\xe0\x5c\xc2\x04\x04\xf3\xf7\x00"),
[]byte("\xec\x07\x4c\x83\x55\x80\x74\x17\x01\x42\x5b\x62\x32\x35\xad\xd6"),
[]byte("\xfb\x44\x73\x50\xc4\xe8\x68\xc5\x2a\xc3\x27\x5c\xf9\xd4\x32\x7e"),
[]byte("\xf4\xc6\x33\xc3\x04\x4f\xc1\x45\xf8\x4f\x33\x5c\xb8\x19\x53\xde"),
},
{
[]byte(""),
[]byte("\xa0\xf3\x08\x00\x00\xf4\x64\x00\xd0\xc7\xe9\x07\x6c\x83\x44\x03"),
[]byte("\x75\xde\xaa\x25\xc0\x9f\x20\x8e\x1d\xc4\xce\x6b\x5c\xad\x3f\xbf"),
[]byte("\x61\xee\x09\x21\x8d\x29\xb0\xaa\xed\x7e\x15\x4a\x2c\x55\x09\xcc"),
[]byte("\xdd\x3f\xab\x22\x51\xf1\x1a\xc7\x59\xf0\x88\x71\x29\xcc\x2e\xe7"),
},
{
[]byte("\x66\x3c\xea\x19\x0f\xfb\x83\xd8\x95\x93\xf3\xf4\x76\xb6\xbc\x24\xd7\xe6\x79\x10\x7e\xa2\x6a\xdb\x8c\xaf\x66\x52\xd0\x65\x61\x36"),
[]byte("\x48\x44\x3d\x0b\xb0\xd2\x11\x09\xc8\x9a\x10\x0b\x5c\xe2\xc2\x08"),
[]byte("\x6a\xcb\x5f\x61\xa7\x17\x6d\xd3\x20\xc5\xc1\xeb\x2e\xdc\xdc\x74"),
[]byte("\xae\x21\x2a\x55\x39\x97\x29\x59\x5d\xea\x45\x8b\xc6\x21\xff\x0e"),
[]byte("\x0e\xe1\xc1\x6b\xb7\x3f\x0f\x4f\xd1\x98\x81\x75\x3c\x01\xcd\xbe"),
}, {
[]byte("\xab\x08\x12\x72\x4a\x7f\x1e\x34\x27\x42\xcb\xed\x37\x4d\x94\xd1\x36\xc6\xb8\x79\x5d\x45\xb3\x81\x98\x30\xf2\xc0\x44\x91\xfa\xf0\x99\x0c\x62\xe4\x8b\x80\x18\xb2\xc3\xe4\xa0\xfa\x31\x34\xcb\x67\xfa\x83\xe1\x58\xc9\x94\xd9\x61\xc4\xcb\x21\x09\x5c\x1b\xf9"),
[]byte("\x12\x97\x6a\x08\xc4\x42\x6d\x0c\xe8\xa8\x24\x07\xc4\xf4\x82\x07"),
[]byte("\xe1\xa5\x66\x8a\x4d\x5b\x66\xa5\xf6\x8c\xc5\x42\x4e\xd5\x98\x2d"),
[]byte("\x9a\xe8\x31\xe7\x43\x97\x8d\x3a\x23\x52\x7c\x71\x28\x14\x9e\x3a"),
[]byte("\x51\x54\xad\x0d\x2c\xb2\x6e\x01\x27\x4f\xc5\x11\x48\x49\x1f\x1b"),
},
}
func TestPoly1305(t *testing.T) {
for _, test := range poly1305Tests {
key := &MACKey{}
copy(key.K[:], test.k)
copy(key.R[:], test.r)
mac := poly1305MAC(test.msg, test.nonce, key)
if !bytes.Equal(mac, test.mac) {
t.Fatalf("wrong mac calculated, want: %02x, got: %02x", test.mac, mac)
}
if !poly1305Verify(test.msg, test.nonce, key, test.mac) {
t.Fatalf("mac does not verify: mac: %02x", test.mac)
}
}
}
var testValues = []struct {
ekey EncryptionKey
skey MACKey
ciphertext []byte
plaintext []byte
}{
{
ekey: decodeArray32("303e8687b1d7db18421bdc6bb8588ccadac4d59ee87b8ff70c44e635790cafef"),
skey: MACKey{
K: decodeArray16("ef4d8824cb80b2bcc5fbff8a9b12a42c"),
R: decodeArray16("cc8d4b948ee0ebfe1d415de921d10353"),
},
ciphertext: decodeHex("69fb41c62d12def4593bd71757138606338f621aeaeb39da0fe4f99233f8037a54ea63338a813bcf3f75d8c3cc75dddf8750"),
plaintext: []byte("Dies ist ein Test!"),
},
}
func decodeArray16(s string) (dst [16]byte) {
data := decodeHex(s)
if len(data) != 16 {
panic("data has wrong length")
}
copy(dst[:], data)
return
}
func decodeArray32(s string) (dst [32]byte) {
data := decodeHex(s)
if len(data) != 32 {
panic("data has wrong length")
}
copy(dst[:], data)
return
}
// decodeHex decodes the string s and panics on error.
func decodeHex(s string) []byte {
d, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return d
}
func TestCrypto(t *testing.T) {
msg := make([]byte, 0, 8*1024*1024) // use 8MiB for now
for _, tv := range testValues {
// test encryption
k := &Key{
EncryptionKey: tv.ekey,
MACKey: tv.skey,
}
nonce := NewRandomNonce()
ciphertext := k.Seal(msg[0:], nonce, tv.plaintext, nil)
// decrypt message
buf := make([]byte, 0, len(tv.plaintext))
buf, err := k.Open(buf, nonce, ciphertext, nil)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, tv.plaintext) {
t.Fatalf("wrong plaintext returned")
}
// change mac, this must fail
ciphertext[len(ciphertext)-8] ^= 0x23
if _, err = k.Open(buf[:0], nonce, ciphertext, nil); err != ErrUnauthenticated {
t.Fatal("wrong MAC value not detected")
}
// reset mac
ciphertext[len(ciphertext)-8] ^= 0x23
// tamper with nonce, this must fail
nonce[2] ^= 0x88
if _, err = k.Open(buf[:0], nonce, ciphertext, nil); err != ErrUnauthenticated {
t.Fatal("tampered nonce not detected")
}
// reset nonce
nonce[2] ^= 0x88
// tamper with message, this must fail
ciphertext[16+5] ^= 0x85
if _, err = k.Open(buf[:0], nonce, ciphertext, nil); err != ErrUnauthenticated {
t.Fatal("tampered message not detected")
}
// test decryption
p := make([]byte, len(tv.ciphertext))
nonce, ciphertext = tv.ciphertext[:16], tv.ciphertext[16:]
p, err = k.Open(p[:0], nonce, ciphertext, nil)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(p, tv.plaintext) {
t.Fatalf("wrong plaintext: expected %q but got %q\n", tv.plaintext, p)
}
}
}
func TestNonceValid(t *testing.T) {
nonce := make([]byte, ivSize)
if validNonce(nonce) {
t.Error("null nonce detected as valid")
}
for i := 0; i < 100; i++ {
nonce = NewRandomNonce()
if !validNonce(nonce) {
t.Errorf("random nonce not detected as valid: %02x", nonce)
}
}
}
func BenchmarkNonceValid(b *testing.B) {
nonce := NewRandomNonce()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !validNonce(nonce) {
b.Fatal("nonce is invalid")
}
}
}
+302
View File
@@ -0,0 +1,302 @@
package crypto_test
import (
"bytes"
"crypto/rand"
"io"
"testing"
"github.com/restic/restic/internal/repository/crypto"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/chunker"
)
const testLargeCrypto = false
func TestEncryptDecrypt(t *testing.T) {
k := crypto.NewRandomKey()
tests := []int{5, 23, 2<<18 + 23, 1 << 20}
if testLargeCrypto {
tests = append(tests, 7<<20+123)
}
for _, size := range tests {
data := rtest.Random(42, size)
buf := make([]byte, 0, size+crypto.Extension)
nonce := crypto.NewRandomNonce()
ciphertext := k.Seal(buf[:0], nonce, data, nil)
rtest.Assert(t, len(ciphertext) == len(data)+k.Overhead(),
"ciphertext length does not match: want %d, got %d",
len(data)+crypto.Extension, len(ciphertext))
plaintext := make([]byte, 0, len(ciphertext))
plaintext, err := k.Open(plaintext[:0], nonce, ciphertext, nil)
rtest.OK(t, err)
rtest.Assert(t, len(plaintext) == len(data),
"plaintext length does not match: want %d, got %d",
len(data), len(plaintext))
rtest.Equals(t, plaintext, data)
}
}
func TestSmallBuffer(t *testing.T) {
k := crypto.NewRandomKey()
size := 600
data := make([]byte, size)
_, err := io.ReadFull(rand.Reader, data)
rtest.OK(t, err)
ciphertext := make([]byte, 0, size/2)
nonce := crypto.NewRandomNonce()
ciphertext = k.Seal(ciphertext[:0], nonce, data, nil)
// this must extend the slice
rtest.Assert(t, cap(ciphertext) > size/2,
"expected extended slice, but capacity is only %d bytes",
cap(ciphertext))
// check for the correct plaintext
plaintext := make([]byte, len(ciphertext))
plaintext, err = k.Open(plaintext[:0], nonce, ciphertext, nil)
rtest.OK(t, err)
rtest.Assert(t, bytes.Equal(plaintext, data),
"wrong plaintext returned")
}
func TestSameBuffer(t *testing.T) {
k := crypto.NewRandomKey()
size := 600
data := make([]byte, size)
_, err := io.ReadFull(rand.Reader, data)
rtest.OK(t, err)
ciphertext := make([]byte, 0, size+crypto.Extension)
nonce := crypto.NewRandomNonce()
ciphertext = k.Seal(ciphertext, nonce, data, nil)
// use the same buffer for decryption
ciphertext, err = k.Open(ciphertext[:0], nonce, ciphertext, nil)
rtest.OK(t, err)
rtest.Assert(t, bytes.Equal(ciphertext, data),
"wrong plaintext returned")
}
func encrypt(t testing.TB, k *crypto.Key, data, ciphertext, nonce []byte) []byte {
prefixlen := len(ciphertext)
ciphertext = k.Seal(ciphertext, nonce, data, nil)
if len(ciphertext) != len(data)+k.Overhead()+prefixlen {
t.Fatalf("destination slice has wrong length, want %d, got %d",
len(data)+k.Overhead(), len(ciphertext))
}
return ciphertext
}
func decryptNewSliceAndCompare(t testing.TB, k *crypto.Key, data, ciphertext, nonce []byte) {
plaintext := make([]byte, 0, len(ciphertext))
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
}
func decryptAndCompare(t testing.TB, k *crypto.Key, data, ciphertext, nonce, dst []byte) {
prefix := make([]byte, len(dst))
copy(prefix, dst)
plaintext, err := k.Open(dst, nonce, ciphertext, nil)
if err != nil {
t.Fatalf("unable to decrypt ciphertext: %v", err)
}
if len(data)+len(prefix) != len(plaintext) {
t.Fatalf("wrong plaintext returned, want %d bytes, got %d", len(data)+len(prefix), len(plaintext))
}
if !bytes.Equal(plaintext[:len(prefix)], prefix) {
t.Fatal("prefix is wrong")
}
if !bytes.Equal(plaintext[len(prefix):], data) {
t.Fatal("wrong plaintext returned")
}
}
func TestAppendOpen(t *testing.T) {
k := crypto.NewRandomKey()
nonce := crypto.NewRandomNonce()
data := make([]byte, 600)
_, err := io.ReadFull(rand.Reader, data)
rtest.OK(t, err)
ciphertext := encrypt(t, k, data, nil, nonce)
// we need to test several different cases:
// * destination slice is nil
// * destination slice is empty and has enough capacity
// * destination slice is empty and does not have enough capacity
// * destination slice contains data and has enough capacity
// * destination slice contains data and does not have enough capacity
// destination slice is nil
t.Run("nil", func(t *testing.T) {
var plaintext []byte
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
})
// destination slice is empty and has enough capacity
t.Run("empty-large", func(t *testing.T) {
plaintext := make([]byte, 0, len(data)+100)
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
})
// destination slice is empty and does not have enough capacity
t.Run("empty-small", func(t *testing.T) {
plaintext := make([]byte, 0, len(data)/2)
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
})
// destination slice contains data and has enough capacity
t.Run("prefix-large", func(t *testing.T) {
plaintext := make([]byte, 0, len(data)+100)
plaintext = append(plaintext, []byte("foobar")...)
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
})
// destination slice contains data and does not have enough capacity
t.Run("prefix-small", func(t *testing.T) {
plaintext := make([]byte, 0, len(data)/2)
plaintext = append(plaintext, []byte("foobar")...)
decryptAndCompare(t, k, data, ciphertext, nonce, plaintext)
})
}
func TestAppendSeal(t *testing.T) {
k := crypto.NewRandomKey()
data := make([]byte, 600)
_, err := io.ReadFull(rand.Reader, data)
rtest.OK(t, err)
// we need to test several different cases:
// * destination slice is nil
// * destination slice is empty and has enough capacity
// * destination slice is empty and does not have enough capacity
// * destination slice contains data and has enough capacity
// * destination slice contains data and does not have enough capacity
// destination slice is nil
t.Run("nil", func(t *testing.T) {
nonce := crypto.NewRandomNonce()
var ciphertext []byte
ciphertext = encrypt(t, k, data, ciphertext, nonce)
decryptNewSliceAndCompare(t, k, data, ciphertext, nonce)
})
// destination slice is empty and has enough capacity
t.Run("empty-large", func(t *testing.T) {
nonce := crypto.NewRandomNonce()
ciphertext := make([]byte, 0, len(data)+100)
ciphertext = encrypt(t, k, data, ciphertext, nonce)
decryptNewSliceAndCompare(t, k, data, ciphertext, nonce)
})
// destination slice is empty and does not have enough capacity
t.Run("empty-small", func(t *testing.T) {
nonce := crypto.NewRandomNonce()
ciphertext := make([]byte, 0, len(data)/2)
ciphertext = encrypt(t, k, data, ciphertext, nonce)
decryptNewSliceAndCompare(t, k, data, ciphertext, nonce)
})
// destination slice contains data and has enough capacity
t.Run("prefix-large", func(t *testing.T) {
nonce := crypto.NewRandomNonce()
ciphertext := make([]byte, 0, len(data)+100)
ciphertext = append(ciphertext, []byte("foobar")...)
ciphertext = encrypt(t, k, data, ciphertext, nonce)
if string(ciphertext[:6]) != "foobar" {
t.Errorf("prefix is missing")
}
decryptNewSliceAndCompare(t, k, data, ciphertext[6:], nonce)
})
// destination slice contains data and does not have enough capacity
t.Run("prefix-small", func(t *testing.T) {
nonce := crypto.NewRandomNonce()
ciphertext := make([]byte, 0, len(data)/2)
ciphertext = append(ciphertext, []byte("foobar")...)
ciphertext = encrypt(t, k, data, ciphertext, nonce)
if string(ciphertext[:6]) != "foobar" {
t.Errorf("prefix is missing")
}
decryptNewSliceAndCompare(t, k, data, ciphertext[6:], nonce)
})
}
func TestLargeEncrypt(t *testing.T) {
if !testLargeCrypto {
t.SkipNow()
}
k := crypto.NewRandomKey()
for _, size := range []int{chunker.MaxSize, chunker.MaxSize + 1, chunker.MaxSize + 1<<20} {
data := make([]byte, size)
_, err := io.ReadFull(rand.Reader, data)
rtest.OK(t, err)
nonce := crypto.NewRandomNonce()
ciphertext := k.Seal(make([]byte, size+k.Overhead()), nonce, data, nil)
plaintext, err := k.Open([]byte{}, nonce, ciphertext, nil)
rtest.OK(t, err)
rtest.Equals(t, plaintext, data)
}
}
func BenchmarkEncrypt(b *testing.B) {
size := 8 << 20 // 8MiB
data := make([]byte, size)
k := crypto.NewRandomKey()
buf := make([]byte, len(data)+crypto.Extension)
nonce := crypto.NewRandomNonce()
b.ResetTimer()
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
_ = k.Seal(buf, nonce, data, nil)
}
}
func BenchmarkDecrypt(b *testing.B) {
size := 8 << 20 // 8MiB
data := make([]byte, size)
k := crypto.NewRandomKey()
plaintext := make([]byte, 0, size)
ciphertext := make([]byte, 0, size+crypto.Extension)
nonce := crypto.NewRandomNonce()
ciphertext = k.Seal(ciphertext, nonce, data, nil)
var err error
b.ResetTimer()
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
_, err = k.Open(plaintext, nonce, ciphertext, nil)
rtest.OK(b, err)
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package crypto provides all cryptographic operations needed in restic.
package crypto
+102
View File
@@ -0,0 +1,102 @@
package crypto
import (
"crypto/rand"
"time"
"github.com/restic/restic/internal/errors"
sscrypt "github.com/elithrar/simple-scrypt"
"golang.org/x/crypto/scrypt"
)
const saltLength = 64
// Params are the default parameters used for the key derivation function KDF().
type Params struct {
N int
R int
P int
}
// DefaultKDFParams are the default parameters used for Calibrate and KDF().
var DefaultKDFParams = Params{
N: sscrypt.DefaultParams.N,
R: sscrypt.DefaultParams.R,
P: sscrypt.DefaultParams.P,
}
// Calibrate determines new KDF parameters for the current hardware.
func Calibrate(timeout time.Duration, memory int) (Params, error) {
defaultParams := sscrypt.Params{
N: DefaultKDFParams.N,
R: DefaultKDFParams.R,
P: DefaultKDFParams.P,
DKLen: sscrypt.DefaultParams.DKLen,
SaltLen: sscrypt.DefaultParams.SaltLen,
}
params, err := sscrypt.Calibrate(timeout, memory, defaultParams)
if err != nil {
return DefaultKDFParams, errors.Wrap(err, "scrypt.Calibrate")
}
return Params{
N: params.N,
R: params.R,
P: params.P,
}, nil
}
// KDF derives encryption and message authentication keys from the password
// using the supplied parameters N, R and P and the Salt.
func KDF(p Params, salt []byte, password string) (*Key, error) {
if len(salt) != saltLength {
return nil, errors.Errorf("scrypt() called with invalid salt bytes (len %d)", len(salt))
}
// make sure we have valid parameters
params := sscrypt.Params{
N: p.N,
R: p.R,
P: p.P,
DKLen: sscrypt.DefaultParams.DKLen,
SaltLen: len(salt),
}
if err := params.Check(); err != nil {
return nil, errors.Wrap(err, "Check")
}
derKeys := &Key{}
keybytes := macKeySize + aesKeySize
scryptKeys, err := scrypt.Key([]byte(password), salt, p.N, p.R, p.P, keybytes)
if err != nil {
return nil, errors.Wrap(err, "scrypt.Key")
}
if len(scryptKeys) != keybytes {
return nil, errors.Errorf("invalid numbers of bytes expanded from scrypt(): %d", len(scryptKeys))
}
// first 32 byte of scrypt output is the encryption key
copy(derKeys.EncryptionKey[:], scryptKeys[:aesKeySize])
// next 32 byte of scrypt output is the mac key, in the form k||r
macKeyFromSlice(&derKeys.MACKey, scryptKeys[aesKeySize:])
return derKeys, nil
}
// NewSalt returns new random salt bytes to use with KDF(). If NewSalt returns
// an error, this is a grave situation and the program must abort and terminate.
func NewSalt() ([]byte, error) {
buf := make([]byte, saltLength)
n, err := rand.Read(buf)
if n != saltLength || err != nil {
panic("unable to read enough random bytes for new salt")
}
return buf, nil
}
+14
View File
@@ -0,0 +1,14 @@
package crypto
import (
"testing"
"time"
)
func TestCalibrate(t *testing.T) {
params, err := Calibrate(100*time.Millisecond, 50)
if err != nil {
t.Fatal(err)
}
t.Logf("testing calibrate, params after: %v", params)
}
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/klauspost/compress/zstd"
"golang.org/x/sync/errgroup"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
@@ -5,7 +5,7 @@ import (
"slices"
"testing"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test"
+1 -1
View File
@@ -13,8 +13,8 @@ import (
"sync"
"time"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
@@ -10,9 +10,9 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/restic/restic/internal/checker"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/data"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
+1 -1
View File
@@ -12,8 +12,8 @@ import (
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/repository/crypto"
)
var (
+1 -1
View File
@@ -3,7 +3,7 @@ package pack
import (
"fmt"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/restic"
)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
)
// ErrBroken is returned by Add and Finalize after a write error. The packer
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
+1 -1
View File
@@ -11,8 +11,8 @@ import (
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/mem"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
+1 -1
View File
@@ -15,9 +15,9 @@ import (
"github.com/restic/restic/internal/repository/hashing"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/pack"
)
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"sync"
"testing"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test"
)
+1 -1
View File
@@ -14,9 +14,9 @@ import (
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/cache"
"github.com/restic/restic/internal/backend/dryrun"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
@@ -14,8 +14,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/klauspost/compress/zstd"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
+1 -1
View File
@@ -18,9 +18,9 @@ import (
"github.com/restic/restic/internal/backend/cache"
"github.com/restic/restic/internal/backend/local"
"github.com/restic/restic/internal/backend/mem"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/repository/index"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/restic/restic/internal/backend/local"
"github.com/restic/restic/internal/backend/mem"
"github.com/restic/restic/internal/backend/retry"
"github.com/restic/restic/internal/crypto"
"github.com/restic/restic/internal/repository/crypto"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test"