94 lines
2.4 KiB
Go
94 lines
2.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// dataFormat is a file of bytes sealed as a backup is, but not one: the
|
|
// usage a computer shares through sync (#542). Open refuses it.
|
|
const dataFormat = "magpie-data"
|
|
|
|
// keys are the keys derived this run, by passphrase and salt: a computer
|
|
// seals every file it shares with one salt, and opens each other
|
|
// computer's with that one's, so a sync derives a key once a computer
|
|
// rather than once a file, PBKDF2 being slow on purpose.
|
|
var (
|
|
keysMu sync.Mutex
|
|
keys = map[[32]byte][]byte{}
|
|
salts = map[[32]byte][]byte{}
|
|
)
|
|
|
|
func derived(pass string, e envelope) ([]byte, error) {
|
|
k := sha256.Sum256(append([]byte(pass+"\x00"), e.Salt...))
|
|
keysMu.Lock()
|
|
defer keysMu.Unlock()
|
|
if key, ok := keys[k]; ok {
|
|
return key, nil
|
|
}
|
|
key, err := deriveKey(pass, e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys[k] = key
|
|
return key, nil
|
|
}
|
|
|
|
// SealData seals data under the passphrase, as Seal does a bundle.
|
|
func SealData(data []byte, pass string) ([]byte, error) {
|
|
if pass == "" {
|
|
return nil, errors.New("sealing needs a passphrase")
|
|
}
|
|
p := sha256.Sum256([]byte(pass))
|
|
keysMu.Lock()
|
|
salt := salts[p]
|
|
if salt == nil {
|
|
salt = make([]byte, 16)
|
|
rand.Read(salt)
|
|
salts[p] = salt
|
|
}
|
|
keysMu.Unlock()
|
|
e := envelope{Format: dataFormat, Version: 1, KDF: "pbkdf2-sha256", Iterations: iterations, Salt: salt, Nonce: make([]byte, 12)}
|
|
rand.Read(e.Nonce)
|
|
key, err := derived(pass, e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gcm, err := gcmOf(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Data = gcm.Seal(nil, e.Nonce, data, header(e))
|
|
return json.Marshal(e)
|
|
}
|
|
|
|
// OpenData opens what SealData sealed.
|
|
func OpenData(sealed []byte, pass string) ([]byte, error) {
|
|
var e envelope
|
|
if json.Unmarshal(sealed, &e) != nil || e.Format != dataFormat {
|
|
return nil, errors.New("not a file magpie sealed")
|
|
}
|
|
if e.Version != 1 || e.KDF != "pbkdf2-sha256" {
|
|
return nil, errors.New("sealed by a newer magpie")
|
|
}
|
|
if e.Iterations < 100_000 || e.Iterations > 10_000_000 || len(e.Nonce) != 12 || len(e.Salt) < 16 {
|
|
return nil, errors.New("not a file magpie sealed")
|
|
}
|
|
key, err := derived(pass, e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gcm, err := gcmOf(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plain, err := gcm.Open(nil, e.Nonce, e.Data, header(e))
|
|
if err != nil {
|
|
return nil, ErrPassphrase
|
|
}
|
|
return plain, nil
|
|
}
|