aboutsummaryrefslogtreecommitdiff
path: root/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'crypto')
-rw-r--r--crypto/crypto.go94
-rw-r--r--crypto/crypto_test.go148
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})
+}