forked from canonical/secboot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcrypt.go
executable file
·919 lines (790 loc) · 33.2 KB
/
crypt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2019-2022 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package secboot
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strconv"
"github.com/snapcore/snapd/asserts"
"golang.org/x/xerrors"
"github.com/snapcore/secboot/internal/keyring"
"github.com/snapcore/secboot/internal/luks2"
"github.com/snapcore/secboot/internal/luksview"
)
var (
// ErrMissingCryptsetupFeature is returned from some functions that make
// use of the system's cryptsetup binary, if that binary is missing some
// required features.
ErrMissingCryptsetupFeature = luks2.ErrMissingCryptsetupFeature
luks2Activate = luks2.Activate
luks2AddKey = luks2.AddKey
luks2Deactivate = luks2.Deactivate
luks2Format = luks2.Format
luks2ImportToken = luks2.ImportToken
luks2KillSlot = luks2.KillSlot
luks2RemoveToken = luks2.RemoveToken
luks2SetSlotPriority = luks2.SetSlotPriority
newLUKSView = luksview.NewView
osStderr io.Writer = os.Stderr
)
const (
defaultKeyslotName = "default"
defaultRecoveryKeyslotName = "default-recovery"
)
// RecoveryKey corresponds to a 16-byte recovery key in its binary form.
type RecoveryKey [16]byte
func (k RecoveryKey) String() string {
var u16 [8]uint16
for i := 0; i < 8; i++ {
u16[i] = binary.LittleEndian.Uint16(k[i*2:])
}
return fmt.Sprintf("%05d-%05d-%05d-%05d-%05d-%05d-%05d-%05d", u16[0], u16[1], u16[2], u16[3], u16[4], u16[5], u16[6], u16[7])
}
// ParseRecoveryKey interprets the supplied string and returns the corresponding RecoveryKey. The recovery key is a
// 16-byte number, and the formatted version of this is represented as 8 5-digit zero-extended base-10 numbers (each
// with a range of 00000-65535) which may be separated by an optional '-', eg:
//
// "61665-00531-54469-09783-47273-19035-40077-28287"
//
// The formatted version of the recovery key is designed to be able to be inputted on a numeric keypad.
func ParseRecoveryKey(s string) (out RecoveryKey, err error) {
for i := 0; i < 8; i++ {
if len(s) < 5 {
return RecoveryKey{}, errors.New("incorrectly formatted: insufficient characters")
}
x, err := strconv.ParseUint(s[0:5], 10, 16)
if err != nil {
return RecoveryKey{}, xerrors.Errorf("incorrectly formatted: %w", err)
}
binary.LittleEndian.PutUint16(out[i*2:], uint16(x))
// Move to the next 5 digits
s = s[5:]
// Permit each set of 5 digits to be separated by an optional '-', but don't allow the formatted key to end or begin with one.
if len(s) > 1 && s[0] == '-' {
s = s[1:]
}
}
if len(s) > 0 {
return RecoveryKey{}, errors.New("incorrectly formatted: too many characters")
}
return
}
type activateWithKeyDataError struct {
k *KeyData
err error
}
func (e *activateWithKeyDataError) Error() string {
return fmt.Sprintf("%s: %v", e.k.ReadableName(), e.err)
}
func (e *activateWithKeyDataError) Unwrap() error {
return e.err
}
type keyCandidate struct {
*KeyData
slot int
err error
}
type activateWithKeyDataState struct {
volumeName string
sourceDevicePath string
model SnapModel
keyringPrefix string
authRequestor AuthRequestor
kdf KDF
passphraseTries int
keys []*keyCandidate
}
func (s *activateWithKeyDataState) errors() (out []*activateWithKeyDataError) {
for _, k := range s.keys {
if k.err == nil {
continue
}
out = append(out, &activateWithKeyDataError{k: k.KeyData, err: k.err})
}
return out
}
func (s *activateWithKeyDataState) tryActivateWithRecoveredKey(key DiskUnlockKey, slot int, keyData *KeyData, auxKey PrimaryKey) error {
model := s.model
// Snap model checking is skipped for generation 2 keys regardless of the model argument.
// Although a gen 1 key could fake the generation field which is unprotected to also
// bypass the model version check, that will result in an umarshalling error later on.
switch keyData.Generation() {
case 1:
if model == nil {
return errors.New("nil Model for generation 1 key")
}
default:
// Model authorization checking is skipped for version 2 keys and
// up as it is now responsibility of the platform to verify the model.
model = SkipSnapModelCheck
}
if model != SkipSnapModelCheck {
authorized, err := keyData.IsSnapModelAuthorized(auxKey, model)
switch {
case err != nil:
return xerrors.Errorf("cannot check if snap model is authorized: %w", err)
case !authorized:
return errors.New("snap model is not authorized")
}
}
if err := luks2Activate(s.volumeName, s.sourceDevicePath, key, slot); err != nil {
return xerrors.Errorf("cannot activate volume: %w", err)
}
if err := keyring.AddKeyToUserKeyring(key, s.sourceDevicePath, keyringPurposeDiskUnlock, s.keyringPrefix); err != nil {
fmt.Fprintf(os.Stderr, "secboot: Cannot add key to user keyring: %v\n", err)
}
if err := keyring.AddKeyToUserKeyring(auxKey, s.sourceDevicePath, keyringPurposeAuxiliary, s.keyringPrefix); err != nil {
fmt.Fprintf(os.Stderr, "secboot: Cannot add key to user keyring: %v\n", err)
}
return nil
}
func (s *activateWithKeyDataState) tryKeyDataAuthModeNone(k *KeyData, slot int) error {
key, auxKey, err := k.RecoverKeys()
if err != nil {
return xerrors.Errorf("cannot recover key: %w", err)
}
return s.tryActivateWithRecoveredKey(key, slot, k, auxKey)
}
func (s *activateWithKeyDataState) tryKeyDataAuthModePassphrase(k *KeyData, slot int, passphrase string) error {
key, auxKey, err := k.RecoverKeysWithPassphrase(passphrase, s.kdf)
if err != nil {
return xerrors.Errorf("cannot recover key: %w", err)
}
return s.tryActivateWithRecoveredKey(key, slot, k, auxKey)
}
func (s *activateWithKeyDataState) run() (success bool, err error) {
numPassphraseKeys := 0
// Try keys that don't require any additional authentication first
for _, k := range s.keys {
if k.AuthMode()&AuthModePassphrase > 0 {
numPassphraseKeys += 1
}
if k.AuthMode() != AuthModeNone {
continue
}
if err := s.tryKeyDataAuthModeNone(k.KeyData, k.slot); err != nil {
k.err = err
continue
}
return true, nil
}
// Try keys that require a passphrase
tries := s.passphraseTries
var passphraseErr error
for tries > 0 && numPassphraseKeys > 0 {
tries -= 1
// Request a passphrase first and then try each key with it. One downside of
// this approach is that if there are multiple keys with different passphrases
// or the passphrase is wrong for all keys, this will accelerate the rate at
// which dictionary attack protections kick in for platforms that support that.
// This shouldn't be an issue for standard configurations where there would be
// a maximum of 2 keys with passphrases enabled (Ubuntu Core based desktop on
// a UEFI+TPM platform with run+recovery and recovery-only protectors for
// ubuntu-data).
passphrase, err := s.authRequestor.RequestPassphrase(s.volumeName, s.sourceDevicePath)
if err != nil {
passphraseErr = xerrors.Errorf("cannot obtain passphrase: %w", err)
continue
}
for _, k := range s.keys {
if k.AuthMode()&AuthModePassphrase == 0 {
continue
}
if k.err != nil && !xerrors.Is(k.err, ErrInvalidPassphrase) {
// Skip keys that failed for anything other than an invalid passphrase.
continue
}
if err := s.tryKeyDataAuthModePassphrase(k.KeyData, k.slot, passphrase); err != nil {
if !xerrors.Is(err, ErrInvalidPassphrase) {
numPassphraseKeys -= 1
}
k.err = err
continue
}
return true, nil
}
}
// We've failed at this point
return false, passphraseErr
}
func newActivateWithKeyDataState(volumeName, sourceDevicePath string, keyringPrefix string, model SnapModel, keys []*keyCandidate, authRequestor AuthRequestor, kdf KDF, passphraseTries int) *activateWithKeyDataState {
return &activateWithKeyDataState{
volumeName: volumeName,
sourceDevicePath: sourceDevicePath,
keyringPrefix: keyringPrefixOrDefault(keyringPrefix),
model: model,
authRequestor: authRequestor,
kdf: kdf,
passphraseTries: passphraseTries,
keys: keys}
}
func activateWithRecoveryKey(volumeName, sourceDevicePath string, authRequestor AuthRequestor, tries int, keyringPrefix string) error {
if tries == 0 {
return errors.New("no recovery key tries permitted")
}
var lastErr error
for ; tries > 0; tries-- {
lastErr = nil
key, err := authRequestor.RequestRecoveryKey(volumeName, sourceDevicePath)
if err != nil {
lastErr = xerrors.Errorf("cannot obtain recovery key: %w", err)
continue
}
if err := luks2Activate(volumeName, sourceDevicePath, key[:], luks2.AnySlot); err != nil {
lastErr = xerrors.Errorf("cannot activate volume: %w", err)
continue
}
if err := keyring.AddKeyToUserKeyring(key[:], sourceDevicePath, keyringPurposeDiskUnlock, keyringPrefixOrDefault(keyringPrefix)); err != nil {
fmt.Fprintf(os.Stderr, "secboot: Cannot add key to user keyring: %v\n", err)
}
break
}
return lastErr
}
type nullSnapModel struct{}
func (_ nullSnapModel) Series() string { return "" }
func (_ nullSnapModel) BrandID() string { return "" }
func (_ nullSnapModel) Model() string { return "" }
func (_ nullSnapModel) Classic() bool { return false }
func (_ nullSnapModel) Grade() asserts.ModelGrade { return "" }
func (_ nullSnapModel) SignKeyID() string { return "" }
// SkipSnapModelCheck provides a mechanism to skip the snap device model
// check when calling ActivateVolumeWithKeyData.
var SkipSnapModelCheck SnapModel = nullSnapModel{}
// ActivateVolumeOptions provides options to the ActivateVolumeWith*
// family of functions.
type ActivateVolumeOptions struct {
// PassphraseTries specifies the maximum number of times
// that activation with a user passphrase should be attempted
// before failing with an error and falling back to activating
// with the recovery key (see RecoveryKeyTries).
//
// Setting this to zero disables activation with a user
// passphrase - in this case, any protected keys that require
// a passphrase are ignored and activation will fall back to
// requesting a recovery key.
//
// For each passphrase attempt, the supplied passphrase is
// tested against every protected key that requires a passphrase.
//
// The actual number of available passphrase attempts may be
// limited by the platform to a number that is lower than this
// value (eg, in the TPM case because of the current auth fail
// counter value which means the dictionary attack protection
// might be triggered first).
//
// It is ignored by ActivateVolumeWithRecoveryKey.
PassphraseTries int
// RecoveryKeyTries specifies the maximum number of times that
// activation with the fallback recovery key should be
// attempted.
//
// It is used directly by ActivateVolumeWithRecoveryKey and
// indirectly with other methods upon failure, for example
// in the case where no other keys can be recovered.
//
// Setting this to zero will disable attempts to activate with
// the fallback recovery key.
RecoveryKeyTries int
// KeyringPrefix is the prefix used for the description of any
// kernel keys created during activation.
KeyringPrefix string
// Model is the snap device model that will access the data
// on the encrypted container. The ActivateVolumeWithKeyData
// function will check that this model is authorized via the KeyData
// binding before unlocking the encrypted container.
//
// The caller of the ActivateVolumeWithKeyData API is responsible
// for validating the associated model assertion and snaps.
//
// Set this to SkipSnapModelCheck to skip the check. It cannot
// be left set as nil when calling ActivateVolumeWithKeyData.
//
// It is ignored by ActivateVolumeWithRecoveryKey, and it is
// ok to leave it set as nil in this case.
Model SnapModel
}
type activateVolumeWithKeyDataError struct {
keyDataErrs []error
recoveryKeyUsageErr error
}
func (e *activateVolumeWithKeyDataError) Error() string {
var s bytes.Buffer
fmt.Fprintf(&s, "cannot activate with platform protected keys:")
for _, err := range e.keyDataErrs {
fmt.Fprintf(&s, "\n- %v", err)
}
fmt.Fprintf(&s, "\nand activation with recovery key failed: %v", e.recoveryKeyUsageErr)
return s.String()
}
// ErrRecoveryKeyUsed is returned from ActivateVolumeWithKeyData if the
// volume could not be activated with any platform protected keys but
// activation with the recovery key was successful.
var ErrRecoveryKeyUsed = errors.New("cannot activate with platform protected keys but activation with the recovery key was successful")
// ActivateVolumeWithKeyData attempts to activate the LUKS encrypted container at
// sourceDevicePath and create a mapping with the name volumeName, using one of
// the KeyData objects stored in the container's metadata area to recover the
// disk unlock key from the platform's secure device. This makes use of
// systemd-cryptsetup.
//
// External KeyData objects can be supplied via the keys argument, and these
// will be attempted first.
//
// If activation with all of the KeyData objects fails, this function will
// attempt to activate it with the fallback recovery key instead. The fallback
// recovery key is requested via the supplied authRequestor. If an AuthRequestor
// is not supplied, an error will be returned if the fallback recovery key is
// required. The RecoveryKeyTries field of options specifies how many attemps to
// request and use the recovery key will be made before failing. If it is set to
// 0, then no attempts will be made to request and use the fallback recovery key.
//
// If either the PassphraseTries or RecoveryKeyTries fields of options are less
// than zero, an error will be returned. If the Model field of options is nil,
// an error will be returned.
//
// If the fallback recovery key is used for successfully for activation, an
// ErrRecoveryKeyUsed error will be returned.
//
// If activation fails, an error will be returned.
//
// If activation with one of the KeyData objects succeeds (ie, no error is
// returned), then the supplied SnapModel is authorized to access the data on
// this volume.
func ActivateVolumeWithKeyData(volumeName, sourceDevicePath string, authRequestor AuthRequestor, kdf KDF, options *ActivateVolumeOptions, keys ...*KeyData) error {
if options.PassphraseTries < 0 {
return errors.New("invalid PassphraseTries")
}
if options.RecoveryKeyTries < 0 {
return errors.New("invalid RecoveryKeyTries")
}
if (options.PassphraseTries > 0 || options.RecoveryKeyTries > 0) && authRequestor == nil {
return errors.New("nil authRequestor")
}
if options.PassphraseTries > 0 && kdf == nil {
return errors.New("nil kdf")
}
var candidates []*keyCandidate
for _, key := range keys {
candidates = append(candidates, &keyCandidate{KeyData: key, slot: luks2.AnySlot})
}
view, err := newLUKSView(sourceDevicePath, luks2.LockModeBlocking)
if err != nil {
fmt.Fprintf(osStderr, "secboot: cannot obtain LUKS2 header view: %v\n", err)
} else {
tokens := view.KeyDataTokensByPriority()
for _, token := range tokens {
if token.Data == nil {
// Skip uninitialized token
continue
}
r := &LUKS2KeyDataReader{
name: sourceDevicePath + ":" + token.Name(),
Reader: bytes.NewReader(token.Data)}
kd, err := ReadKeyData(r)
if err != nil {
fmt.Fprintf(osStderr, "secboot: cannot read keydata from token %s: %v\n", token.Name(), err)
continue
}
candidates = append(candidates, &keyCandidate{KeyData: kd, slot: token.Keyslots()[0]})
}
}
s := newActivateWithKeyDataState(volumeName, sourceDevicePath, options.KeyringPrefix, options.Model, candidates, authRequestor, kdf, options.PassphraseTries)
success, err := s.run()
switch {
case success:
return nil
default: // failed - try recovery key
if rErr := activateWithRecoveryKey(volumeName, sourceDevicePath, authRequestor, options.RecoveryKeyTries, options.KeyringPrefix); rErr != nil {
// failed with recovery key - return errors
var kdErrs []error
for _, e := range s.errors() {
kdErrs = append(kdErrs, e)
}
if err != nil {
kdErrs = append(kdErrs, err)
}
return &activateVolumeWithKeyDataError{kdErrs, rErr}
}
// succeeded with recovery key
return ErrRecoveryKeyUsed
}
}
// ActivateVolumeWithRecoveryKey attempts to activate the LUKS encrypted volume at
// sourceDevicePath and create a mapping with the name volumeName, using the fallback
// recovery key. This makes use of systemd-cryptsetup.
//
// The recovery key is requested via the supplied AuthRequestor. If an AuthRequestor
// is not supplied, an error will be returned. The RecoveryKeyTries field of options
// specifies how many attempts to request and use the recovery key will be made before
// failing.
//
// If the RecoveryKeyTries field of options is less than zero, an error will be
// returned.
func ActivateVolumeWithRecoveryKey(volumeName, sourceDevicePath string, authRequestor AuthRequestor, options *ActivateVolumeOptions) error {
if authRequestor == nil {
return errors.New("nil authRequestor")
}
if options.RecoveryKeyTries < 0 {
return errors.New("invalid RecoveryKeyTries")
}
return activateWithRecoveryKey(volumeName, sourceDevicePath, authRequestor, options.RecoveryKeyTries, options.KeyringPrefix)
}
// ActivateVolumeWithKey attempts to activate the LUKS encrypted volume at
// sourceDevicePath and create a mapping with the name volumeName, using the
// provided key. This makes use of systemd-cryptsetup.
func ActivateVolumeWithKey(volumeName, sourceDevicePath string, key []byte, options *ActivateVolumeOptions) error {
return luks2Activate(volumeName, sourceDevicePath, key, luks2.AnySlot)
}
// DeactivateVolume attempts to deactivate the LUKS encrypted volumeName.
// This makes use of systemd-cryptsetup.
func DeactivateVolume(volumeName string) error {
return luks2Deactivate(volumeName)
}
// InitializeLUKS2ContainerOptions carries options for initializing LUKS2
// containers.
type InitializeLUKS2ContainerOptions struct {
// MetadataKiBSize sets the size of the metadata area in KiB. This
//
// MetadataKiBSize sets the size of the metadata area in KiB. 4KiB of
// this is used for the fixed-size binary header, with the remaining
// space being used for the JSON area. Setting this to zero causes
// the container to be initialized with the default metadata area size.
// If set to a non zero value, it must be a power of 2 between 16KiB
// and 4MiB.
MetadataKiBSize int
// KeyslotsAreaKiBSize sets the size of the binary keyslot area in KiB.
// Setting this to zero causes the container to be initialized with
// the default keyslots area size. If set to a non-zero value, the
// value must be a multiple of 4KiB up to a maximum of 128MiB.
KeyslotsAreaKiBSize int
// KDFOptions sets the KDF options for the initial keyslot. If this
// is nil then the default settings defined by this package are used
// (4 iterations and a memory cost of 32KiB).
KDFOptions *KDFOptions
// InitialKeyslotName sets the name that will be used to identify
// the initial keyslot. If this is empty, then the name will be
// set to "default".
InitialKeyslotName string
// InlineCryptoEngine set flag if to use Inline Crypto Engine
InlineCryptoEngine bool
}
func (o *InitializeLUKS2ContainerOptions) formatOpts() *luks2.FormatOptions {
return &luks2.FormatOptions{
MetadataKiBSize: o.MetadataKiBSize,
KeyslotsAreaKiBSize: o.KeyslotsAreaKiBSize,
KDFOptions: o.KDFOptions.luksOpts(),
InlineCryptoEngine: o.InlineCryptoEngine}
}
// InitializeLUKS2Container will initialize the partition at the specified devicePath
// as a new LUKS2 container. This can only be called on a partition that isn't mapped.
// The label for the new LUKS2 container is provided via the label argument.
//
// The container will be configured to encrypt data with AES-256 and XTS block cipher
// mode.
//
// The initial key used for unlocking the container is provided via the key argument,
// and must be a cryptographically secure random number of at least 32-bytes.
//
// The initial keyslot will be created with the name specified in the
// InitialKeyslotName field of options. If this is empty, "default" will be used.
//
// The initial key should be protected by some platform-specific mechanism in order
// to create a KeyData object. The KeyData object can be saved to the
// keyslot using LUKS2KeyDataWriter.
//
// On failure, this will return an error containing the output of the cryptsetup command.
//
// WARNING: This function is destructive. Calling this on an existing LUKS container
// will make the data contained inside of it irretrievable.
func InitializeLUKS2Container(devicePath, label string, key DiskUnlockKey, options *InitializeLUKS2ContainerOptions) error {
if len(key) < 32 {
return fmt.Errorf("expected a key length of at least 256-bits (got %d)", len(key)*8)
}
// Use a reduced cost for the KDF. This is done because we have a high entropy key rather
// than a low entropy passphrase. Setting a higher cost provides no security benefit but
// does slow down unlocking. If an adversary is going to attempt to brute force this key,
// then they could instead turn their attention to one of the other keys involved in the
// protection of this key, some of which can be verified without running a KDF. For
// example, with a TPM sealed object, you can verify the parent storage key's seed by
// computing the key object's HMAC key and verifying the integrity value on the outer wrapper.
if options == nil {
var defaultOptions InitializeLUKS2ContainerOptions
options = &defaultOptions
} else {
// copy options to avoid modification of the supplied struct
options = &InitializeLUKS2ContainerOptions{
MetadataKiBSize: options.MetadataKiBSize,
KeyslotsAreaKiBSize: options.KeyslotsAreaKiBSize,
KDFOptions: options.KDFOptions,
InitialKeyslotName: options.InitialKeyslotName,
InlineCryptoEngine: options.InlineCryptoEngine}
}
if options.KDFOptions == nil {
options.KDFOptions = &KDFOptions{MemoryKiB: 32, ForceIterations: 4}
}
initialKeyslotName := options.InitialKeyslotName
if initialKeyslotName == "" {
initialKeyslotName = defaultKeyslotName
}
if err := luks2Format(devicePath, label, key, options.formatOpts()); err != nil {
return xerrors.Errorf("cannot format: %w", err)
}
token := luksview.KeyDataToken{
TokenBase: luksview.TokenBase{
TokenKeyslot: 0,
TokenName: initialKeyslotName}}
if err := luks2ImportToken(devicePath, &token, nil); err != nil {
return xerrors.Errorf("cannot import token: %w", err)
}
if err := luks2SetSlotPriority(devicePath, 0, luks2.SlotPriorityHigh); err != nil {
return xerrors.Errorf("cannot change keyslot priority: %w", err)
}
return nil
}
func removeOrphanedTokens(devicePath string, view *luksview.View) {
for _, id := range view.OrphanedTokenIds() {
luks2RemoveToken(devicePath, id)
}
}
func addLUKS2ContainerKey(devicePath, keyslotName string, existingKey, newKey DiskUnlockKey, options *KDFOptions,
newToken func(base *luksview.TokenBase) luks2.Token, priority luks2.SlotPriority) error {
view, err := newLUKSView(devicePath, luks2.LockModeBlocking)
if err != nil {
return xerrors.Errorf("cannot obtain LUKS header view: %w", err)
}
if _, _, exists := view.TokenByName(keyslotName); exists {
return errors.New("the specified name is already in use")
}
removeOrphanedTokens(devicePath, view)
freeSlot := 0
for _, slot := range view.UsedKeyslots() {
if slot != freeSlot {
break
}
freeSlot++
}
if err := luks2AddKey(devicePath, existingKey, newKey, &luks2.AddKeyOptions{KDFOptions: options.luksOpts(), Slot: freeSlot}); err != nil {
return xerrors.Errorf("cannot add key: %w", err)
}
// XXX: If we fail between AddKey and ImportToken, then we end up with a
// used keyslot that cannot be identified and no way to roll back the
// interrupted transaction safely. Ideally we'd be able to add a key and
// token in a single atomic operation, but this isn't even something that
// is possible with the libcryptsetup API.
//
// I have an idea for how to make this more resilient and avoid ending up
// in this state in the event of an interruption, but it adds a bit more
// complexity and is for a future PR, as it's a bit of an edge case. But
// it's something like this:
// - Select an unused keyslot ID.
// - Add a transient token associated with an existing keyslot (there will
// always be at least one in this context. A token has to be associated
// with an active slot at import time). The new token will reference the
// selected unused keyslot ID in a new field.
// - Create the keyslot at the new keyslot ID.
// - Import the proper token associated with the new keyslot.
// - Delete the transient token.
//
// This should ensure we can always roll back an interrupted operation to
// add a new key (or complete it if we've imported the proper token). It's
// not fully atomic (no transaction consisting of multiple cryptsetup
// operations is), but that's ok - on Ubuntu Core, all changes to the
// LUKS container should go through secboot. If we have multiple processes
// that could make changes (eg, snapd and a hypothetical fdectl or something),
// then we can add some locking to serialize transactions.
//
// Or, we could propose an API to libcrypsetup and the corresponding changes
// to cryptsetup instead to support adding a keyslot with an initial token in
// a single atomic transaction ¯\_(ツ)_/¯
tokenBase := luksview.TokenBase{
TokenName: keyslotName,
TokenKeyslot: freeSlot}
if err := luks2ImportToken(devicePath, newToken(&tokenBase), nil); err != nil {
return xerrors.Errorf("cannot import token: %w", err)
}
if err := luks2SetSlotPriority(devicePath, freeSlot, priority); err != nil {
return xerrors.Errorf("cannot change keyslot priority: %w", err)
}
return nil
}
func listLUKS2ContainerKeyNames(devicePath string, tokenType luks2.TokenType) ([]string, error) {
view, err := newLUKSView(devicePath, luks2.LockModeBlocking)
if err != nil {
return nil, xerrors.Errorf("cannot obtain LUKS header view: %w", err)
}
var names []string
for _, name := range view.TokenNames() {
token, _, _ := view.TokenByName(name)
if token.Type() != tokenType {
continue
}
names = append(names, name)
}
return names, nil
}
// AddLUKS2ContainerUnlockKey creates a keyslot with the specified name on
// the LUKS2 container at the specified path, and uses it to protect the master
// key with the supplied key. The created keyslot is one that will normally be
// used for unlocking the specified LUKS2 container.
//
// If the specified name is empty, the name "default" will be used.
//
// The new key should be a cryptographically strong random number of at least
// 32-bytes.
//
// If a keyslot with the supplied name already exists, an error will be returned.
// The keyslot must first be deleted with DeleteLUKS2ContainerKey or renamed
// with RenameLUKS2ContainerKey.
//
// In order to perform this action, an existing key must be supplied.
//
// The new key should be protected by some platform-specific mechanism in
// order to create a KeyData object. The KeyData object can be saved to the
// keyslot using LUKS2KeyDataWriter.
func AddLUKS2ContainerUnlockKey(devicePath, keyslotName string, existingKey, newKey DiskUnlockKey, options *KDFOptions) error {
if len(newKey) < 32 {
return fmt.Errorf("expected a key length of at least 256-bits (got %d)", len(newKey)*8)
}
if keyslotName == "" {
keyslotName = defaultKeyslotName
}
// Use a reduced cost for the KDF. This is done because we have a high entropy key rather
// than a low entropy passphrase. Setting a higher cost provides no security benefit but
// does slow down unlocking. If an adversary is going to attempt to brute force this key,
// then they could instead turn their attention to one of the other keys involved in the
// protection of this key, some of which can be verified without running a KDF. For
// example, with a TPM sealed object, you can verify the parent storage key's seed by
// computing the key object's HMAC key and verifying the integrity value on the outer wrapper.
if options == nil {
options = &KDFOptions{MemoryKiB: 32, ForceIterations: 4}
}
return addLUKS2ContainerKey(devicePath, keyslotName, existingKey, newKey, options, func(base *luksview.TokenBase) luks2.Token {
return &luksview.KeyDataToken{TokenBase: *base}
}, luks2.SlotPriorityHigh)
}
// ListLUKS2ContainerUnlockKeyNames lists the names of keyslots on the specified
// LUKS2 container configured as normal unlock slots (the keys associated with
// these should be protected by the platform's secure device).
func ListLUKS2ContainerUnlockKeyNames(devicePath string) ([]string, error) {
return listLUKS2ContainerKeyNames(devicePath, luksview.KeyDataTokenType)
}
// AddLUKS2ContainerRecoveryKey creates a fallback recovery keyslot with the
// specified name on the LUKS2 container at the specified path and uses it to
// protect the LUKS master key with the supplied recovery key. The keyslot can
// be used to unlock the container in scenarios where it cannot be unlocked
// using a platform protected key.
//
// If the specified name is empty, the name "default-recovery" will be used.
//
// The recovery key must be generated by a cryptographically strong random
// number source.
//
// If a keyslot with the supplied name already exists, an error will be returned.
// The keyslot must first be deleted with DeleteLUKS2ContainerKey or renamed
// with RenameLUKS2ContainerKey.
//
// In order to perform this action, an existing key must be supplied.
func AddLUKS2ContainerRecoveryKey(devicePath, keyslotName string, existingKey DiskUnlockKey, recoveryKey RecoveryKey, options *KDFOptions) error {
if keyslotName == "" {
keyslotName = defaultRecoveryKeyslotName
}
if options == nil {
options = &KDFOptions{}
}
return addLUKS2ContainerKey(devicePath, keyslotName, existingKey, recoveryKey[:], options, func(base *luksview.TokenBase) luks2.Token {
return &luksview.RecoveryToken{TokenBase: *base}
}, luks2.SlotPriorityNormal)
}
// ListLUKS2ContainerRecoveryKeyNames lists the names of keyslots on the specified
// LUKS2 container configured as recovery slots.
func ListLUKS2ContainerRecoveryKeyNames(devicePath string) ([]string, error) {
return listLUKS2ContainerKeyNames(devicePath, luksview.RecoveryTokenType)
}
// DeleteLUKS2ContainerKey deletes the keyslot with the specified name from the
// LUKS2 container at the specified path. This will return an error if the container
// only has a single keyslot remaining.
func DeleteLUKS2ContainerKey(devicePath, keyslotName string) error {
view, err := newLUKSView(devicePath, luks2.LockModeBlocking)
if err != nil {
return xerrors.Errorf("cannot obtain LUKS header view: %w", err)
}
token, id, exists := view.TokenByName(keyslotName)
if !exists {
return errors.New("no key with the specified name exists")
}
if len(view.TokenNames()) == 1 {
// This is stricter than not permitting the deletion of the last keyslot
// - it intentionally does not permit deleting the last secboot named
// keyslot, even if the container has other keyslots that might have
// been created outside of this package.
return errors.New("cannot kill last remaining slot")
}
removeOrphanedTokens(devicePath, view)
slot := token.Keyslots()[0]
if err := luks2KillSlot(devicePath, slot); err != nil {
return xerrors.Errorf("cannot kill existing slot %d: %w", slot, err)
}
// KillSlot will clear the keyslot field from the associated token so
// that we can identify it as orphaned and complete the transaction in
// the future if we are interrupted between KillSlot and RemoveToken.
if err := luks2RemoveToken(devicePath, id); err != nil {
return xerrors.Errorf("cannot remove existing token %d: %w", id, err)
}
return nil
}
// RenameLUKS2Container key renames the keyslot with the specified oldName on
// the LUKS2 container at the specified path.
func RenameLUKS2ContainerKey(devicePath, oldName, newName string) error {
view, err := newLUKSView(devicePath, luks2.LockModeBlocking)
if err != nil {
return xerrors.Errorf("cannot obtain LUKS header view: %w", err)
}
removeOrphanedTokens(devicePath, view)
token, id, exists := view.TokenByName(oldName)
if !exists {
return errors.New("no key with the specified name exists")
}
if _, _, exists := view.TokenByName(newName); exists {
return errors.New("the new name is already in use")
}
var newToken luks2.Token
switch t := token.(type) {
case *luksview.KeyDataToken:
newToken = &luksview.KeyDataToken{
TokenBase: luksview.TokenBase{
TokenKeyslot: t.TokenKeyslot,
TokenName: newName},
Priority: t.Priority,
Data: t.Data}
case *luksview.RecoveryToken:
newToken = &luksview.RecoveryToken{
TokenBase: luksview.TokenBase{
TokenKeyslot: t.TokenKeyslot,
TokenName: newName}}
default:
return errors.New("cannot rename key with unexpected token type")
}
if err := luks2ImportToken(devicePath, newToken, &luks2.ImportTokenOptions{Id: id, Replace: true}); err != nil {
return xerrors.Errorf("cannot import new token: %w", err)
}
return nil
}