From 3f9c09b1e0901248c96c47e392a2888c40b2f182 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 2 Mar 2017 17:32:50 -0800 Subject: [PATCH] crypto: passphrase hashing with Argon2 This commit adds in the PassphraseHash function which hashes the provided passphrase (in key form) using Argon2id. This cost parameters for Argon2id and that salt are both fed into the function. It also includes tests and benchmarks for the passphrase hashing. Change-Id: I060db3e71213c756d45ce5603a0a59d3d7a1e609 --- README.md | 11 ++++ crypto/crypto.go | 94 +++++++++++++++++++++++++-- crypto/crypto_test.go | 148 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 244 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e9cb5c8..79cbca8 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,15 @@ You will also want to add `$GOPATH/bin` to your `$PATH`. * `make` * A C compiler (`gcc` or `clang`) * Go +* [Argon2 Passphrase Hash](https://github.com/P-H-C/phc-winner-argon2), a C + library which can be installed (both the header `argon2.h` and library + `libargon2`) by running: + ```bash + > git clone https://github.com/P-H-C/phc-winner-argon2 argon2 + > cd argon2 + > make + > sudo make install + ``` Once this is setup, you can run `make fscrypt` to build the executable in `build/fscrypt`. Pass `"LDFLAGS += -static"` to `make` to get a static @@ -84,6 +93,8 @@ dynamically linked binary by default. `fscrypt` has the following runtime dependencies: * Kernel support for filesystem encryption (this will depend on your kernel configuration and specific filesystem) +* `libargon2` (see the above installation instructions for Argon2), unless you + built a static executable. Installing it just requires placing it in your path or running `make install`. Change `$GOBIN` to change the install location of `fscrypt`. By default, 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 // malloc(), free() +#include +*/ +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 -m -p

-l 32 +// where costs.Time = , costs.Memory = 2^, and costs.Parallelism =

. +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}) +} -- 2.39.5