diff --git a/key.go b/key.go index 8162f21ea..459729f3c 100644 --- a/key.go +++ b/key.go @@ -12,6 +12,7 @@ import ( "fmt" "hash" "io" + "io/ioutil" "os" "os/user" "time" @@ -462,6 +463,79 @@ func (k *Key) DecryptUser(ciphertext []byte) ([]byte, error) { return k.decrypt(k.user, ciphertext) } +// decryptFrom verifies and decrypts the ciphertext read from rd with ks and +// makes it available on the returned Reader. Ciphertext must be in the form IV +// || Ciphertext || HMAC. In order to correctly verify the ciphertext, rd is +// drained, locally buffered and made available on the returned Reader +// afterwards. If an HMAC verification failure is observed, it is returned +// immediately. +func (k *Key) decryptFrom(ks *keys, rd io.Reader) (io.Reader, error) { + ciphertext, err := ioutil.ReadAll(rd) + if err != nil { + return nil, err + } + + // check for plausible length + if len(ciphertext) < ivSize+hmacSize { + panic("trying to decryipt invalid data: ciphertext too small") + } + + hm := hmac.New(sha256.New, ks.Sign) + + // extract hmac + l := len(ciphertext) - hm.Size() + ciphertext, mac := ciphertext[:l], ciphertext[l:] + + // calculate new hmac + n, err := hm.Write(ciphertext) + if err != nil || n != len(ciphertext) { + panic(fmt.Sprintf("unable to calculate hmac of ciphertext, err %v", err)) + } + + // verify hmac + mac2 := hm.Sum(nil) + + if !hmac.Equal(mac, mac2) { + return nil, ErrUnauthenticated + } + + // extract iv + iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:] + + // decrypt data + c, err := aes.NewCipher(ks.Encrypt) + if err != nil { + panic(fmt.Sprintf("unable to create cipher: %v", err)) + } + + r := cipher.StreamReader{ + S: cipher.NewCTR(c, iv), + R: bytes.NewReader(ciphertext), + } + + return r, nil +} + +// DecryptFrom verifies and decrypts the ciphertext read from rd and makes it +// available on the returned Reader. Ciphertext must be in the form IV || +// Ciphertext || HMAC. In order to correctly verify the ciphertext, rd is +// drained, locally buffered and made available on the returned Reader +// afterwards. If an HMAC verification failure is observed, it is returned +// immediately. +func (k *Key) DecryptFrom(rd io.Reader) (io.Reader, error) { + return k.decryptFrom(k.master, rd) +} + +// DecryptFrom verifies and decrypts the ciphertext read from rd with the user +// key and makes it available on the returned Reader. Ciphertext must be in the +// form IV || Ciphertext || HMAC. In order to correctly verify the ciphertext, +// rd is drained, locally buffered and made available on the returned Reader +// afterwards. If an HMAC verification failure is observed, it is returned +// immediately. +func (k *Key) DecryptUserFrom(rd io.Reader) (io.Reader, error) { + return k.decryptFrom(k.user, rd) +} + func (k *Key) String() string { if k == nil { return "" diff --git a/key_test.go b/key_test.go index f7ae5d9e7..c241c8900 100644 --- a/key_test.go +++ b/key_test.go @@ -164,6 +164,54 @@ func BenchmarkEncrypt(b *testing.B) { restic.FreeChunkBuf("BenchmarkEncrypt", buf) } +func BenchmarkDecryptReader(b *testing.B) { + be := setupBackend(b) + defer teardownBackend(b, be) + k := setupKey(b, be, testPassword) + + size := 8 << 20 // 8MiB + buf := get_random(23, size) + + ciphertext := make([]byte, len(buf)+restic.CiphertextExtension) + _, err := k.Encrypt(ciphertext, buf) + ok(b, err) + + rd := bytes.NewReader(ciphertext) + + b.ResetTimer() + b.SetBytes(int64(size)) + + for i := 0; i < b.N; i++ { + rd.Seek(0, 0) + decRd, err := k.DecryptFrom(rd) + ok(b, err) + + _, err = io.Copy(ioutil.Discard, decRd) + ok(b, err) + } +} + +func BenchmarkEncryptDecryptReader(b *testing.B) { + be := setupBackend(b) + defer teardownBackend(b, be) + k := setupKey(b, be, testPassword) + + size := 8 << 20 // 8MiB + rd := randomReader(23, size) + + b.ResetTimer() + b.SetBytes(int64(size)) + + for i := 0; i < b.N; i++ { + rd.Seek(0, 0) + decRd, err := k.DecryptFrom(k.EncryptFrom(rd)) + ok(b, err) + + _, err = io.Copy(ioutil.Discard, decRd) + ok(b, err) + } +} + func BenchmarkDecrypt(b *testing.B) { size := 8 << 20 // 8MiB data := make([]byte, size) @@ -255,3 +303,39 @@ func TestEncryptStreamReader(t *testing.T) { data, plaintext) } } + +func TestDecryptStreamReader(t *testing.T) { + s := setupBackend(t) + defer teardownBackend(t, s) + k := setupKey(t, s, testPassword) + + tests := []int{5, 23, 2<<18 + 23, 1 << 20} + if *testLargeCrypto { + tests = append(tests, 7<<20+123) + } + + for _, size := range tests { + data := make([]byte, size) + _, err := io.ReadFull(randomReader(42, size), data) + ok(t, err) + + ciphertext := make([]byte, size+restic.CiphertextExtension) + + // encrypt with default function + n, err := k.Encrypt(ciphertext, data) + ok(t, err) + assert(t, n == len(data)+restic.CiphertextExtension, + "wrong number of bytes returned after encryption: expected %d, got %d", + len(data)+restic.CiphertextExtension, n) + + rd, err := k.DecryptFrom(bytes.NewReader(ciphertext)) + ok(t, err) + + plaintext, err := ioutil.ReadAll(rd) + ok(t, err) + + assert(t, bytes.Equal(data, plaintext), + "wrong plaintext after decryption: expected %02x, got %02x", + data, plaintext) + } +}