From 0ef3da2f23e903b49d6ca38b490292196715fbc5 Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 22 Jan 2025 10:45:46 -0500 Subject: [PATCH 1/4] rbd: fix a pair of erroneous comments Fix a couple of comments to make them a bit more accurate. They were probably true at one point in the development of the feature but now referred to functions that don't exist. Signed-off-by: John Mulligan --- rbd/encryption.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rbd/encryption.go b/rbd/encryption.go index a0ddfea9df..1fede749d0 100644 --- a/rbd/encryption.go +++ b/rbd/encryption.go @@ -64,7 +64,7 @@ func (opts EncryptionOptionsLUKS1) allocateEncryptionOptions() cEncryptionData { var cOpts C.rbd_encryption_luks1_format_options_t var retData cEncryptionData cOpts.alg = C.rbd_encryption_algorithm_t(opts.Alg) - //CBytes allocates memory which we'll free by calling cOptsFree() + // CBytes allocates memory. it will be freed when cEncryptionData.free is called cOpts.passphrase = (*C.char)(C.CBytes(opts.Passphrase)) cOpts.passphrase_size = C.size_t(len(opts.Passphrase)) retData.opts = C.rbd_encryption_options_t(&cOpts) @@ -78,7 +78,7 @@ func (opts EncryptionOptionsLUKS2) allocateEncryptionOptions() cEncryptionData { var cOpts C.rbd_encryption_luks2_format_options_t var retData cEncryptionData cOpts.alg = C.rbd_encryption_algorithm_t(opts.Alg) - //CBytes allocates memory which we'll free by calling cOptsFree() + // CBytes allocates memory. it will be freed when cEncryptionData.free is called cOpts.passphrase = (*C.char)(C.CBytes(opts.Passphrase)) cOpts.passphrase_size = C.size_t(len(opts.Passphrase)) retData.opts = C.rbd_encryption_options_t(&cOpts) From 8c89f462545f1207ba7d54a4f187a42c83e9fae2 Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 22 Jan 2025 16:25:04 -0500 Subject: [PATCH 2/4] rbd: clean up rbd encryption load test case Restructure the existing rbd encryption load test function so that it makes use of defer and uses subtests to divide things up more clearly. I needed to make this change in preparation for adding a binding for rbd_encryption_load2 and a similar test - but I couldn't make sense of the existing test in it's more monolithic form. Signed-off-by: John Mulligan --- rbd/encryption_test.go | 88 ++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/rbd/encryption_test.go b/rbd/encryption_test.go index 3bc0661583..b5d9851c97 100644 --- a/rbd/encryption_test.go +++ b/rbd/encryption_test.go @@ -49,13 +49,16 @@ func TestEncryptionFormat(t *testing.T) { func TestEncryptionLoad(t *testing.T) { conn := radosConnect(t) + defer conn.Shutdown() poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) + defer conn.DeletePool(poolname) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) + defer ioctx.Destroy() name := GetUUID() testImageSize := uint64(1 << 23) // format requires more than 4194304 bytes @@ -78,55 +81,58 @@ func TestEncryptionLoad(t *testing.T) { // then write some encrypted data at the end of the image err = img.Close() assert.NoError(t, err) - img, err = OpenImage(ioctx, name, NoSnapshot) - err = img.EncryptionLoad(opts) - assert.NoError(t, err) + defer func() { + assert.NoError(t, img.Remove()) + }() - outData := []byte("Hi rbd! Nice to talk through go-ceph :)") - - stats, err := img.Stat() - require.NoError(t, err) - offset := int64(stats.Size) - int64(len(outData)) - - nOut, err := img.WriteAt(outData, offset) - assert.Equal(t, len(outData), nOut) - assert.NoError(t, err) - - err = img.Close() - assert.NoError(t, err) + testData := []byte("Hi rbd! Nice to talk through go-ceph :)") + var offset int64 - // Re-open the image, load the encryption format, and read the encrypted data - img, err = OpenImage(ioctx, name, NoSnapshot) - assert.NoError(t, err) - err = img.EncryptionLoad(opts) - assert.NoError(t, err) + t.Run("prepare", func(t *testing.T) { + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad(opts) + assert.NoError(t, err) - inData := make([]byte, len(outData)) - nIn, err := img.ReadAt(inData, offset) - assert.Equal(t, nIn, len(inData)) - assert.Equal(t, inData, outData) - assert.NoError(t, err) + stats, err := img.Stat() + require.NoError(t, err) + offset = int64(stats.Size) - int64(len(testData)) - err = img.Close() - assert.NoError(t, err) + nOut, err := img.WriteAt(testData, offset) + assert.Equal(t, len(testData), nOut) + assert.NoError(t, err) + }) - // Re-open the image and attempt to read the encrypted data without loading the encryption - img, err = OpenImage(ioctx, name, NoSnapshot) - assert.NoError(t, err) + t.Run("readEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image, load the encryption format, and read the encrypted data + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad(opts) + assert.NoError(t, err) - nIn, err = img.ReadAt(inData, offset) - assert.Equal(t, nIn, len(inData)) - assert.NotEqual(t, inData, outData) - assert.NoError(t, err) + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(inData)) + assert.Equal(t, inData, testData) + assert.NoError(t, err) + }) - err = img.Close() - assert.NoError(t, err) - err = img.Remove() - assert.NoError(t, err) + t.Run("noEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image and attempt to read the encrypted data without loading the encryption + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() - ioctx.Destroy() - conn.DeletePool(poolname) - conn.Shutdown() + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(inData)) + assert.NotEqual(t, inData, testData) + assert.NoError(t, err) + }) } func TestEncryptedResize(t *testing.T) { From 083f6151feede2a9e2a87566e3aff23a72b851f9 Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 22 Jan 2025 16:37:26 -0500 Subject: [PATCH 3/4] rbd: add EncryptionLoad2 implementing rbd_encryption_load2 Add a new Image method EncryptionLoad2 implementing rbd_encryption_load2. This method adds the ability to have different encryption schemes across parent images. Signed-off-by: John Mulligan Fixes: #1059 --- docs/api-status.json | 6 + docs/api-status.md | 1 + rbd/encryption_load2.go | 67 +++++++++ rbd/encryption_load2_test.go | 261 +++++++++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 rbd/encryption_load2.go create mode 100644 rbd/encryption_load2_test.go diff --git a/docs/api-status.json b/docs/api-status.json index 6e0803296d..959a3977c7 100644 --- a/docs/api-status.json +++ b/docs/api-status.json @@ -1953,6 +1953,12 @@ "comment": "GroupSnapGetInfo returns a slice of RBD image snapshots that are part of a\ngroup snapshot.\n\nImplements:\n\n\tint rbd_group_snap_get_info(rados_ioctx_t group_p,\n\t const char *group_name,\n\t const char *snap_name,\n\t rbd_group_snap_info2_t *snaps);\n", "added_in_version": "v0.30.0", "expected_stable_version": "v0.32.0" + }, + { + "name": "Image.EncryptionLoad2", + "comment": "EncryptionLoad2 enables IO on an open encrypted image. The difference\nbetween EncryptionLoad and EncryptionLoad2 is that EncryptionLoad2 can open\nancestor images with a different encryption options than the current image.\nThe first EncryptionOptions in the slice is applied to the current image,\nthe second to the first ancestor, the third to the second ancestor and so\non. If the length of the slice is smaller than the number of ancestors the\nfinal item in the slice will be applied to all remaining ancestors, or if\nthe ancestor does not match the encryption format the ancestor will be\ninterpreted as plain-text.\n\nImplements:\n\n\tint rbd_encryption_load2(rbd_image_t image,\n\t const rbd_encryption_spec_t *specs,\n\t size_t spec_count);\n", + "added_in_version": "$NEXT_RELEASE", + "expected_stable_version": "$NEXT_RELEASE_STABLE" } ] }, diff --git a/docs/api-status.md b/docs/api-status.md index e2865fadba..4f301e43b2 100644 --- a/docs/api-status.md +++ b/docs/api-status.md @@ -25,6 +25,7 @@ Conn.GetAddrs | v0.31.0 | v0.33.0 | Name | Added in Version | Expected Stable Version | ---- | ---------------- | ----------------------- | GroupSnapGetInfo | v0.30.0 | v0.32.0 | +Image.EncryptionLoad2 | $NEXT_RELEASE | $NEXT_RELEASE_STABLE | ### Deprecated APIs diff --git a/rbd/encryption_load2.go b/rbd/encryption_load2.go new file mode 100644 index 0000000000..1f22ab2e5d --- /dev/null +++ b/rbd/encryption_load2.go @@ -0,0 +1,67 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +// #cgo LDFLAGS: -lrbd +// /* force XSI-complaint strerror_r() */ +// #define _POSIX_C_SOURCE 200112L +// #undef _GNU_SOURCE +// #include +import "C" + +import ( + "unsafe" +) + +// toEncryptionSpec returns a rbd_encryption_spec_t converted from the +// cEncryptionData type. +func (edata cEncryptionData) toEncryptionSpec() C.rbd_encryption_spec_t { + var cSpec C.rbd_encryption_spec_t + cSpec.format = edata.format + cSpec.opts = edata.opts + cSpec.opts_size = edata.optsSize + return cSpec +} + +// EncryptionLoad2 enables IO on an open encrypted image. The difference +// between EncryptionLoad and EncryptionLoad2 is that EncryptionLoad2 can open +// ancestor images with a different encryption options than the current image. +// The first EncryptionOptions in the slice is applied to the current image, +// the second to the first ancestor, the third to the second ancestor and so +// on. If the length of the slice is smaller than the number of ancestors the +// final item in the slice will be applied to all remaining ancestors, or if +// the ancestor does not match the encryption format the ancestor will be +// interpreted as plain-text. +// +// Implements: +// +// int rbd_encryption_load2(rbd_image_t image, +// const rbd_encryption_spec_t *specs, +// size_t spec_count); +func (image *Image) EncryptionLoad2(opts []EncryptionOptions) error { + if image.image == nil { + return ErrImageNotOpen + } + + length := len(opts) + eos := make([]cEncryptionData, length) + cspecs := (*C.rbd_encryption_spec_t)(C.malloc( + C.size_t(C.sizeof_rbd_encryption_spec_t * length))) + specs := unsafe.Slice(cspecs, length) + + for idx, option := range opts { + eos[idx] = option.allocateEncryptionOptions() + specs[idx] = eos[idx].toEncryptionSpec() + } + defer func() { + for _, eopt := range eos { + eopt.free() + } + }() + + ret := C.rbd_encryption_load2( + image.image, + cspecs, + C.size_t(length)) + return getError(ret) +} diff --git a/rbd/encryption_load2_test.go b/rbd/encryption_load2_test.go new file mode 100644 index 0000000000..7a7b07578e --- /dev/null +++ b/rbd/encryption_load2_test.go @@ -0,0 +1,261 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncryptionLoad2(t *testing.T) { + conn := radosConnect(t) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + assert.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + name := GetUUID() + testImageSize := uint64(50) * 1024 * 1024 + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + err = CreateImage(ioctx, name, testImageSize, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + + encOpts := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES256, + Passphrase: []byte("test-password"), + } + err = img.EncryptionFormat(encOpts) + assert.NoError(t, err) + + // close the image so we can reopen it and load the encryption info + // then write some encrypted data at the end of the image + err = img.Close() + assert.NoError(t, err) + defer func() { + assert.NoError(t, img.Remove()) + }() + + testData := []byte("Jinxed wizards pluck ivy from the big quilt") + var offset int64 + + t.Run("prepare", func(t *testing.T) { + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2([]EncryptionOptions{encOpts}) + assert.NoError(t, err) + + stats, err := img.Stat() + require.NoError(t, err) + offset = int64(stats.Size) - int64(len(testData)) + + nOut, err := img.WriteAt(testData, offset) + assert.Equal(t, len(testData), nOut) + assert.NoError(t, err) + }) + + t.Run("readEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image, load the encryption format, and read the encrypted data + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2([]EncryptionOptions{encOpts}) + assert.NoError(t, err) + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(testData)) + assert.Equal(t, inData, testData) + assert.NoError(t, err) + }) + + t.Run("noEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image and attempt to read the encrypted data without loading the encryption + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(testData)) + assert.NotEqual(t, inData, testData) + assert.NoError(t, err) + }) +} + +func TestEncryptionLoad2WithParents(t *testing.T) { + dlength := int64(32) + testData1 := []byte("Very nice object ahead of change") + testData2 := []byte("A nice object encryption applied") + testData3 := []byte("A good object encryption abounds") + testData4 := []byte("Another portion is here and well") + written := [][]byte{} + assert.EqualValues(t, len(testData1), dlength) + assert.EqualValues(t, len(testData2), dlength) + assert.EqualValues(t, len(testData3), dlength) + assert.EqualValues(t, len(testData4), dlength) + + encOpts1 := EncryptionOptionsLUKS1{ + Alg: EncryptionAlgorithmAES128, + Passphrase: []byte("test-password"), + } + encOpts2 := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES128, + Passphrase: []byte("test-password"), + } + encOpts3 := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES256, + Passphrase: []byte("something-stronger"), + } + + conn := radosConnect(t) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + assert.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + name := GetUUID() + testImageSize := uint64(256) * 1024 * 1024 + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + err = CreateImage(ioctx, name, testImageSize, options) + assert.NoError(t, err) + + t.Run("prepare", func(t *testing.T) { + img, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + _, err = img.WriteAt(testData1, 0) + assert.NoError(t, err) + written = append(written, testData1) + }) + + t.Run("createClone1", func(t *testing.T) { + require.Len(t, written, 1) + parent, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn1") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, name, "sn1", ioctx, name+"clone1", options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, name+"clone1", NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts1) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{encOpts1}) + assert.NoError(t, err) + _, err = img.WriteAt(testData2, dlength) + assert.NoError(t, err) + written = append(written, testData2) + }) + + t.Run("createClone2", func(t *testing.T) { + require.Len(t, written, 2) + parentName := name + "clone1" + cloneName := name + "clone2" + + parent, err := OpenImage(ioctx, parentName, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn2") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, parentName, "sn2", ioctx, cloneName, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, cloneName, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts2) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{encOpts2, encOpts1}) + assert.NoError(t, err) + _, err = img.WriteAt(testData3, dlength*2) + assert.NoError(t, err) + written = append(written, testData3) + }) + + t.Run("createClone3", func(t *testing.T) { + require.Len(t, written, 3) + parentName := name + "clone2" + cloneName := name + "clone3" + + parent, err := OpenImage(ioctx, parentName, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn3") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, parentName, "sn3", ioctx, cloneName, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, cloneName, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts3) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{ + encOpts3, encOpts2, encOpts1, + }) + assert.NoError(t, err) + _, err = img.WriteAt(testData4, dlength*3) + assert.NoError(t, err) + written = append(written, testData4) + }) + + t.Run("readAll", func(t *testing.T) { + require.Len(t, written, 4) + img, err := OpenImage(ioctx, name+"clone3", NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + err = img.EncryptionLoad2([]EncryptionOptions{ + encOpts3, encOpts2, encOpts1, + }) + assert.NoError(t, err) + + inData := make([]byte, int(dlength)) + for idx, td := range written { + n, err := img.ReadAt(inData, int64(idx)*dlength) + assert.NoError(t, err) + assert.EqualValues(t, dlength, n) + assert.Equal(t, inData, td) + } + }) +} From bef69a27820ee367891fbbf37839675ad3deb43d Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Fri, 24 Jan 2025 12:59:10 -0500 Subject: [PATCH 4/4] rbd: add EncryptionOptionsLUKS for opening LUKS images Add EncryptionOptionsLUKS type for opening, but not formatting, existing LUKS images generically. Signed-off-by: John Mulligan --- rbd/encryption_opt_luks.go | 34 +++++++++++ rbd/encryption_opt_luks_test.go | 103 ++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 rbd/encryption_opt_luks.go create mode 100644 rbd/encryption_opt_luks_test.go diff --git a/rbd/encryption_opt_luks.go b/rbd/encryption_opt_luks.go new file mode 100644 index 0000000000..32faa6eaa2 --- /dev/null +++ b/rbd/encryption_opt_luks.go @@ -0,0 +1,34 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +// #cgo LDFLAGS: -lrbd +// /* force XSI-complaint strerror_r() */ +// #define _POSIX_C_SOURCE 200112L +// #undef _GNU_SOURCE +// #include +// #include +import "C" + +import ( + "unsafe" +) + +// EncryptionOptionsLUKS generic options for either LUKS v1 or v2. May only be +// used for opening existing images - not valid for the EncryptionFormat call. +type EncryptionOptionsLUKS struct { + Passphrase []byte +} + +func (opts EncryptionOptionsLUKS) allocateEncryptionOptions() cEncryptionData { + var cOpts C.rbd_encryption_luks_format_options_t + var retData cEncryptionData + // CBytes allocates memory. it will be freed when cEncryptionData.free is called + cOpts.passphrase = (*C.char)(C.CBytes(opts.Passphrase)) + cOpts.passphrase_size = C.size_t(len(opts.Passphrase)) + retData.opts = C.rbd_encryption_options_t(&cOpts) + retData.optsSize = C.size_t(C.sizeof_rbd_encryption_luks_format_options_t) + retData.free = func() { C.free(unsafe.Pointer(cOpts.passphrase)) } + retData.format = C.RBD_ENCRYPTION_FORMAT_LUKS + return retData +} diff --git a/rbd/encryption_opt_luks_test.go b/rbd/encryption_opt_luks_test.go new file mode 100644 index 0000000000..10b6d9f10a --- /dev/null +++ b/rbd/encryption_opt_luks_test.go @@ -0,0 +1,103 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncryptionOptionsLUKS(t *testing.T) { + conn := radosConnect(t) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + assert.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + name := GetUUID() + testImageSize := uint64(50) * 1024 * 1024 + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + err = CreateImage(ioctx, name, testImageSize, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + + encOpts := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES256, + Passphrase: []byte("sesame-123-luks-ury"), + } + err = img.EncryptionFormat(encOpts) + assert.NoError(t, err) + + // close the image so we can reopen it and load the encryption info + // then write some encrypted data at the end of the image + err = img.Close() + assert.NoError(t, err) + defer func() { + assert.NoError(t, img.Remove()) + }() + + testData := []byte("Another day another image to unlock") + var offset int64 + + t.Run("prepare", func(t *testing.T) { + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2([]EncryptionOptions{encOpts}) + assert.NoError(t, err) + + stats, err := img.Stat() + require.NoError(t, err) + offset = int64(stats.Size) - int64(len(testData)) + + nOut, err := img.WriteAt(testData, offset) + assert.Equal(t, len(testData), nOut) + assert.NoError(t, err) + }) + + unlock := []EncryptionOptions{ + EncryptionOptionsLUKS{Passphrase: []byte("sesame-123-luks-ury")}, + } + + t.Run("readEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image, using the generic luks encryption options + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2(unlock) + assert.NoError(t, err) + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(testData)) + assert.Equal(t, inData, testData) + assert.NoError(t, err) + }) + + t.Run("noEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image and attempt to read the encrypted data without loading the encryption + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(testData)) + assert.NotEqual(t, inData, testData) + assert.NoError(t, err) + }) +}