From 20924ca06efba5a50356bdb5abb1f7b87f34f817 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 2 Mar 2017 11:22:43 -0800 Subject: [PATCH] crypto: Key struct for secure buffers This commit adds in the crypto package, which will hold all of the security primitives for fscrypt. This first component deals with securely handling keys in memory. To do this in a consistent way across fscrypt, we introduce the Key struct. Any sensitive memory (like keys, passwords, or recovery tokens) in fscrypt will be held in a Key. No code outside of the crypto package should access the Key's data directly. Convenience functions and methods are provided to construct keys from io.Readers (either with fixed length or with variable length) and to access information about the Keys. The most important property of Keys is that the data is locked in memory on construction, and the data is unlocked and wiped when Wipe is called. This happens either by something like "defer key.Wipe()" or through the finalizer. Change-Id: Ice76335f3975efb439b3f1ab605ef34cb7fcb4d6 --- crypto/crypto_test.go | 113 +++++++++++++++++++++++ crypto/key.go | 203 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 crypto/crypto_test.go create mode 100644 crypto/key.go diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go new file mode 100644 index 0000000..d76381e --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,113 @@ +/* + * crypto_test.go - tests for the crypto package + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package crypto + +import ( + "bytes" + "os" + "testing" +) + +// Reader that always returns the same byte +type ConstReader byte + +func (r ConstReader) Read(b []byte) (n int, err error) { + for i := range b { + b[i] = byte(r) + } + return len(b), nil +} + +// Makes a key of the same repeating byte +func makeKey(b byte, n int) (*Key, error) { + return NewFixedLengthKeyFromReader(ConstReader(b), n) +} + +// Tests the two ways of making keys +func TestMakeKeys(t *testing.T) { + data := []byte("1234\n6789") + + key1, err := NewKeyFromReader(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(data, key1.data) { + t.Error("Key from reader contained incorrect data") + } + + key2, err := NewFixedLengthKeyFromReader(bytes.NewReader(data), 6) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal([]byte("1234\n6"), key2.data) { + t.Error("Fixed length key from reader contained incorrect data") + } +} + +// Tests that wipe succeeds +func TestWipe(t *testing.T) { + key, err := makeKey(1, 1000) + if err != nil { + t.Fatal(err) + } + if err := key.Wipe(); err != nil { + t.Error(err) + } +} + +// Making keys with negative length should fail +func TestInvalidLength(t *testing.T) { + _, err := NewFixedLengthKeyFromReader(bytes.NewReader([]byte{1, 2, 3, 4}), -1) + if err == nil { + t.Error("Negative lengths should cause failure") + } +} + +// Test making keys of zero length +func TestZeroLength(t *testing.T) { + key1, err := NewFixedLengthKeyFromReader(os.Stdin, 0) + if err != nil { + t.Fatal(err) + } + if key1.data != nil { + t.Error("FIxed length key from reader contained data") + } + + key2, err := NewKeyFromReader(bytes.NewReader(nil)) + if err != nil { + t.Fatal(err) + } + if key2.data != nil { + t.Error("Key from empty reader contained data") + } +} + +// Test making keys long enough that the keys will have to resize +func TestLongLength(t *testing.T) { + // Key will have to resize 3 times + data := bytes.Repeat([]byte{1}, os.Getpagesize()*5) + key, err := NewKeyFromReader(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(data, key.data) { + t.Error("Key contained incorrect data") + } +} diff --git a/crypto/key.go b/crypto/key.go new file mode 100644 index 0000000..88b5a2c --- /dev/null +++ b/crypto/key.go @@ -0,0 +1,203 @@ +/* + * key.go - Cryptographic key management for fscrypt. Ensures that sensitive + * material is properly handled throughout the program. + * + * Copyright 2017 Google Inc. + * Author: Joe Richey (joerichey@google.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package crypto + +import ( + "io" + "os" + "runtime" + + "golang.org/x/sys/unix" + + "fscrypt/util" +) + +/* +UseMlock determines whether we should use the mlock/munlock syscalls to +prevent sensitive data like keys and passphrases from being paged to disk. +UseMlock defaults to true, but can be set to false if the application calling +into this library has insufficient privileges to lock memory. A package could +also bind this setting to a flag by using: + + flag.BoolVar(&crypto.UseMlock, "lock-memory", true, "lock keys in memory") +*/ +var UseMlock = true + +/* +Key protects some arbitrary buffer of cryptographic material. Its methods +ensure that the Key's data is locked in memory before being used (if +UseMlock is set to true), and is wiped and unlocked after use (via the Wipe() +method). This data is never accessed outside of the fscrypt/crypto package +(except for the UnsafeData method). If a key is successfully created, the +Wipe() method should be called after it's use. For example: + + func UseKeyFromStdin() error { + key, err := NewKeyFromReader(os.Stdin) + if err != nil { + return err + } + defer key.Wipe() + + // Do stuff with key + + return nil + } + +The Wipe() method will also be called when a key is garbage collected; however, +it is best practice to clear the key as soon as possible, so it spends a minimal +amount of time in memory. + +Note that Key is not thread safe, as a key could be wiped while another thread +is using it. Also, calling Wipe() from two threads could cause an error as +memory could be freed twice. +*/ +type Key struct { + data []byte +} + +const ( + // Keys need to readable and writable, but hidden from other processes. + keyProtection = unix.PROT_READ | unix.PROT_WRITE + keyMmapFlags = unix.MAP_PRIVATE | unix.MAP_ANONYMOUS +) + +// newBlankKey constructs a blank key of a specified length and returns an error +// if we are unable to allocate or lock the necessary memory. +func newBlankKey(length int) (*Key, error) { + if length == 0 { + return &Key{data: nil}, nil + } else if length < 0 { + return nil, util.InvalidInputF("requested key length %d is negative", length) + } + + flags := keyMmapFlags + if UseMlock { + flags |= unix.MAP_LOCKED + } + + // See MAP_ANONYMOUS in http://man7.org/linux/man-pages/man2/mmap.2.html + data, err := unix.Mmap(-1, 0, length, keyProtection, flags) + if err != nil { + return nil, util.SystemErrorF("could not mmap() buffer: %v", err) + } + + key := &Key{data: data} + + // Backup finalizer in case user forgets to "defer key.Wipe()" + runtime.SetFinalizer(key, (*Key).Wipe) + return key, nil +} + +// Wipe destroys a Key by zeroing and freeing the memory. The data is zeroed +// even if Wipe returns an error, which occurs if we are unable to unlock or +// free the key memory. Calling Wipe() multiple times on a key has no effect. +func (key *Key) Wipe() error { + if key.data != nil { + data := key.data + key.data = nil + + for i := range data { + data[i] = 0 + } + + if err := unix.Munmap(data); err != nil { + return util.SystemErrorF("could not munmap() buffer: %v", err) + } + } + return nil +} + +// Len is the underlying data buffer's length. +func (key *Key) Len() int { + return len(key.data) +} + +// UnsafeData exposes the underlying protected slice. This is unsafe because the +// data can be paged to disk if the buffer is copied, or the slice may be +// wiped while being used. +func (key *Key) UnsafeData() []byte { + return key.data +} + +// resize returns a new key with size requestedSize and the appropriate data +// copied over. The original data is wiped. This method does nothing and returns +// itself if the key's length equals requestedSize. +func (key *Key) resize(requestedSize int) (*Key, error) { + if key.Len() == requestedSize { + return key, nil + } + defer key.Wipe() + + resizedKey, err := newBlankKey(requestedSize) + if err != nil { + return nil, err + } + copy(resizedKey.data, key.data) + return resizedKey, nil +} + +// NewKeyFromReader constructs a key of abritary length by reading from reader +// until hitting EOF. +func NewKeyFromReader(reader io.Reader) (*Key, error) { + // Use an initial key size of a page. As Mmap allocates a page anyway, + // there isn't much additional overhead from starting with a whole page. + key, err := newBlankKey(os.Getpagesize()) + if err != nil { + return nil, err + } + + totalBytesRead := 0 + for { + bytesRead, err := reader.Read(key.data[totalBytesRead:]) + totalBytesRead += bytesRead + + switch err { + case nil: + // Need to continue reading. Grow key if necessary + if key.Len() == totalBytesRead { + if key, err = key.resize(2 * key.Len()); err != nil { + return nil, err + } + } + case io.EOF: + // Getting the EOF error means we are done + return key.resize(totalBytesRead) + default: + // Fail if Read() has a failure + key.Wipe() + return nil, err + } + } +} + +// NewFixedLengthKeyFromReader constructs a key with a specified length by +// reading exactly length bytes from reader. +func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) { + key, err := newBlankKey(length) + if err != nil { + return nil, err + } + if _, err := io.ReadFull(reader, key.data); err != nil { + key.Wipe() + return nil, err + } + return key, nil +} -- 2.39.5