diff --git a/orbit/changes/46457-tpm-backed-luks-recovery-key-prompt b/orbit/changes/46457-tpm-backed-luks-recovery-key-prompt deleted file mode 100644 index 6db5f1fee4..0000000000 --- a/orbit/changes/46457-tpm-backed-luks-recovery-key-prompt +++ /dev/null @@ -1 +0,0 @@ -- Updated the Fleet Desktop disk encryption prompt on TPM-backed Linux hosts (e.g., Ubuntu 23.10+) to ask for the disk recovery key saved during installation, instead of a LUKS passphrase. diff --git a/orbit/pkg/luks/luks.go b/orbit/pkg/luks/luks.go index 6e0a365b30..1b8cf78e11 100644 --- a/orbit/pkg/luks/luks.go +++ b/orbit/pkg/luks/luks.go @@ -9,7 +9,6 @@ import ( type LuksDump struct { Keyslots map[string]Keyslot `json:"keyslots"` // keyslot -> salt - Tokens map[string]Token `json:"tokens"` } type Keyslot struct { @@ -20,62 +19,6 @@ type KDF struct { Salt string `json:"salt"` } -// Token represents an entry from the LUKS2 "tokens" object. We only need the -// type field to identify TPM2/FIDO2/recovery setups; other fields are ignored. -type Token struct { - Type string `json:"type"` -} - -// EncryptionType values returned by DetectEncryptionType. These are local to -// orbit — the server does not currently persist or care about which one the -// host is on; the value is only used to branch dialog copy when prompting the -// end user during escrow. -const ( - EncryptionTypePassphrase = "passphrase" - EncryptionTypeTPM2 = "tpm2" - EncryptionTypeFIDO2 = "fido2" - EncryptionTypeRecovery = "recovery" -) - -// LUKS2 token-type identifiers emitted by systemd-cryptenroll. These match -// the literal "type" field of entries in the luksDump "tokens" object. -const ( - systemdTPM2Type = "systemd-tpm2" - systemdFIDO2Type = "systemd-fido2" - systemdRecoveryType = "systemd-recovery" -) - -// DetectEncryptionType inspects a LUKS2 dump's tokens and returns the -// best-matching encryption type for the volume. Priority order when multiple -// tokens are present is tpm2 > fido2 > recovery > passphrase. A nil dump, an -// empty tokens map, or unrecognized token types all map to -// EncryptionTypePassphrase. -func DetectEncryptionType(dump *LuksDump) string { - if dump == nil { - return EncryptionTypePassphrase - } - - var hasFIDO2, hasRecovery bool - for _, tok := range dump.Tokens { - switch tok.Type { - case systemdTPM2Type: - return EncryptionTypeTPM2 - case systemdFIDO2Type: - hasFIDO2 = true - case systemdRecoveryType: - hasRecovery = true - } - } - - switch { - case hasFIDO2: - return EncryptionTypeFIDO2 - case hasRecovery: - return EncryptionTypeRecovery - } - return EncryptionTypePassphrase -} - type KeyEscrower interface { SendLinuxKeyEscrowResponse(LuksResponse) error } diff --git a/orbit/pkg/luks/luks_linux.go b/orbit/pkg/luks/luks_linux.go index 4f1e362799..8848dcd148 100644 --- a/orbit/pkg/luks/luks_linux.go +++ b/orbit/pkg/luks/luks_linux.go @@ -31,21 +31,12 @@ const ( entryDialogTitle = "Enter disk encryption passphrase" entryDialogText = "Passphrase:" retryEntryDialogText = "Passphrase incorrect. Please try again." - - // Dialog copy shown when the LUKS2 volume is sealed with a TPM2 token - // (e.g. Ubuntu 23.10+ TPM-backed FDE). In that setup the user only has - // the recovery key shown during installation; there is no per-user - // passphrase to ask for. - entryDialogTitleTPM2 = "Enter disk recovery key" - entryDialogTextTPM2 = "Recovery key (saved during installation):" - retryEntryDialogTextTPM2 = "Recovery key incorrect. Please try again." - - infoTitle = "Disk encryption" - infoFailedText = "Failed to escrow key. Please try again later." - infoSuccessText = "Disk encryption key escrowed to Fleet. Close this window, navigate to your Fleet My Device page, and select Refetch to clear the yellow banner." - timeoutMessage = "Please visit Fleet Desktop > My device and click Create key" - maxKeySlots = 8 - userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase + infoTitle = "Disk encryption" + infoFailedText = "Failed to escrow key. Please try again later." + infoSuccessText = "Disk encryption key escrowed to Fleet. Close this window, navigate to your Fleet My Device page, and select Refetch to clear the yellow banner." + timeoutMessage = "Please visit Fleet Desktop > My device and click Create key" + maxKeySlots = 8 + userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase ) var ErrKeySlotFull = regexp.MustCompile(`Key slot \d+ is full`) @@ -143,20 +134,8 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by // AESXTSPlain64Cipher is the default cipher used by ubuntu/kubuntu/fedora device := luksdevice.New(luksdevice.AESXTSPlain64Cipher) - // Inspect LUKS2 metadata once up front so we can branch the dialog copy - // for TPM-backed volumes — on those, the user has a recovery key from - // install time, not a typed passphrase. - dump, err := GetLuksDump(ctx, devicePath) - if err != nil { - return nil, nil, fmt.Errorf("inspecting LUKS metadata: %w", err) - } - encType := DetectEncryptionType(dump) - log.Debug().Str("encryption_type", encType).Msg("detected LUKS encryption type") - - title, prompt, retry := dialogCopyForEncryptionType(encType) - - // Prompt user for existing LUKS passphrase / recovery key - passphrase, err := lr.entryPrompt(title, prompt) + // Prompt user for existing LUKS passphrase + passphrase, err := lr.entryPrompt(entryDialogTitle, entryDialogText) if err != nil { return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err) } @@ -178,7 +157,7 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by break } - passphrase, err = lr.entryPrompt(title, retry) + passphrase, err = lr.entryPrompt(entryDialogTitle, retryEntryDialogText) if err != nil { return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err) } @@ -223,18 +202,6 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by return escrowPassphrase, &keySlot, nil } -// dialogCopyForEncryptionType returns the (title, prompt, retry) strings shown -// to the end user when asking them to unlock the LUKS volume. TPM2-backed and -// recovery-key setups use distinct copy today; passphrase / fido2 share the -// default passphrase wording since they accept a typed secret from the user's -// perspective. -func dialogCopyForEncryptionType(encType string) (title, prompt, retry string) { - if encType == EncryptionTypeTPM2 || encType == EncryptionTypeRecovery { - return entryDialogTitleTPM2, entryDialogTextTPM2, retryEntryDialogTextTPM2 - } - return entryDialogTitle, entryDialogText, retryEntryDialogText -} - func (lr *LuksRunner) passphraseIsValid(ctx context.Context, device *luksdevice.LUKS, devicePath string, passphrase []byte, keyslot uint) (bool, error) { if len(passphrase) == 0 { return false, nil diff --git a/orbit/pkg/luks/luks_test.go b/orbit/pkg/luks/luks_test.go index b2816409e0..9a6bc35328 100644 --- a/orbit/pkg/luks/luks_test.go +++ b/orbit/pkg/luks/luks_test.go @@ -1,11 +1,9 @@ package luks import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/tj/assert" ) // output from cryptsetup luksDump /dev/sda3 --debug-json command @@ -208,145 +206,10 @@ var extractedJSON = `{ }` func TestExtractJson(t *testing.T) { - extracted, err := extractJSON([]byte(output)) + json, err := extractJSON([]byte(output)) assert.NoError(t, err) - assert.JSONEq(t, extractedJSON, string(extracted)) + assert.Equal(t, extractedJSON, string(json)) _, err = extractJSON([]byte("no json")) assert.Error(t, err) } - -// tpm2AndRecoveryDumpJSON is a LUKS2 luksDump JSON fragment that includes a -// systemd-tpm2 entry alongside a recovery entry, matching what -// systemd-cryptenroll writes on a TPM-backed Ubuntu install. Used to assert -// that the Tokens field of LuksDump parses correctly. -const tpm2AndRecoveryDumpJSON = `{ - "keyslots":{ - "0":{ - "type":"luks2", - "kdf":{"type":"argon2id","salt":"abc"} - }, - "1":{ - "type":"luks2", - "kdf":{"type":"argon2id","salt":"def"} - } - }, - "tokens":{ - "0":{ - "type":"systemd-tpm2", - "keyslots":["1"], - "tpm2-blob":"blob", - "tpm2-pcrs":[7] - }, - "1":{ - "type":"systemd-recovery", - "keyslots":["0"] - } - }, - "segments":{}, - "digests":{}, - "config":{} -}` - -func TestLuksDumpTokensUnmarshal(t *testing.T) { - t.Run("cryptsetup <2.4 debug output has empty tokens", func(t *testing.T) { - raw, err := extractJSON([]byte(output)) - require.NoError(t, err) - - var dump LuksDump - require.NoError(t, json.Unmarshal(raw, &dump)) - assert.Empty(t, dump.Tokens) - assert.NotEmpty(t, dump.Keyslots, "keyslots should still parse") - }) - - t.Run("tpm2 + recovery tokens parse with type field", func(t *testing.T) { - var dump LuksDump - require.NoError(t, json.Unmarshal([]byte(tpm2AndRecoveryDumpJSON), &dump)) - require.Len(t, dump.Tokens, 2) - assert.Equal(t, systemdTPM2Type, dump.Tokens["0"].Type) - assert.Equal(t, systemdRecoveryType, dump.Tokens["1"].Type) - }) -} - -func TestDetectEncryptionType(t *testing.T) { - cases := []struct { - name string - dump *LuksDump - want string - }{ - { - name: "nil dump", - dump: nil, - want: EncryptionTypePassphrase, - }, - { - name: "no tokens map (nil)", - dump: &LuksDump{}, - want: EncryptionTypePassphrase, - }, - { - name: "empty tokens map", - dump: &LuksDump{Tokens: map[string]Token{}}, - want: EncryptionTypePassphrase, - }, - { - name: "only tpm2", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdTPM2Type}, - }}, - want: EncryptionTypeTPM2, - }, - { - name: "only fido2", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdFIDO2Type}, - }}, - want: EncryptionTypeFIDO2, - }, - { - name: "only recovery", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdRecoveryType}, - }}, - want: EncryptionTypeRecovery, - }, - { - name: "tpm2 + recovery -> tpm2", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdTPM2Type}, - "1": {Type: systemdRecoveryType}, - }}, - want: EncryptionTypeTPM2, - }, - { - name: "fido2 + recovery -> fido2", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdFIDO2Type}, - "1": {Type: systemdRecoveryType}, - }}, - want: EncryptionTypeFIDO2, - }, - { - name: "tpm2 + fido2 + recovery -> tpm2", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: systemdRecoveryType}, - "1": {Type: systemdFIDO2Type}, - "2": {Type: systemdTPM2Type}, - }}, - want: EncryptionTypeTPM2, - }, - { - name: "unknown token type -> passphrase", - dump: &LuksDump{Tokens: map[string]Token{ - "0": {Type: "some-future-thing"}, - }}, - want: EncryptionTypePassphrase, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.want, DetectEncryptionType(tc.dump)) - }) - } -}