forked from cwtch.im/cwtch
251 lines
8.2 KiB
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
|
|
}
|