2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-26 06:46:34 +00:00
restic/key.go
Alexander Neumann 662e07d17a Refactor crypto layer, switch HMAC for Poyl1305-AES
HMAC-SHA256 calls SHA256() twice which is very expensive. Therefore,
this commit uses Poly1305-AES instead of HMAC-SHA256.

benchcmp:

     benchmark                         old ns/op      new ns/op      delta
     BenchmarkChunkEncrypt             261033772      195114818      -25.25%
     BenchmarkChunkEncryptParallel     260973195      195787368      -24.98%
     BenchmarkArchiveDirectory         1050500651     1002615884     -4.56%
     BenchmarkPreload                  23544286       24994508       +6.16%
     BenchmarkLoadTree                 350065         427665         +22.17%
     BenchmarkEncryptWriter            87789753       31069126       -64.61%
     BenchmarkEncrypt                  88283197       38259043       -56.66%
     BenchmarkDecryptReader            90478843       40714818       -55.00%
     BenchmarkEncryptDecryptReader     179917626      81231730       -54.85%
     BenchmarkDecrypt                  87871591       37784207       -57.00%
     BenchmarkSaveJSON                 52481          56861          +8.35%
     BenchmarkSaveFrom                 75404085       51108596       -32.22%
     BenchmarkLoadJSONID               90545437       82696805       -8.67%

     benchmark                         old MB/s     new MB/s     speedup
     BenchmarkChunkEncrypt             40.17        53.74        1.34x
     BenchmarkChunkEncryptParallel     40.18        53.56        1.33x
     BenchmarkEncryptWriter            95.55        270.00       2.83x
     BenchmarkEncrypt                  95.02        219.26       2.31x
     BenchmarkDecryptReader            92.71        206.03       2.22x
     BenchmarkEncryptDecryptReader     46.62        103.27       2.22x
     BenchmarkDecrypt                  95.46        222.01       2.33x
     BenchmarkSaveFrom                 55.62        82.07        1.48x

     benchmark                         old allocs     new allocs     delta
     BenchmarkChunkEncrypt             112            110            -1.79%
     BenchmarkChunkEncryptParallel     103            100            -2.91%
     BenchmarkArchiveDirectory         383704         392083         +2.18%
     BenchmarkPreload                  21765          21874          +0.50%
     BenchmarkLoadTree                 341            436            +27.86%
     BenchmarkEncryptWriter            20             17             -15.00%
     BenchmarkEncrypt                  14             13             -7.14%
     BenchmarkDecryptReader            18             15             -16.67%
     BenchmarkEncryptDecryptReader     46             39             -15.22%
     BenchmarkDecrypt                  16             12             -25.00%
     BenchmarkSaveJSON                 81             86             +6.17%
     BenchmarkSaveFrom                 117            121            +3.42%
     BenchmarkLoadJSONID               80525          80264          -0.32%

     benchmark                         old bytes     new bytes     delta
     BenchmarkChunkEncrypt             118956        64697         -45.61%
     BenchmarkChunkEncryptParallel     118972        64681         -45.63%
     BenchmarkArchiveDirectory         160236600     177498232     +10.77%
     BenchmarkPreload                  2772488       3302992       +19.13%
     BenchmarkLoadTree                 49102         46484         -5.33%
     BenchmarkEncryptWriter            28927         8388146       +28897.64%
     BenchmarkEncrypt                  2473          1950          -21.15%
     BenchmarkDecryptReader            527827        2774          -99.47%
     BenchmarkEncryptDecryptReader     4100875       1528036       -62.74%
     BenchmarkDecrypt                  2509          2154          -14.15%
     BenchmarkSaveJSON                 4971          5892          +18.53%
     BenchmarkSaveFrom                 40117         31742         -20.88%
     BenchmarkLoadJSONID               9444217       9442106       -0.02%

This closes #102.
2015-03-14 20:00:41 +01:00

322 lines
7.7 KiB
Go

