diff --git a/.gitignore b/.gitignore index 514e2dcf..2f8d222b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ cmd/micromdm/micromdm tools/ngrok/config_root tools/ngrok/filerepo tools/ngrok/env +mdm-files/ diff --git a/dep/activation_lock.go b/dep/activation_lock.go new file mode 100644 index 00000000..b42ff3a0 --- /dev/null +++ b/dep/activation_lock.go @@ -0,0 +1,34 @@ +package dep + +import "github.com/pkg/errors" + +const ( + activationLockPath = "device/activationlock" +) + +type ActivationLockRequest struct { + Device string `json:"device"` + + // If the escrow key is not provided, the device will be locked to the person who created the MDM server in the portal. + // https://developer.apple.com/documentation/devicemanagement/device_assignment/activation_lock_a_device/creating_and_using_bypass_codes + // The EscrowKey is a hex-encoded PBKDF2 derivation of the bypass code. See activationlock.BypassCode. + EscrowKey string `json:"escrow_key"` + + LostMessage string `json:"lost_message"` +} + +type ActivationLockResponse struct { + SerialNumber string `json:"serial_number"` + Status string `json:"response_status"` +} + +func (c *Client) ActivationLock(alr ActivationLockRequest) (*ActivationLockResponse, error) { + req, err := c.newRequest("POST", activationLockPath, &alr) + if err != nil { + return nil, errors.Wrap(err, "create activation lock request") + } + + var response ActivationLockResponse + err = c.do(req, &response) + return &response, errors.Wrap(err, "activation lock") +} diff --git a/pkg/activationlock/activationlock.go b/pkg/activationlock/activationlock.go new file mode 100644 index 00000000..18c128ef --- /dev/null +++ b/pkg/activationlock/activationlock.go @@ -0,0 +1,107 @@ +// Package activationlock supports Apple device activation locking and unlocking. +// https://developer.apple.com/documentation/devicemanagement/activation_lock_a_device +package activationlock + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +// The available character set for a bypass code string. +const charset = "0123456789ACDEFGHJKLMNPQRTUVWXYZ" + +var ( + // Positions to insert "-" in the bypass code string. + dashPositions = []int{5, 10, 14, 18, 22} + + // The salt for the hash is always the same. + salt = []uint8{0, 0, 0, 0} +) + +// BypassCode is the ActivationLock code. +type BypassCode struct { + Key [16]byte + + format string +} + +// Create generates a usable bypass code. +func Create(key []byte) (BypassCode, error) { + var code BypassCode + + if key == nil { + key = make([]byte, 16) + if _, err := rand.Read(key); err != nil { + return code, err + } + } + + copy(code.Key[:], key) + + // Format human readable version of key. + values, err := convertBits(key, 8, 5) + if err != nil { + return code, err + } + + dashIdx := 0 + var str strings.Builder + for i, p := range values { + if dashIdx < len(dashPositions) && i == dashPositions[dashIdx] { + str.WriteString("-") + dashIdx++ + } + str.WriteByte(charset[p]) + } + code.format = str.String() + + return code, nil +} + +// Hash returns a PBKKDF2 derived hash of the bypass code. +// The hex encoded string is sent to Apple to lock a device. +func (c BypassCode) Hash() string { + return hex.EncodeToString(pbkdf2.Key(c.Key[:], salt, 50000, sha256.Size, sha256.New)) +} + +// String returns the bypass code in human readable format. +func (c BypassCode) String() string { + return c.format +} + +// convert binary data from one bits-per-byte arrangement to another. +// Ex: re-arrange 8 bit bytes to groups of 5 when converting to base32. +// This is a modified helper from a Go implementation of the bech32 format. +// https://github.com/FiloSottile/age/blob/c9a35c072716b5ac6cd815366999c9e189b0c317/internal/bech32/bech32.go#L79-L105 +func convertBits(data []byte, frombits, tobits byte) ([]byte, error) { + var ret []byte + acc := uint32(0) + bits := byte(0) + maxv := byte(1<>frombits != 0 { + return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits) + } + acc = acc<= tobits { + bits -= tobits + ret = append(ret, byte(acc>>bits)&maxv) + } + } + + // zero out most significant bits of the last value, until we get to remaining bits + if bits > 0 { + for bit := frombits; bit >= bits; bit-- { + acc = acc &^ (1 << bit) + } + ret = append(ret, byte(acc)&maxv) + } + + return ret, nil +} diff --git a/pkg/activationlock/activationlock_test.go b/pkg/activationlock/activationlock_test.go new file mode 100644 index 00000000..a940faba --- /dev/null +++ b/pkg/activationlock/activationlock_test.go @@ -0,0 +1,51 @@ +package activationlock + +import ( + "encoding/hex" + "testing" +) + +func TestBypassCode(t *testing.T) { + tests := []struct { + key string + humanReadable string + hash string + }{ + { + key: "1ea841db5edfafe6075b5ae0d845d254", + humanReadable: "3UM43-PUYVY-QYD1-UVCC-HEHJ-FKA4", + hash: "6ab40d5eabe7218ec04182f461005600c7e3426bddd82cdb405bde9a1e0014b5", + }, + { + key: "44ebe63375969fec2da67e87e7317946", + humanReadable: "8LNYD-DVNKU-GYRC-E6GU-3YFD-CT86", + hash: "c1968cb4c013ea893f1922bb5c39f81e35012c0bd9ce3c01cc2a05873a2499e6", + }, + { + key: "cb84798c3ca85a674194550a2e96aed8", + humanReadable: "TF27L-31WN1-E6FH-DMAM-52X5-NFV0", + hash: "23cf8b7873425fd8efe31dc5b6ab9c357eb98a2a59c82ea1084ca8af58cc480a", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.key, func(t *testing.T) { + t.Parallel() + + key, _ := hex.DecodeString(tt.key) + code, err := Create(key) + if err != nil { + t.Fatal(err) + } + + if got, want := code.Hash(), tt.hash; got != want { + t.Errorf("Hash(): got %q, want %q", got, want) + } + + if got, want := code.String(), tt.humanReadable; got != want { + t.Errorf("Human Readable String: got %q, want %q", got, want) + } + }) + } +}