mirror of
https://github.com/rclone/rclone.git
synced 2025-07-17 12:45:26 +02:00
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
145 lines
3.8 KiB
Go
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) })
|
|
}
|