package restic
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/user"
"time"
"github.com/restic/restic/backend"
"github.com/restic/restic/chunker"
"golang.org/x/crypto/poly1305"
)
// max size is 8MiB, defined in chunker
const macSize = poly1305.TagSize // Poly1305 size is 16 byte
const maxCiphertextSize = ivSize + chunker.MaxSize + macSize
const CiphertextExtension = ivSize + macSize
var (
// ErrUnauthenticated is returned when ciphertext verification has failed.
ErrUnauthenticated = errors.New("ciphertext verification failed")
// ErrNoKeyFound is returned when no key for the repository could be decrypted.
ErrNoKeyFound = errors.New("no key could be found")
// ErrBufferTooSmall is returned when the destination slice is too small
// for the ciphertext.
ErrBufferTooSmall = errors.New("destination buffer too small")
)
// TODO: figure out scrypt values on the fly depending on the current
// hardware.
const (
scryptN = 65536
scryptR = 8
scryptP = 1
scryptSaltsize = 64
)
// Key represents an encrypted master key for a repository.
type Key struct {
Created time.Time `json:"created"`
Username string `json:"username"`
Hostname string `json:"hostname"`
Comment string `json:"comment,omitempty"`
KDF string `json:"kdf"`
N int `json:"N"`
R int `json:"r"`
P int `json:"p"`
Salt []byte `json:"salt"`
Data []byte `json:"data"`
user *keys
master *keys
id backend.ID
}
// keys is a JSON structure that holds signing and encryption keys.
type keys struct {
Sign *MACKey
Encrypt *AESKey
}
// CreateKey initializes a master key in the given backend and encrypts it with
// the password.
func CreateKey(s Server, password string) (*Key, error) {
return AddKey(s, password, nil)
}
// OpenKey tries do decrypt the key specified by id with the given password.
func OpenKey(s Server, id backend.ID, password string) (*Key, error) {
k, err := LoadKey(s, id)
if err != nil {
return nil, err
}
// check KDF
if k.KDF != "scrypt" {
return nil, errors.New("only supported KDF is scrypt()")
}
// derive user key
k.user, err = kdf(k, password)
if err != nil {
return nil, err
}
// decrypt master keys
buf, err := k.DecryptUser([]byte{}, k.Data)
if err != nil {
return nil, err
}
// restore json
k.master = &keys{}
err = json.Unmarshal(buf, k.master)
if err != nil {
return nil, err
}
k.id = id
return k, nil
}
// SearchKey tries to decrypt all keys in the backend with the given password.
// If none could be found, ErrNoKeyFound is returned.
func SearchKey(s Server, password string) (*Key, error) {
// list all keys
ids, err := s.List(backend.Key)
if err != nil {
panic(err)
}
// try all keys in repo
var key *Key
for _, id := range ids {
key, err = OpenKey(s, id, password)
if err != nil {
continue
}
return key, nil
}
return nil, ErrNoKeyFound
}
// LoadKey loads a key from the backend.
func LoadKey(s Server, id backend.ID) (*Key, error) {
// extract data from repo
data, err := s.Get(backend.Key, id)
if err != nil {
return nil, err
}
// restore json
k := &Key{}
err = json.Unmarshal(data, k)
if err != nil {
return nil, err
}
return k, err
}
// AddKey adds a new key to an already existing repository.
func AddKey(s Server, password string, template *Key) (*Key, error) {
// fill meta data about key
newkey := &Key{
Created: time.Now(),
KDF: "scrypt",
N: scryptN,
R: scryptR,
P: scryptP,
}
hn, err := os.Hostname()
if err == nil {
newkey.Hostname = hn
}
usr, err := user.Current()
if err == nil {
newkey.Username = usr.Username
}
// generate random salt
newkey.Salt = make([]byte, scryptSaltsize)
n, err := rand.Read(newkey.Salt)
if n != scryptSaltsize || err != nil {
panic("unable to read enough random bytes for salt")
}
// call KDF to derive user key
newkey.user, err = kdf(newkey, password)
if err != nil {
return nil, err
}
if template == nil {
// generate new random master keys
newkey.master = newkey.newKeys()
} else {
// copy master keys from old key
newkey.master = template.master
}
// encrypt master keys (as json) with user key
buf, err := json.Marshal(newkey.master)
if err != nil {
return nil, err
}
newkey.Data = GetChunkBuf("key")
n, err = newkey.EncryptUser(newkey.Data, buf)
newkey.Data = newkey.Data[:n]
// dump as json
buf, err = json.Marshal(newkey)
if err != nil {
return nil, err
}
// store in repository and return
blob, err := s.Create(backend.Key)
if err != nil {
return nil, err
}
_, err = blob.Write(buf)
if err != nil {
return nil, err
}
err = blob.Close()
if err != nil {
return nil, err
}
id, err := blob.ID()
if err != nil {
return nil, err
}
newkey.id = id
FreeChunkBuf("key", newkey.Data)
return newkey, nil
}
func (k *Key) newKeys() *keys {
return &keys{
Encrypt: generateRandomAESKey(),
Sign: generateRandomMACKey(),
}
}
func (k *Key) newIV(buf []byte) error {
_, err := io.ReadFull(rand.Reader, buf[:ivSize])
buf = buf[:ivSize]
if err != nil {
return err
}
return nil
}
// EncryptUser encrypts and signs data with the user key. Stored in ciphertext
// is IV || Ciphertext || MAC.
func (k *Key) EncryptUser(ciphertext, plaintext []byte) (int, error) {
return Encrypt(k.user, ciphertext, plaintext)
}
// Encrypt encrypts and signs data with the master key. Stored in ciphertext is
// IV || Ciphertext || MAC. Returns the ciphertext length.
func (k *Key) Encrypt(ciphertext, plaintext []byte) (int, error) {
return Encrypt(k.master, ciphertext, plaintext)
}
// EncryptTo encrypts and signs data with the master key. The returned
// io.Writer writes IV || Ciphertext || HMAC. For the hash function, SHA256 is
// used.
func (k *Key) EncryptTo(wr io.Writer) io.WriteCloser {
return EncryptTo(k.master, wr)
}
// EncryptUserTo encrypts and signs data with the user key. The returned
// io.Writer writes IV || Ciphertext || HMAC. For the hash function, SHA256 is
// used.
func (k *Key) EncryptUserTo(wr io.Writer) io.WriteCloser {
return EncryptTo(k.user, wr)
}
// Decrypt verifes and decrypts the ciphertext with the master key. Ciphertext
// must be in the form IV || Ciphertext || MAC.
func (k *Key) Decrypt(plaintext, ciphertext []byte) ([]byte, error) {
return Decrypt(k.master, plaintext, ciphertext)
}
// DecryptUser verifes and decrypts the ciphertext with the user key. Ciphertext
// must be in the form IV || Ciphertext || MAC.
func (k *Key) DecryptUser(plaintext, ciphertext []byte) ([]byte, error) {
return Decrypt(k.user, plaintext, ciphertext)
}
// 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 || MAC. In order to correctly verify the ciphertext, rd is
// drained, locally buffered and made available on the returned Reader
// afterwards. If an MAC verification failure is observed, it is returned
// immediately.
func (k *Key) DecryptFrom(rd io.Reader) (io.ReadCloser, error) {
return 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 || MAC. In order to correctly verify the ciphertext,
// rd is drained, locally buffered and made available on the returned Reader
// afterwards. If an MAC verification failure is observed, it is returned
// immediately.
func (k *Key) DecryptUserFrom(rd io.Reader) (io.ReadCloser, error) {
return DecryptFrom(k.user, rd)
}
func (k *Key) String() string {
if k == nil {
return "<Key nil>"
}
return fmt.Sprintf("<Key of %s@%s, created on %s>", k.Username, k.Hostname, k.Created)
}
func (k Key) ID() backend.ID {
return k.id
}