Files
rclone/backend/crypt/crypt_internal_test.go
Tomasz Raganowicz a5d6af71d0 crypt: Implement V2 cipher version with MD5 hash support
In V2 version each file encrypted using separate CEK (wrapped with AES Keywrap RFC3394). Truncation protection uses last byte of nonce to indicate last encryption block.
Added MD5 hash support for crypt.go (only if V2 cipher is used), MD5 of plaintext is encrypted and authenticated at the end of file.
Added 4 reserved bytes at the beginning for future use.
Added 1 hash type byte in the footer to allow upgrade to different than MD5 hash.
cipher_version setting determines hash version used for writes.
For reads we determine cipher version by reading header magic bytes, that's regardless of cipher_version setting. A
Added TestNewEncrypterV2 test to demonstrate the outputs and make sure that outputs match the expected hardcoded values.
Modified other tests to satisfy newly added params (e.g. cek).
Related to: https://github.com/rclone/rclone/issues/7192
2024-09-26 15:12:37 +02:00

145 lines
3.8 KiB
Go

package crypt
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"io"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/lib/random"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Create a temporary local fs to upload things from
func makeTempLocalFs(t *testing.T) (localFs fs.Fs) {
localFs, err := fs.TemporaryLocalFs(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, localFs.Rmdir(context.Background(), ""))
})
return localFs
}
// Upload a file to a remote
func uploadFile(t *testing.T, f fs.Fs, remote, contents string) (obj fs.Object) {
inBuf := bytes.NewBufferString(contents)
t1 := time.Date(2012, time.December, 17, 18, 32, 31, 0, time.UTC)
upSrc := object.NewStaticObjectInfo(remote, t1, int64(len(contents)), true, nil, nil)
obj, err := f.Put(context.Background(), inBuf, upSrc)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, obj.Remove(context.Background()))
})
return obj
}
// Test the ObjectInfo
func testObjectInfo(t *testing.T, f *Fs, wrap bool) {
var (
contents = random.String(100)
path = "hash_test_object"
ctx = context.Background()
)
if wrap {
path = "_wrap"
}
localFs := makeTempLocalFs(t)
obj := uploadFile(t, localFs, path, contents)
// encrypt the data
inBuf := bytes.NewBufferString(contents)
var outBuf bytes.Buffer
var preHasher *hash.MultiHasher
var wrappedIn io.Reader
if f.cipher.version == CipherVersionV2 {
wrappedIn, preHasher, _ = wrapReaderCalculatePlaintextHash(inBuf)
} else {
wrappedIn = inBuf
}
enc, err := f.cipher.newEncrypter(wrappedIn, nil, nil)
require.NoError(t, err)
nonce := enc.nonce // read the nonce at the start
cek := enc.cek
_, err = io.Copy(&outBuf, enc)
if f.cipher.version == CipherVersionV2 { // Append hash to the end
emptyReader := bytes.NewReader([]byte{})
hashReader := wrapReaderAppendPlaintextHash(emptyReader, preHasher, enc)
_, err = io.Copy(&outBuf, hashReader)
}
require.NoError(t, err)
var oi fs.ObjectInfo = obj
if wrap {
// wrap the object in an fs.ObjectUnwrapper if required
oi = fs.NewOverrideRemote(oi, "new_remote")
}
// wrap the object in a crypt for upload using the nonce we
// saved from the encrypter
src := f.newObjectInfo(oi, nonce, cek)
// Test ObjectInfo methods
if !f.opt.NoDataEncryption {
assert.Equal(t, int64(outBuf.Len()), src.Size())
}
assert.Equal(t, f, src.Fs())
assert.NotEqual(t, path, src.Remote())
// Test ObjectInfo.Hash
wantHash := md5.Sum(outBuf.Bytes())
gotHash, err := src.Hash(ctx, hash.MD5)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("%x", wantHash), gotHash)
}
func testComputeHash(t *testing.T, f *Fs) {
var (
contents = random.String(100)
path = "compute_hash_test"
ctx = context.Background()
hashType = f.Fs.Hashes().GetOne()
)
if hashType == hash.None {
t.Skipf("%v: does not support hashes", f.Fs)
}
localFs := makeTempLocalFs(t)
// Upload a file to localFs as a test object
localObj := uploadFile(t, localFs, path, contents)
// Upload the same data to the remote Fs also
remoteObj := uploadFile(t, f, path, contents)
// Calculate the expected Hash of the remote object
computedHash, err := f.ComputeHash(ctx, remoteObj.(*Object), localObj, hashType)
require.NoError(t, err)
// Test computed hash matches remote object hash
remoteObjHash, err := remoteObj.(*Object).Object.Hash(ctx, hashType)
require.NoError(t, err)
assert.Equal(t, remoteObjHash, computedHash)
}
// InternalTest is called by fstests.Run to extra tests
func (f *Fs) InternalTest(t *testing.T) {
t.Run("ObjectInfo", func(t *testing.T) { testObjectInfo(t, f, false) })
t.Run("ObjectInfoWrap", func(t *testing.T) { testObjectInfo(t, f, true) })
t.Run("ComputeHash", func(t *testing.T) { testComputeHash(t, f) })
}