mirror of
https://github.com/syncthing/syncthing.git
synced 2026-02-07 20:52:54 -05:00
This makes a couple of backwards compatible changes to the ClusterConfig: - Remove the `ignore_permissions` and `ignore_delete` booleans which we've never read or used for anything - Remove the `disable_temp_indexes` boolean and option entirely. We did use this one, and about 1% of users have set the option. The only thing it does is inhibits sending of periodical DownloadProgress messages while downloading data, which is a minuscule bandwidth optimisation given that we're already sending data at the time. - Change the `read_only` boolean (which indicated send-only folders) to an enum `FolderType`, where the values zero and one match the existing usage. Again, we don't actually use this value, but I can see that we might want to and then it makes more sense for it to be more comprehensive. - Change the `paused` boolean to an enum `StopReason`, where zero indicates not stopped and one indicates paused, exactly the same wire representation as previously but leaves space for additional stop reasons (errors etc).
706 lines
20 KiB
Go
706 lines
20 KiB
Go
// Copyright (C) 2019 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
package protocol
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base32"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
"github.com/miscreant/miscreant.go"
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
"golang.org/x/crypto/hkdf"
|
|
"golang.org/x/crypto/scrypt"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/syncthing/syncthing/internal/gen/bep"
|
|
"github.com/syncthing/syncthing/lib/rand"
|
|
)
|
|
|
|
const (
|
|
nonceSize = 24 // chacha20poly1305.NonceSizeX
|
|
tagSize = 16 // chacha20poly1305.Overhead()
|
|
keySize = 32 // fits both chacha20poly1305 and AES-SIV
|
|
minPaddedSize = 1024 // smallest block we'll allow
|
|
blockOverhead = tagSize + nonceSize
|
|
maxPathComponent = 200 // characters
|
|
encryptedDirExtension = ".syncthing-enc" // for top level dirs
|
|
miscreantAlgo = "AES-SIV"
|
|
folderKeyCacheEntries = 1000
|
|
fileKeyCacheEntries = 5000
|
|
)
|
|
|
|
// The encryptedModel sits between the encrypted device and the model. It
|
|
// receives encrypted metadata and requests from the untrusted device, so it
|
|
// must decrypt those and answer requests by encrypting the data.
|
|
type encryptedModel struct {
|
|
model rawModel
|
|
folderKeys *folderKeyRegistry
|
|
keyGen *KeyGenerator
|
|
}
|
|
|
|
func newEncryptedModel(model rawModel, keyGen *KeyGenerator) encryptedModel {
|
|
return encryptedModel{
|
|
model: model,
|
|
folderKeys: newFolderKeyRegistry(),
|
|
keyGen: keyGen,
|
|
}
|
|
}
|
|
|
|
func (e encryptedModel) Index(idx *Index) error {
|
|
if folderKey, ok := e.folderKeys.get(idx.Folder); ok {
|
|
// incoming index data to be decrypted
|
|
if err := decryptFileInfos(e.keyGen, idx.Files, folderKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return e.model.Index(idx)
|
|
}
|
|
|
|
func (e encryptedModel) IndexUpdate(idxUp *IndexUpdate) error {
|
|
if folderKey, ok := e.folderKeys.get(idxUp.Folder); ok {
|
|
// incoming index data to be decrypted
|
|
if err := decryptFileInfos(e.keyGen, idxUp.Files, folderKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return e.model.IndexUpdate(idxUp)
|
|
}
|
|
|
|
func (e encryptedModel) Request(req *Request) (RequestResponse, error) {
|
|
folderKey, ok := e.folderKeys.get(req.Folder)
|
|
if !ok {
|
|
return e.model.Request(req)
|
|
}
|
|
|
|
// Figure out the real file name, offset and size from the encrypted /
|
|
// tweaked values.
|
|
|
|
realName, err := decryptName(req.Name, folderKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decrypting name: %w", err)
|
|
}
|
|
realSize := req.Size - blockOverhead
|
|
realOffset := req.Offset - int64(req.BlockNo*blockOverhead)
|
|
|
|
if req.Size < minPaddedSize {
|
|
return nil, errors.New("short request")
|
|
}
|
|
|
|
// Attempt to decrypt the block hash; it may be nil depending on what
|
|
// type of device the request comes from. Trusted devices with
|
|
// encryption enabled know the hash but don't bother to encrypt & send
|
|
// it to us. Untrusted devices have the hash from the encrypted index
|
|
// data and do send it. The model knows to only verify the hash if it
|
|
// actually gets one.
|
|
|
|
var realHash []byte
|
|
fileKey := e.keyGen.FileKey(realName, folderKey)
|
|
if len(req.Hash) > 0 {
|
|
var additional [8]byte
|
|
binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
|
|
realHash, err = decryptDeterministic(req.Hash, fileKey, additional[:])
|
|
if err != nil {
|
|
// "Legacy", no offset additional data?
|
|
realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decrypting block hash: %w", err)
|
|
}
|
|
}
|
|
|
|
// Perform that request and grab the data.
|
|
|
|
req.Name = realName
|
|
req.Size = realSize
|
|
req.Offset = realOffset
|
|
req.Hash = realHash
|
|
resp, err := e.model.Request(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Encrypt the response. Blocks smaller than minPaddedSize are padded
|
|
// with random data.
|
|
|
|
data := resp.Data()
|
|
if len(data) < minPaddedSize {
|
|
nd := make([]byte, minPaddedSize)
|
|
copy(nd, data)
|
|
if _, err := rand.Read(nd[len(data):]); err != nil {
|
|
panic("catastrophic randomness failure")
|
|
}
|
|
data = nd
|
|
}
|
|
enc := encryptBytes(data, fileKey)
|
|
resp.Close()
|
|
return rawResponse{enc}, nil
|
|
}
|
|
|
|
func (e encryptedModel) DownloadProgress(p *DownloadProgress) error {
|
|
if _, ok := e.folderKeys.get(p.Folder); !ok {
|
|
return e.model.DownloadProgress(p)
|
|
}
|
|
|
|
// We currently ignore these, though we could in principle translate
|
|
// them and use partially downloaded encrypted files like we do normal
|
|
// files.
|
|
return nil
|
|
}
|
|
|
|
func (e encryptedModel) ClusterConfig(config *ClusterConfig) error {
|
|
return e.model.ClusterConfig(config)
|
|
}
|
|
|
|
func (e encryptedModel) Closed(err error) {
|
|
e.model.Closed(err)
|
|
}
|
|
|
|
// The encryptedConnection sits between the model and the encrypted device. It
|
|
// encrypts outgoing metadata and decrypts incoming responses.
|
|
type encryptedConnection struct {
|
|
ConnectionInfo
|
|
conn *rawConnection
|
|
folderKeys *folderKeyRegistry
|
|
keyGen *KeyGenerator
|
|
}
|
|
|
|
func newEncryptedConnection(ci ConnectionInfo, conn *rawConnection, folderKeys *folderKeyRegistry, keyGen *KeyGenerator) encryptedConnection {
|
|
return encryptedConnection{
|
|
ConnectionInfo: ci,
|
|
conn: conn,
|
|
folderKeys: folderKeys,
|
|
keyGen: keyGen,
|
|
}
|
|
}
|
|
|
|
func (e encryptedConnection) Start() {
|
|
e.conn.Start()
|
|
}
|
|
|
|
func (e encryptedConnection) DeviceID() DeviceID {
|
|
return e.conn.DeviceID()
|
|
}
|
|
|
|
func (e encryptedConnection) Index(ctx context.Context, idx *Index) error {
|
|
if folderKey, ok := e.folderKeys.get(idx.Folder); ok {
|
|
encryptFileInfos(e.keyGen, idx.Files, folderKey)
|
|
}
|
|
return e.conn.Index(ctx, idx)
|
|
}
|
|
|
|
func (e encryptedConnection) IndexUpdate(ctx context.Context, idxUp *IndexUpdate) error {
|
|
if folderKey, ok := e.folderKeys.get(idxUp.Folder); ok {
|
|
encryptFileInfos(e.keyGen, idxUp.Files, folderKey)
|
|
}
|
|
return e.conn.IndexUpdate(ctx, idxUp)
|
|
}
|
|
|
|
func (e encryptedConnection) Request(ctx context.Context, req *Request) ([]byte, error) {
|
|
folderKey, ok := e.folderKeys.get(req.Folder)
|
|
if !ok {
|
|
return e.conn.Request(ctx, req)
|
|
}
|
|
fileKey := e.keyGen.FileKey(req.Name, folderKey)
|
|
|
|
// Encrypt / adjust the request parameters.
|
|
|
|
encSize := req.Size
|
|
if encSize < minPaddedSize {
|
|
// Make a request for minPaddedSize data instead of the smaller
|
|
// block. We'll chop of the extra data later.
|
|
encSize = minPaddedSize
|
|
}
|
|
encSize += blockOverhead
|
|
encName := encryptName(req.Name, folderKey)
|
|
encOffset := req.Offset + int64(req.BlockNo*blockOverhead)
|
|
encHash := encryptBlockHash(req.Hash, req.Offset, fileKey)
|
|
|
|
// Perform that request, getting back an encrypted block.
|
|
|
|
encReq := &Request{
|
|
ID: req.ID,
|
|
Folder: req.Folder,
|
|
Name: encName,
|
|
Offset: encOffset,
|
|
Size: encSize,
|
|
Hash: encHash,
|
|
BlockNo: req.BlockNo,
|
|
}
|
|
bs, err := e.conn.Request(ctx, encReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the decrypted block (or an error if it fails decryption)
|
|
|
|
bs, err = DecryptBytes(bs, fileKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bs[:req.Size], nil
|
|
}
|
|
|
|
func (e encryptedConnection) DownloadProgress(ctx context.Context, dp *DownloadProgress) {
|
|
if _, ok := e.folderKeys.get(dp.Folder); !ok {
|
|
e.conn.DownloadProgress(ctx, dp)
|
|
}
|
|
|
|
// No need to send these
|
|
}
|
|
|
|
func (e encryptedConnection) ClusterConfig(config *ClusterConfig, passwords map[string]string) {
|
|
e.folderKeys.setPasswords(e.keyGen, passwords)
|
|
e.conn.ClusterConfig(config, passwords)
|
|
}
|
|
|
|
func (e encryptedConnection) Close(err error) {
|
|
e.conn.Close(err)
|
|
}
|
|
|
|
func (e encryptedConnection) Closed() <-chan struct{} {
|
|
return e.conn.Closed()
|
|
}
|
|
|
|
func (e encryptedConnection) Statistics() Statistics {
|
|
return e.conn.Statistics()
|
|
}
|
|
|
|
func encryptFileInfos(keyGen *KeyGenerator, files []FileInfo, folderKey *[keySize]byte) {
|
|
for i, fi := range files {
|
|
files[i] = encryptFileInfo(keyGen, fi, folderKey)
|
|
}
|
|
}
|
|
|
|
// encryptFileInfo encrypts a FileInfo and wraps it into a new fake FileInfo
|
|
// with an encrypted name.
|
|
func encryptFileInfo(keyGen *KeyGenerator, fi FileInfo, folderKey *[keySize]byte) FileInfo {
|
|
fileKey := keyGen.FileKey(fi.Name, folderKey)
|
|
|
|
// The entire FileInfo is encrypted with a random nonce, and concatenated
|
|
// with that nonce.
|
|
|
|
bs, err := proto.Marshal(fi.ToWire(false))
|
|
if err != nil {
|
|
panic("impossible serialization mishap: " + err.Error())
|
|
}
|
|
encryptedFI := encryptBytes(bs, fileKey)
|
|
|
|
// The vector is set to something that is higher than any other version sent
|
|
// previously. We do this because
|
|
// there is no way for the insecure device on the other end to do proper
|
|
// conflict resolution, so they will simply accept and keep whatever is the
|
|
// latest version they see. The secure devices will decrypt the real
|
|
// FileInfo, see the real Version, and act appropriately regardless of what
|
|
// this fake version happens to be.
|
|
// The vector also needs to be deterministic/the same among all trusted
|
|
// devices with the same vector, such that the pulling/remote completion
|
|
// works correctly on the untrusted device(s).
|
|
|
|
version := Vector{
|
|
Counters: []Counter{
|
|
{
|
|
ID: 1,
|
|
},
|
|
},
|
|
}
|
|
for _, counter := range fi.Version.Counters {
|
|
version.Counters[0].Value += counter.Value
|
|
}
|
|
|
|
// Construct the fake block list. Each block will be blockOverhead bytes
|
|
// larger than the corresponding real one and have an encrypted hash.
|
|
// Very small blocks will be padded upwards to minPaddedSize.
|
|
//
|
|
// The encrypted hash becomes just a "token" for the data -- it doesn't
|
|
// help verifying it, but it lets the encrypted device do block level
|
|
// diffs and data reuse properly when it gets a new version of a file.
|
|
|
|
var offset int64
|
|
blocks := make([]BlockInfo, len(fi.Blocks))
|
|
for i, b := range fi.Blocks {
|
|
if b.Size < minPaddedSize {
|
|
b.Size = minPaddedSize
|
|
}
|
|
size := b.Size + blockOverhead
|
|
hash := encryptBlockHash(b.Hash, b.Offset, fileKey)
|
|
blocks[i] = BlockInfo{
|
|
Hash: hash,
|
|
Offset: offset,
|
|
Size: size,
|
|
}
|
|
offset += int64(size)
|
|
}
|
|
|
|
// Construct the fake FileInfo. This is mostly just a wrapper around the
|
|
// encrypted FileInfo and fake block list. We'll represent symlinks as
|
|
// directories, because they need some sort of on disk representation
|
|
// but have no data outside of the metadata. Deletion and sequence
|
|
// numbering are handled as usual.
|
|
|
|
typ := FileInfoTypeFile
|
|
if fi.Type != FileInfoTypeFile {
|
|
typ = FileInfoTypeDirectory
|
|
}
|
|
enc := FileInfo{
|
|
Name: encryptName(fi.Name, folderKey),
|
|
Type: typ,
|
|
Permissions: 0o644,
|
|
ModifiedS: 1234567890, // Sat Feb 14 00:31:30 CET 2009
|
|
Deleted: fi.Deleted,
|
|
Version: version,
|
|
Sequence: fi.Sequence,
|
|
Encrypted: encryptedFI,
|
|
}
|
|
if fi.IsInvalid() {
|
|
enc.LocalFlags = FlagLocalRemoteInvalid
|
|
}
|
|
if typ == FileInfoTypeFile {
|
|
enc.Size = offset // new total file size
|
|
enc.Blocks = blocks
|
|
enc.RawBlockSize = int32(fi.BlockSize() + blockOverhead)
|
|
}
|
|
|
|
return enc
|
|
}
|
|
|
|
func encryptBlockHash(hash []byte, offset int64, fileKey *[keySize]byte) []byte {
|
|
// The offset goes into the encrypted block hash as additional data,
|
|
// essentially mixing in with the nonce. This means a block hash
|
|
// remains stable for the same data at the same offset, but doesn't
|
|
// reveal the existence of identical data blocks at other offsets.
|
|
var additional [8]byte
|
|
binary.BigEndian.PutUint64(additional[:], uint64(offset))
|
|
return encryptDeterministic(hash, fileKey, additional[:])
|
|
}
|
|
|
|
func decryptFileInfos(keyGen *KeyGenerator, files []FileInfo, folderKey *[keySize]byte) error {
|
|
for i, fi := range files {
|
|
decFI, err := DecryptFileInfo(keyGen, fi, folderKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files[i] = decFI
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DecryptFileInfo extracts the encrypted portion of a FileInfo, decrypts it
|
|
// and returns that.
|
|
func DecryptFileInfo(keyGen *KeyGenerator, fi FileInfo, folderKey *[keySize]byte) (FileInfo, error) {
|
|
realName, err := decryptName(fi.Name, folderKey)
|
|
if err != nil {
|
|
return FileInfo{}, err
|
|
}
|
|
|
|
fileKey := keyGen.FileKey(realName, folderKey)
|
|
dec, err := DecryptBytes(fi.Encrypted, fileKey)
|
|
if err != nil {
|
|
return FileInfo{}, err
|
|
}
|
|
|
|
var decFI bep.FileInfo
|
|
if err := proto.Unmarshal(dec, &decFI); err != nil {
|
|
return FileInfo{}, err
|
|
}
|
|
|
|
// Preserve sequence, which is legitimately controlled by the untrusted device
|
|
decFI.Sequence = fi.Sequence
|
|
|
|
return FileInfoFromWire(&decFI), nil
|
|
}
|
|
|
|
var base32Hex = base32.HexEncoding.WithPadding(base32.NoPadding)
|
|
|
|
// encryptName encrypts the given string in a deterministic manner (the
|
|
// result is always the same for any given string) and encodes it in a
|
|
// filesystem-friendly manner.
|
|
func encryptName(name string, key *[keySize]byte) string {
|
|
enc := encryptDeterministic([]byte(name), key, nil)
|
|
return slashify(base32Hex.EncodeToString(enc))
|
|
}
|
|
|
|
// decryptName decrypts a string from encryptName
|
|
func decryptName(name string, key *[keySize]byte) (string, error) {
|
|
name, err := deslashify(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
bs, err := base32Hex.DecodeString(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dec, err := decryptDeterministic(bs, key, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(dec), nil
|
|
}
|
|
|
|
// encryptBytes encrypts bytes with a random nonce
|
|
func encryptBytes(data []byte, key *[keySize]byte) []byte {
|
|
nonce := randomNonce()
|
|
return encrypt(data, nonce, key)
|
|
}
|
|
|
|
// encryptDeterministic encrypts bytes using AES-SIV
|
|
func encryptDeterministic(data []byte, key *[keySize]byte, additionalData []byte) []byte {
|
|
aead, err := miscreant.NewAEAD(miscreantAlgo, key[:], 0)
|
|
if err != nil {
|
|
panic("cipher failure: " + err.Error())
|
|
}
|
|
return aead.Seal(nil, nil, data, additionalData)
|
|
}
|
|
|
|
// decryptDeterministic decrypts bytes using AES-SIV
|
|
func decryptDeterministic(data []byte, key *[keySize]byte, additionalData []byte) ([]byte, error) {
|
|
aead, err := miscreant.NewAEAD(miscreantAlgo, key[:], 0)
|
|
if err != nil {
|
|
panic("cipher failure: " + err.Error())
|
|
}
|
|
return aead.Open(nil, nil, data, additionalData)
|
|
}
|
|
|
|
func encrypt(data []byte, nonce *[nonceSize]byte, key *[keySize]byte) []byte {
|
|
aead, err := chacha20poly1305.NewX(key[:])
|
|
if err != nil {
|
|
// Can only fail if the key is the wrong length
|
|
panic("cipher failure: " + err.Error())
|
|
}
|
|
|
|
if aead.NonceSize() != nonceSize || aead.Overhead() != tagSize {
|
|
// We want these values to be constant for our type declarations so
|
|
// we don't use the values returned by the GCM, but we verify them
|
|
// here.
|
|
panic("crypto parameter mismatch")
|
|
}
|
|
|
|
// Data is appended to the nonce
|
|
return aead.Seal(nonce[:], nonce[:], data, nil)
|
|
}
|
|
|
|
// DecryptBytes returns the decrypted bytes, or an error if decryption
|
|
// failed.
|
|
func DecryptBytes(data []byte, key *[keySize]byte) ([]byte, error) {
|
|
if len(data) < blockOverhead {
|
|
return nil, errors.New("data too short")
|
|
}
|
|
|
|
aead, err := chacha20poly1305.NewX(key[:])
|
|
if err != nil {
|
|
// Can only fail if the key is the wrong length
|
|
panic("cipher failure: " + err.Error())
|
|
}
|
|
|
|
if aead.NonceSize() != nonceSize || aead.Overhead() != tagSize {
|
|
// We want these values to be constant for our type declarations so
|
|
// we don't use the values returned by the GCM, but we verify them
|
|
// here.
|
|
panic("crypto parameter mismatch")
|
|
}
|
|
|
|
return aead.Open(nil, data[:nonceSize], data[nonceSize:], nil)
|
|
}
|
|
|
|
// randomNonce is a normal, cryptographically random nonce
|
|
func randomNonce() *[nonceSize]byte {
|
|
var nonce [nonceSize]byte
|
|
if _, err := rand.Read(nonce[:]); err != nil {
|
|
panic("catastrophic randomness failure: " + err.Error())
|
|
}
|
|
return &nonce
|
|
}
|
|
|
|
// keysFromPasswords converts a set of folder ID to password into a set of
|
|
// folder ID to encryption key, using our key derivation function.
|
|
func keysFromPasswords(keyGen *KeyGenerator, passwords map[string]string) map[string]*[keySize]byte {
|
|
res := make(map[string]*[keySize]byte, len(passwords))
|
|
for folder, password := range passwords {
|
|
res[folder] = keyGen.KeyFromPassword(folder, password)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func knownBytes(folderID string) []byte {
|
|
return []byte("syncthing" + folderID)
|
|
}
|
|
|
|
type KeyGenerator struct {
|
|
mut sync.Mutex
|
|
folderKeys *lru.TwoQueueCache[folderKeyCacheKey, *[keySize]byte]
|
|
fileKeys *lru.TwoQueueCache[fileKeyCacheKey, *[keySize]byte]
|
|
}
|
|
|
|
func NewKeyGenerator() *KeyGenerator {
|
|
folderKeys, _ := lru.New2Q[folderKeyCacheKey, *[keySize]byte](folderKeyCacheEntries)
|
|
fileKeys, _ := lru.New2Q[fileKeyCacheKey, *[keySize]byte](fileKeyCacheEntries)
|
|
return &KeyGenerator{
|
|
folderKeys: folderKeys,
|
|
fileKeys: fileKeys,
|
|
}
|
|
}
|
|
|
|
type folderKeyCacheKey struct {
|
|
folderID string
|
|
password string
|
|
}
|
|
|
|
// KeyFromPassword uses key derivation to generate a stronger key from a
|
|
// probably weak password.
|
|
func (g *KeyGenerator) KeyFromPassword(folderID, password string) *[keySize]byte {
|
|
cacheKey := folderKeyCacheKey{folderID, password}
|
|
g.mut.Lock()
|
|
defer g.mut.Unlock()
|
|
if key, ok := g.folderKeys.Get(cacheKey); ok {
|
|
return key
|
|
}
|
|
bs, err := scrypt.Key([]byte(password), knownBytes(folderID), 32768, 8, 1, keySize)
|
|
if err != nil {
|
|
panic("key derivation failure: " + err.Error())
|
|
}
|
|
if len(bs) != keySize {
|
|
panic("key derivation failure: wrong number of bytes")
|
|
}
|
|
var key [keySize]byte
|
|
copy(key[:], bs)
|
|
g.folderKeys.Add(cacheKey, &key)
|
|
return &key
|
|
}
|
|
|
|
var hkdfSalt = []byte("syncthing")
|
|
|
|
type fileKeyCacheKey struct {
|
|
file string
|
|
key [keySize]byte
|
|
}
|
|
|
|
func (g *KeyGenerator) FileKey(filename string, folderKey *[keySize]byte) *[keySize]byte {
|
|
g.mut.Lock()
|
|
defer g.mut.Unlock()
|
|
cacheKey := fileKeyCacheKey{filename, *folderKey}
|
|
if key, ok := g.fileKeys.Get(cacheKey); ok {
|
|
return key
|
|
}
|
|
kdf := hkdf.New(sha256.New, append(folderKey[:], filename...), hkdfSalt, nil)
|
|
var fileKey [keySize]byte
|
|
n, err := io.ReadFull(kdf, fileKey[:])
|
|
if err != nil || n != keySize {
|
|
panic("hkdf failure")
|
|
}
|
|
g.fileKeys.Add(cacheKey, &fileKey)
|
|
return &fileKey
|
|
}
|
|
|
|
func PasswordToken(keyGen *KeyGenerator, folderID, password string) []byte {
|
|
return encryptDeterministic(knownBytes(folderID), keyGen.KeyFromPassword(folderID, password), nil)
|
|
}
|
|
|
|
// slashify inserts slashes (and file extension) in the string to create an
|
|
// appropriate tree. ABCDEFGH... => A.syncthing-enc/BC/DEFGH... We can use
|
|
// forward slashes here because we're on the outside of native path formats,
|
|
// the slash is the wire format.
|
|
func slashify(s string) string {
|
|
// We somewhat sloppily assume bytes == characters here, but the only
|
|
// file names we should deal with are those that come from our base32
|
|
// encoding.
|
|
|
|
comps := make([]string, 0, len(s)/maxPathComponent+3)
|
|
comps = append(comps, s[:1]+encryptedDirExtension)
|
|
s = s[1:]
|
|
comps = append(comps, s[:2])
|
|
s = s[2:]
|
|
|
|
for len(s) > maxPathComponent {
|
|
comps = append(comps, s[:maxPathComponent])
|
|
s = s[maxPathComponent:]
|
|
}
|
|
if len(s) > 0 {
|
|
comps = append(comps, s)
|
|
}
|
|
return strings.Join(comps, "/")
|
|
}
|
|
|
|
// deslashify removes slashes and encrypted file extensions from the string.
|
|
// This is the inverse of slashify().
|
|
func deslashify(s string) (string, error) {
|
|
if s == "" || !strings.HasPrefix(s[1:], encryptedDirExtension) {
|
|
return "", fmt.Errorf("invalid encrypted path: %q", s)
|
|
}
|
|
s = s[:1] + s[1+len(encryptedDirExtension):]
|
|
return strings.ReplaceAll(s, "/", ""), nil
|
|
}
|
|
|
|
type rawResponse struct {
|
|
data []byte
|
|
}
|
|
|
|
func (r rawResponse) Data() []byte {
|
|
return r.data
|
|
}
|
|
|
|
func (rawResponse) Close() {}
|
|
func (rawResponse) Wait() {}
|
|
|
|
// IsEncryptedParent returns true if the path points at a parent directory of
|
|
// encrypted data, i.e. is not a "real" directory. This is determined by
|
|
// checking for a sentinel string in the path.
|
|
func IsEncryptedParent(pathComponents []string) bool {
|
|
l := len(pathComponents)
|
|
if l == 2 && len(pathComponents[1]) != 2 {
|
|
return false
|
|
} else if l == 0 {
|
|
return false
|
|
}
|
|
if pathComponents[0] == "" {
|
|
return false
|
|
}
|
|
if pathComponents[0][1:] != encryptedDirExtension {
|
|
return false
|
|
}
|
|
if l < 2 {
|
|
return true
|
|
}
|
|
for _, comp := range pathComponents[2:] {
|
|
if len(comp) != maxPathComponent {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type folderKeyRegistry struct {
|
|
keys map[string]*[keySize]byte // folder ID -> key
|
|
mut sync.RWMutex
|
|
}
|
|
|
|
func newFolderKeyRegistry() *folderKeyRegistry {
|
|
return &folderKeyRegistry{
|
|
keys: make(map[string]*[keySize]byte),
|
|
}
|
|
}
|
|
|
|
func (r *folderKeyRegistry) get(folder string) (*[keySize]byte, bool) {
|
|
r.mut.RLock()
|
|
key, ok := r.keys[folder]
|
|
r.mut.RUnlock()
|
|
return key, ok
|
|
}
|
|
|
|
func (r *folderKeyRegistry) setPasswords(keyGen *KeyGenerator, passwords map[string]string) {
|
|
r.mut.Lock()
|
|
r.keys = keysFromPasswords(keyGen, passwords)
|
|
r.mut.Unlock()
|
|
}
|