cwtch/storage/profile_store.go

251 lines
8.2 KiB
Go

package storage
import (
"crypto/rand"
"cwtch.im/cwtch/model"
"errors"
"fmt"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"log"
"os"
"sync"
)
// Profile Storage stores profileGroups and supports storing "secret" profileGroups in a *deniable* manor
// Given preassigned block size and number of blocks the system generates random fillings for all slots
// slots 0 to n/2 - 1 are for public profileGroups and n/2 to n-1 are for "secret" profileGroups, and use different keys
// Any active profileGroups when saved are padded with random to the block size
// Thus on inspection it should be impossible to determine if the secret profileGroups are being used or not
// The reason we need to formally segment "main" and "deniable" profileGroups is that if they are mixed (and there are
// deniable profileGroups with more than one password) it can quikcly lead to a situation where if not all profileGroups are loaded
// and yet the user wants to add a new one, the system will have no way to determine if a slot is in use or not
// Profile block size
// Salt 128
// PublicProfile = ~106 (name is variable, pegged at 16 here)
// Group = 170
//
// PrivateProfile = 320 + Profile(106) ... 426
// + map[onion 56] PubProfile 106 = 162
// + map[groupid 16] Group 170 = 186
//
// Assume 256 peers and 256 groups max [weak] = 89514
// Hand wavy 1.5 multiplier for conversion to json
// = 113,450 bytes per peer block... * ignoring timelines...
// This is still limiting of some uses cases like massive distribution lists mimicing twitter behaviour
// where one peer can have up to millions of followers
// We could enable profileGroups to fill multiple blocks, but this may degrade secret profile deniability ?
// ProfileGroupID is used to select what division of the profiles storage to store a profile in
// They are all the same but we encourage thinking of group1 as a master/main group and the rest as more deniable
// Also depends on how many groups you configure your store to use
type ProfileGroupID int
// Suggested settings for initializing a ProfileStore. Not required to use
const (
HeaderSize = secretbox.Overhead + 24 + 128 // overhead + nounce + salt
DataBlockSizeSmall = 113450
DataBlockSizeMedium = DataBlockSizeSmall * 4
DataBlockSizeLarge = DataBlockSizeMedium * 4
NumProfileBlocks = 32 // means each small store is about 2.2 MB, medium is 10 MB, and large is 40 MB
)
// Usable names for profileStore divisions
const (
GroupMaster ProfileGroupID = iota
GroupDeniable1
GroupDeniable2
GroupDeniable3
GroupDeniable4
GroupDeniable5
)
type profileGroup struct {
profiles []*model.Profile
password string
}
type profileStore struct {
dataBlocksize int
fullBlocksize int
numBlocks int
numGroups int
filename string
profileGroups []profileGroup
mutex sync.Mutex
}
// ProfileStore is a storage system for profiles which can offer deniability.
// It's main purpose is to give peer's profiles persistence
// It's secondard use is the ability to support deniable profiles. If an app uses a store with more than 1 division
// it should offer no way to prove that those divisions are or are not in use
type ProfileStore interface {
InitializeProfileGroup(groupid ProfileGroupID, password string) ([]*model.Profile, error)
AddProfile(groupid ProfileGroupID, profile *model.Profile) error
}
// NewProfileStore creates a new storage class and file for storing profile data
// block size should be as big as you can image a block ever needing to be, there are no current mechanisms for resizing
// numBlocks should be even, first half for profileGroups and second half for deniable profileGroups
func NewProfileStore(filepath string, blocksize int, numBlocks int, numGroups int) ProfileStore {
ps := new(profileStore)
ps.dataBlocksize = blocksize
ps.fullBlocksize = ps.dataBlocksize + HeaderSize
ps.numBlocks = numBlocks
ps.numGroups = numGroups
ps.profileGroups = make([]profileGroup, numGroups)
ps.filename = filepath
if _, err := os.Stat(ps.filename); os.IsNotExist(err) {
err := ps.generateFile()
if err != nil {
return nil
}
}
return ps
}
// generateFile creates a storage backing for a profileStore in a file
// format is [rand*blockSize]*numBlocks
// which yeilds a file of numBlocks blocks of initially random data of blockSize
// As profileGroups are created the blocks can be populated in a semi deniable of use fashion
// (the fact the file exists indicates at least 1 profile, but no way beyond that to determine exactly how many)
func (ps *profileStore) generateFile() error {
ps.mutex.Lock()
defer ps.mutex.Unlock()
file, err := os.OpenFile(ps.filename, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Printf("Error opening profile file for writting: %v", err)
return err
}
// Generate and write NumProfileBlocks blocks
for i := 0; i < ps.numBlocks; i++ {
padding := make([]byte, ps.fullBlocksize)
_, err := rand.Read(padding)
if err != nil {
log.Printf("Error: could not read from Rand to fill profile padding: %v", err)
return err
}
file.Write(padding[:])
}
file.Close()
return nil
}
// InitializeProfileGroup will set a password, attempt to load profile in that group, and then allow new profiles to be
// added to the group and saved under it
func (ps *profileStore) InitializeProfileGroup(groupid ProfileGroupID, password string) ([]*model.Profile, error) {
ps.mutex.Lock()
pg := &ps.profileGroups[groupid]
if len(pg.profiles) != 0 {
ps.mutex.Unlock()
return nil, errors.New("ProfileGroup already has loaded profiles, cannot reinitialize")
}
pg.password = password
ps.mutex.Unlock()
err := ps.attemptLoad(groupid)
return ps.profileGroups[groupid].profiles, err
}
func (ps *profileStore) getSaveFn(groupid ProfileGroupID, slot int) func() error {
return func() error {
ps.mutex.Lock()
defer ps.mutex.Unlock()
file, err := os.OpenFile(ps.filename, os.O_RDWR, 0600)
if err != nil {
log.Printf("Error opening ProfileStore file for saving: %v", err)
return err
}
absoluteSlot := (ps.numBlocks/ps.numGroups)*int(groupid) + slot
_, err = file.Seek(int64(ps.fullBlocksize*absoluteSlot), 0)
if err != nil {
fmt.Printf("Seek error: %v\n", err)
}
encryptedByte := ps.profileGroups[groupid].profiles[slot].EncryptProfile(ps.dataBlocksize)
_, err = file.Write(encryptedByte)
if err != nil {
log.Printf("Error writting to profile store file: %v\n", err)
}
file.Close()
return nil
}
}
func (ps *profileStore) attemptLoad(groupid ProfileGroupID) error {
ps.mutex.Lock()
defer ps.mutex.Unlock()
file, err := os.Open(ps.filename)
if err != nil {
log.Printf("Error opening profile store file: %v", err)
return err
}
defer file.Close()
file.Seek(int64(ps.fullBlocksize*(int(groupid)*(ps.numBlocks/ps.numGroups))), 0)
for i := 0; i < ps.numBlocks/ps.numGroups; i++ {
encryptedBytes := make([]byte, ps.fullBlocksize)
_, err := file.Read(encryptedBytes[:])
if err != nil {
log.Printf("Error reading from profile store file: %v", err)
return err
}
profile := model.AttemptLoadProfile(encryptedBytes[:], ps.profileGroups[groupid].password, ps.getSaveFn(groupid, i))
if profile == nil {
// no more profiles to load
break
}
ps.profileGroups[groupid].profiles = append(ps.profileGroups[groupid].profiles, profile)
}
return nil
}
// createKey derives a key and salt for use in encrypting cwtchPeers
func createKey(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
var dkr [32]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
fmt.Printf("Error: Could not read Rand for salt: %v", err)
return dkr, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
copy(dkr[:], dk)
return dkr, salt, nil
}
// AddProfile adds a profile to a group, and wires it in to be Save()able: gives it a key, salt and save function
// then it is saved
func (ps *profileStore) AddProfile(groupid ProfileGroupID, profile *model.Profile) error {
ps.mutex.Lock()
key, salt, err := createKey(ps.profileGroups[groupid].password)
if err != nil {
return err
}
profile.OnProfileStoreAdd(key, salt, ps.getSaveFn(groupid, len(ps.profileGroups[groupid].profiles)))
ps.profileGroups[groupid].profiles = append(ps.profileGroups[groupid].profiles, profile)
ps.mutex.Unlock()
profile.Save()
return nil
}