Skip to content

Add reencryption - #569

Open
frederic-hoerni wants to merge 13 commits into
masterfrom
feature/luks-reencryption
Open

frederic-hoerni wants to merge 13 commits into
masterfrom
feature/luks-reencryption

Conversation

@frederic-hoerni

@frederic-hoerni frederic-hoerni commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

This adds the feature of reencryption of active LUKS2 containers.

Main changes:

  • API functions:
    • ReencryptionForActiveVolume
    • Initialize
    • Resume
    • Status
  • Named tokens now may have 2 keyslots, which happens during reencryption.
  • Introduction of secboot-tool, that is a tool to test and illustrates high-level functions of secboot (mostly reencryption at the moment).

@frederic-hoerni
frederic-hoerni force-pushed the feature/luks-reencryption branch from d881e43 to 3815939 Compare September 11, 2026 06:37
@frederic-hoerni
frederic-hoerni force-pushed the feature/luks-reencryption branch from 3815939 to 731117d Compare September 17, 2026 08:54
@frederic-hoerni frederic-hoerni changed the title Add reencryption API (MVP) Add reencryption Sep 17, 2026
@tlaurion

Copy link
Copy Markdown

Would be awesome if this go cryptsetup api was in its own repository. u-root and other projects would benefit of this as well

@frederic-hoerni
frederic-hoerni force-pushed the feature/luks-reencryption branch from 0ba1781 to d6ced40 Compare September 22, 2026 16:25
@frederic-hoerni

Copy link
Copy Markdown
Collaborator Author

Would be awesome if this go cryptsetup api was in its own repository

@tlaurion, we have no plan for that at the moment.

@frederic-hoerni
frederic-hoerni force-pushed the feature/luks-reencryption branch from 81a124a to a95d46c Compare September 23, 2026 08:32
@frederic-hoerni
frederic-hoerni marked this pull request as ready for review September 24, 2026 06:34
@frederic-hoerni frederic-hoerni self-assigned this Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It lacks tests about Unmarshalling RecoveryToken and KeyDataToken with:

  • 2 keyslots (nominal)
  • 0 or 3 keyslots (error)

"--resume-only",
"--progress-frequency", "1",
"--progress-json",
"--hotzone-size", "10M",

@frederic-hoerni frederic-hoerni Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain how/why this option and size "10M" are chosen.
How much does it impact performance? Does it have any other side effects?

Comment thread luks2/backend.go
}

// NewOnlineReencryption implements [secboot.StorageContainerBackend.NewOnlineReencryption].
func (b *storageContainerBackend) NewOnlineReencryption(activeName string) (secboot.Reencryption, error) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this detect if the underlying cryptsetup supports reencryption?
Eg: DetectCryptsetupFeatures()&FeatureReencrypt

.. and return (nil, nil) if it does not?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning (nil, nil) would make an unsupported cryptsetup version look like an unrecognized backend

Comment thread keydata_luks.go Outdated
Comment on lines +72 to +73
if len(token.Keyslots()) != 1 {
return nil, fmt.Errorf("token has %v keyslot(s)", len(token.Keyslots()))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for multiple keyslots is needed, for example for the "plainkey" mechanism.

Error seen after reencryption was interrupted:

./secboot-tool activate $DEVICE crypt02 30303030 -v
Error with keyslot "default": invalid key data: cannot read keyslot: cannot obtain reader for "default": token has 2 keyslot(s)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@frederic-hoerni

Copy link
Copy Markdown
Collaborator Author

Please rebase on top of master (and expect conflicts).

@ruifm ruifm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great!

Comment on lines +580 to +590
func ReencryptInitialize(ctx context.Context, activeName string, unlockKeys [][]byte) error {
log.Debugf("ReencryptInitialize: unlockKeys=%v", unlockKeys)
sizes := []string{}
// Prepare concatenated keys
var allKeys []byte
for _, unlockKey := range unlockKeys {
sizes = append(sizes, strconv.Itoa(len(unlockKey)))
allKeys = append(allKeys, unlockKey...)
}
log.Debugf("ReencryptInitialize: sizes=%v", strings.Join(sizes, ","))
log.Debugf("ReencryptInitialize: keys=%v", allKeys)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this write every unlock key to stderr when debug logging is enabled?

Comment thread luks2/reencrypt.go
Comment on lines +145 to +153
func (r reencryptionImpl) Resume(ctx context.Context, unlockKey []byte) (<-chan secboot.ReencryptionProgressEvent, error) {
outChan := make(chan secboot.ReencryptionProgressEvent)

cmd, stdout, stderr, err := luks2.ReencryptResume(ctx, r.dmActiveName, unlockKey)
if err != nil {
return nil, err
}

go superviseReencryption(cmd, stdout, stderr, outChan)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deactivation will select the first matching device. The existing lookup assumes one mapping per container. With this layout, it selects data-hotzone-forward instead of data`.

Maybe we should either change that or pass the active name directly?

Comment thread reencrypt.go
Status() (*ReencryptionStatus, error)
Initialize(ctx context.Context, unlockKeys map[string][]byte) error
Resume(ctx context.Context, unlockKey []byte) (<-chan ReencryptionProgressEvent, error)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also require ActiveName()

Comment thread luks2/luks2_test.go
Comment on lines +144 to +157
func (v *mockLuksView) TokenNamesSortedByKeyslotId() ([]string, error) {
if v.data == nil {
return nil, errors.New("error while getting token names")
}

names := []string{}
for name, _ := range v.data.recoveryKeyslots {
names = append(names, name)
}
for name, _ := range v.data.platformKeyslots {
names = append(names, name)
}

return names, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock is returning names in map order but the tests expect a fixed order. This can lead to flaky tests, we should not allow this sort of indeterminism

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread luks2/reencrypt.go
Comment on lines +111 to +120
details, err := decodeReencryptionProgressDetails(rawBytes)
if err != nil {
outChan <- secboot.ReencryptionProgressEvent{Type: secboot.ReencryptionProgressRunning, Error: err}
} else {
outChan <- secboot.ReencryptionProgressEvent{Type: secboot.ReencryptionProgressRunning, Details: details}
}
}
err := scanner.Err()
if err != nil {
outChan <- secboot.ReencryptionProgressEvent{Type: secboot.ReencryptionProgressRunning, Error: err}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress sends can block after a caller cancels and stops reading. Can this be tested? (just a hypothetical but a risk nonetheless)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is up to the caller to make sure that they read up to the end.
I will add documentation about it.

Comment thread luks2/reencrypt.go
Comment on lines +62 to +66
tokenNames, err := view.TokenNamesSortedByKeyslotId()
log.Debugf("Initialize: tokenNames: %v", tokenNames)

if len(tokenNames) == 0 {
return fmt.Errorf("cannot get any token name")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return err here otherwise we are hiding the true error

Comment thread luks2/reader.go Outdated
ks.keyslotId = token.Keyslots()[0]
} else {
// Two keyslots associated with this token (happens when reencryption is in progress)
ks.keyslotId = internal_luks2.AnySlot

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this break the detection done above?

if ks.keyslotId != internal_luks2.AnySlot {
    // We already have everything for this keyslot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reworked and fixed

//
// activeName: name of the active device mapper volume
// unlockKeys: all unlock keys (passphrases) of the LUK2 container, in the order of the keyslots
func ReencryptInitialize(ctx context.Context, activeName string, unlockKeys [][]byte) error {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this require StorageContainer.OpenRead() and StorageContainer.Activate() to acquire a shared lock, so that they do not get their cached data outdated by ReencryptInitialize()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done for StorageContainer.OpenRead()
No need identified for StorageContainer.Activate()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants