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 }