diff options
Diffstat (limited to 'crypto')
| -rw-r--r-- | crypto/crypto.go | 94 | ||||
| -rw-r--r-- | crypto/crypto_test.go | 148 |
2 files changed, 233 insertions, 9 deletions
diff --git a/crypto/crypto.go b/crypto/crypto.go index 5eeff50..d11dce2 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -21,14 +21,23 @@ // - Key management (key.go) // - Securely holding keys in memory // - Inserting keys into the keyring +// - Making recovery keys // - Randomness (rand.go) // - Cryptographic algorithms (crypto.go) // - encryption (AES256-CTR) // - authentication (SHA256-based HMAC) // - key stretching (SHA256-based HKDF) // - key wrapping/unwrapping (Encrypt then MAC) +// - passphrase-based key derivation (Argon2id) package crypto +/* +#cgo LDFLAGS: -largon2 +#include <stdlib.h> // malloc(), free() +#include <argon2.h> +*/ +import "C" + import ( "crypto/aes" "crypto/cipher" @@ -36,6 +45,7 @@ import ( "crypto/sha256" "fmt" "io" + "unsafe" "golang.org/x/crypto/hkdf" "golang.org/x/sys/unix" @@ -54,7 +64,7 @@ const ( PolicyKeyLen = unix.FS_MAX_KEY_SIZE ) -// "name" has invalid length if expected != actual +// checkInputLength panics if "name" has invalid length (expected != actual) func checkInputLength(name string, expected, actual int) { if expected != actual { util.NeverError(util.InvalidLengthError(name, expected, actual)) @@ -80,9 +90,9 @@ func stretchKey(key *Key) (encKey, authKey *Key) { return } -// Runs AES256-CTR on the input using the provided key and iv. This function can -// be used to either encrypt or decrypt input of any size. Note that input and -// output must be the same size. +// aesCTR runs AES256-CTR on the input using the provided key and iv. This +// function can be used to either encrypt or decrypt input of any size. Note +// that input and output must be the same size. func aesCTR(key *Key, iv, input, output []byte) { checkInputLength("aesCTR key", InternalKeyLen, key.Len()) checkInputLength("aesCTR iv", IVLen, len(iv)) @@ -95,7 +105,7 @@ func aesCTR(key *Key, iv, input, output []byte) { stream.XORKeyStream(output, input) } -// Get a HMAC (with a SHA256-based hash) of some data using the provided key. +// getHMAC returns the SHA256-based HMAC of some data using the provided key. func getHMAC(key *Key, data ...[]byte) []byte { checkInputLength("hmac key", InternalKeyLen, key.Len()) @@ -166,3 +176,77 @@ func Unwrap(wrappingKey *Key, data *metadata.WrappedKeyData) (*Key, error) { return secretKey, nil } + +// newArgon2Context creates an argon2_context C struct given the hash and +// passphrase keys, salt and costs. The structure must be freed by the caller. +func newArgon2Context(hash, passphrase *Key, + salt []byte, costs *metadata.HashingCosts) *C.argon2_context { + + ctx := (*C.argon2_context)(C.malloc(C.sizeof_argon2_context)) + + ctx.out = (*C.uint8_t)(util.Ptr(hash.data)) + ctx.outlen = C.uint32_t(hash.Len()) + + ctx.pwd = (*C.uint8_t)(util.Ptr(passphrase.data)) + ctx.pwdlen = C.uint32_t(passphrase.Len()) + + ctx.salt = (*C.uint8_t)(util.Ptr(salt)) + ctx.saltlen = C.uint32_t(len(salt)) + + ctx.secret = nil // We don't use the secret field. + ctx.secretlen = 0 + ctx.ad = nil // We don't use the associated data field. + ctx.adlen = 0 + + ctx.t_cost = C.uint32_t(costs.Time) + ctx.m_cost = C.uint32_t(costs.Memory) + ctx.lanes = C.uint32_t(costs.Parallelism) + + ctx.threads = ctx.lanes + ctx.version = C.ARGON2_VERSION_13 + + // We use the built in malloc/free for memory. + ctx.allocate_cbk = nil + ctx.free_cbk = nil + ctx.flags = C.ARGON2_FLAG_CLEAR_PASSWORD + + return ctx +} + +/* +PassphraseHash uses Argon2id to produce a Key given the passphrase, salt, and +hashing costs. This method is designed to take a long time and consume +considerable memory. On success, passphrase will no longer have valid data. +However, the caller should still call passphrase.Wipe(). + +Argon2 is the winning algorithm of the Password Hashing Competition +(see: https://password-hashing.net). It is designed to be "memory hard" +in that a large amount of memory is required to compute the hash value. +This makes it hard to use specialized hardware like GPUs and ASICs. We +use it in "id" mode to provide extra protection against side-channel +attacks. For more info see: https://github.com/P-H-C/phc-winner-argon2 +*/ +func PassphraseHash(passphrase *Key, salt []byte, costs *metadata.HashingCosts) (*Key, error) { + if len(salt) != SaltLen { + return nil, util.InvalidLengthError("salt", SaltLen, len(salt)) + } + + // This key will hold the hashing output + hash, err := newBlankKey(InternalKeyLen) + if err != nil { + return nil, err + } + + ctx := newArgon2Context(hash, passphrase, salt, costs) + defer C.free(unsafe.Pointer(ctx)) + + // Run the hashing function (translating the error if there is one) + returnCode := C.argon2id_ctx(ctx) + if returnCode != C.ARGON2_OK { + hash.Wipe() + errorString := C.GoString(C.argon2_error_message(returnCode)) + return nil, util.SystemErrorF("argon2: %s", errorString) + } + + return hash, nil +} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index fe5edf1..471d3ed 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -24,11 +24,12 @@ import ( "compress/zlib" "crypto/aes" "crypto/sha256" + "encoding/hex" "fmt" - "fscrypt/metadata" - "fscrypt/util" "os" "testing" + + . "fscrypt/metadata" ) // Reader that always returns the same byte @@ -48,15 +49,51 @@ func makeKey(b byte, n int) (*Key, error) { var fakeValidDescriptor = "0123456789abcdef" var fakeInvalidDescriptor = "123456789abcdef" +var fakeSalt = bytes.Repeat([]byte{'a'}, SaltLen) +var fakePassword = []byte("password") var fakeValidPolicyKey, _ = makeKey(42, PolicyKeyLen) var fakeInvalidPolicyKey, _ = makeKey(42, PolicyKeyLen-1) var fakeWrappingKey, _ = makeKey(17, InternalKeyLen) +// As the passpharase hashing function clears the passphrase, we need to make +// a new passphrase key for each test +func fakePassphraseKey() (*Key, error) { + return NewFixedLengthKeyFromReader(bytes.NewReader(fakePassword), len(fakePassword)) +} + +// Values for test cases pulled from argon2 command line tool. +// To generate run: +// echo "password" | argon2 "aaaaaaaaaaaaaaaa" -id -t <t> -m <m> -p <p> -l 32 +// where costs.Time = <t>, costs.Memory = 2^<m>, and costs.Parallelism = <p>. +type hashTestCase struct { + costs *HashingCosts + hexHash string +} + +var hashTestCases = []hashTestCase{ + { + costs: &HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1}, + hexHash: "a66f5398e33761bf161fdf1273e99b148f07d88d12d85b7673fddd723f95ec34", + }, + { + costs: &HashingCosts{Time: 10, Memory: 1 << 10, Parallelism: 1}, + hexHash: "5fa2cb89db1f7413ba1776258b7c8ee8c377d122078d28fe1fd645c353787f50", + }, + { + costs: &HashingCosts{Time: 1, Memory: 1 << 15, Parallelism: 1}, + hexHash: "f474a213ed14d16ead619568000939b938ddfbd2ac4a82d253afa81b5ebaef84", + }, + { + costs: &HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 10}, + hexHash: "b7c3d7a0be222680b5ea3af3fb1a0b7b02b92cbd7007821dc8b84800c86c7783", + }, +} + // Checks that len(array) == expected func lengthCheck(name string, array []byte, expected int) error { if len(array) != expected { - return util.InvalidLengthError(name, expected, len(array)) + return fmt.Errorf("length of %s should be %d", name, expected) } return nil } @@ -320,7 +357,7 @@ func TestWrapTwiceDistinct(t *testing.T) { } // Attempts to Unwrap data with key after altering tweek, should fail -func testFailWithTweek(key *Key, data *metadata.WrappedKeyData, tweek []byte) error { +func testFailWithTweek(key *Key, data *WrappedKeyData, tweek []byte) error { tweek[0]++ _, err := Unwrap(key, data) tweek[0]-- @@ -354,6 +391,69 @@ func TestUnwrapWrongData(t *testing.T) { } } +// Run our test cases for passphrase hashing +func TestPassphraseHashing(t *testing.T) { + for i, testCase := range hashTestCases { + pk, err := fakePassphraseKey() + if err != nil { + t.Fatal(err) + } + defer pk.Wipe() + + hash, err := PassphraseHash(pk, fakeSalt, testCase.costs) + if err != nil { + t.Fatal(err) + } + defer hash.Wipe() + + actual := hex.EncodeToString(hash.data) + if actual != testCase.hexHash { + t.Errorf("Hash test %d: for costs=%+v expected hash of %q got %q", + i, testCase.costs, testCase.hexHash, actual) + } + } +} + +func TestBadTime(t *testing.T) { + pk, err := fakePassphraseKey() + if err != nil { + t.Fatal(err) + } + costs := *hashTestCases[0].costs + costs.Time = 0 + _, err = PassphraseHash(pk, fakeSalt, &costs) + if err == nil { + t.Errorf("time cost of %d should be invalid", costs.Time) + } +} + +func TestBadMemory(t *testing.T) { + pk, err := fakePassphraseKey() + if err != nil { + t.Fatal(err) + } + costs := *hashTestCases[0].costs + costs.Memory = 7 + _, err = PassphraseHash(pk, fakeSalt, &costs) + if err == nil { + t.Errorf("memory cost of %d should be invalid", costs.Memory) + } +} + +func TestBadParallelism(t *testing.T) { + pk, err := fakePassphraseKey() + if err != nil { + t.Fatal(err) + } + costs := *hashTestCases[0].costs + costs.Parallelism = 1 << 24 + costs.Memory = 1 << 27 // Running n threads requires at least 8*n memory + _, err = PassphraseHash(pk, fakeSalt, &costs) + if err == nil { + t.Errorf("parallelism cost of %d should be invalid", costs.Parallelism) + } +} + func BenchmarkWrap(b *testing.B) { for n := 0; n < b.N; n++ { Wrap(fakeWrappingKey, fakeValidPolicyKey) @@ -391,3 +491,43 @@ func BenchmarkRandomWrapUnwrap(b *testing.B) { sk.Wipe() } } + +func benchmarkPassphraseHashing(b *testing.B, costs *HashingCosts) { + for n := 0; n < b.N; n++ { + pk, err := fakePassphraseKey() + if err != nil { + b.Fatal(err) + } + defer pk.Wipe() + hash, err := PassphraseHash(pk, fakeSalt, costs) + hash.Wipe() + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPassphraseHashing_1MB_1Thread(b *testing.B) { + benchmarkPassphraseHashing(b, + &HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1}) +} + +func BenchmarkPassphraseHashing_1GB_1Thread(b *testing.B) { + benchmarkPassphraseHashing(b, + &HashingCosts{Time: 1, Memory: 1 << 20, Parallelism: 1}) +} + +func BenchmarkPassphraseHashing_128MB_1Thread(b *testing.B) { + benchmarkPassphraseHashing(b, + &HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 1}) +} + +func BenchmarkPassphraseHashing_128MB_8Thread(b *testing.B) { + benchmarkPassphraseHashing(b, + &HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 8}) +} + +func BenchmarkPassphraseHashing_128MB_8Pass(b *testing.B) { + benchmarkPassphraseHashing(b, + &HashingCosts{Time: 8, Memory: 1 << 17, Parallelism: 1}) +} |