443 lines
13 KiB
Go
443 lines
13 KiB
Go
package list
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/anyproto/any-sync/app/logger"
|
|
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
|
"github.com/anyproto/any-sync/util/crypto"
|
|
"github.com/gogo/protobuf/proto"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var log = logger.NewNamedSugared("common.commonspace.acllist")
|
|
|
|
var (
|
|
ErrNoSuchAccount = errors.New("no such account")
|
|
ErrPendingRequest = errors.New("already exists pending request")
|
|
ErrUnexpectedContentType = errors.New("unexpected content type")
|
|
ErrIncorrectIdentity = errors.New("incorrect identity")
|
|
ErrIncorrectInviteKey = errors.New("incorrect invite key")
|
|
ErrFailedToDecrypt = errors.New("failed to decrypt key")
|
|
ErrNoSuchRecord = errors.New("no such record")
|
|
ErrNoSuchRequest = errors.New("no such request")
|
|
ErrNoSuchInvite = errors.New("no such invite")
|
|
ErrInsufficientPermissions = errors.New("insufficient permissions")
|
|
ErrIsOwner = errors.New("can't be made by owner")
|
|
ErrIncorrectNumberOfAccounts = errors.New("incorrect number of accounts")
|
|
ErrNoReadKey = errors.New("acl state doesn't have a read key")
|
|
ErrIncorrectReadKey = errors.New("incorrect read key")
|
|
ErrInvalidSignature = errors.New("signature is invalid")
|
|
ErrIncorrectRoot = errors.New("incorrect root")
|
|
ErrIncorrectRecordSequence = errors.New("incorrect prev id of a record")
|
|
)
|
|
|
|
type UserPermissionPair struct {
|
|
Identity crypto.PubKey
|
|
Permission aclrecordproto.AclUserPermissions
|
|
}
|
|
|
|
type AclState struct {
|
|
id string
|
|
currentReadKeyId string
|
|
// userReadKeys is a map recordId -> read key which tells us about every read key
|
|
userReadKeys map[string]crypto.SymKey
|
|
// userStates is a map pubKey -> state which defines current user state
|
|
userStates map[string]AclUserState
|
|
// statesAtRecord is a map recordId -> state which define user state at particular record
|
|
// probably this can grow rather large at some point, so we can maybe optimise later to have:
|
|
// - map pubKey -> []recordIds (where recordIds is an array where such identity permissions were changed)
|
|
statesAtRecord map[string][]AclUserState
|
|
// inviteKeys is a map recordId -> invite
|
|
inviteKeys map[string]crypto.PubKey
|
|
// requestRecords is a map recordId -> RequestRecord
|
|
requestRecords map[string]RequestRecord
|
|
// pendingRequests is a map pubKey -> RequestType
|
|
pendingRequests map[string]RequestType
|
|
key crypto.PrivKey
|
|
pubKey crypto.PubKey
|
|
keyStore crypto.KeyStorage
|
|
totalReadKeys int
|
|
|
|
lastRecordId string
|
|
contentValidator ContentValidator
|
|
}
|
|
|
|
func newAclStateWithKeys(
|
|
id string,
|
|
key crypto.PrivKey) (*AclState, error) {
|
|
st := &AclState{
|
|
id: id,
|
|
key: key,
|
|
pubKey: key.GetPublic(),
|
|
userReadKeys: make(map[string]crypto.SymKey),
|
|
userStates: make(map[string]AclUserState),
|
|
statesAtRecord: make(map[string][]AclUserState),
|
|
inviteKeys: make(map[string]crypto.PubKey),
|
|
requestRecords: make(map[string]RequestRecord),
|
|
pendingRequests: make(map[string]RequestType),
|
|
keyStore: crypto.NewKeyStorage(),
|
|
}
|
|
st.contentValidator = &contentValidator{
|
|
keyStore: st.keyStore,
|
|
aclState: st,
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
func newAclState(id string) *AclState {
|
|
st := &AclState{
|
|
id: id,
|
|
userReadKeys: make(map[string]crypto.SymKey),
|
|
userStates: make(map[string]AclUserState),
|
|
statesAtRecord: make(map[string][]AclUserState),
|
|
inviteKeys: make(map[string]crypto.PubKey),
|
|
requestRecords: make(map[string]RequestRecord),
|
|
pendingRequests: make(map[string]RequestType),
|
|
keyStore: crypto.NewKeyStorage(),
|
|
}
|
|
st.contentValidator = &contentValidator{
|
|
keyStore: st.keyStore,
|
|
aclState: st,
|
|
}
|
|
return st
|
|
}
|
|
|
|
func (st *AclState) Validator() ContentValidator {
|
|
return st.contentValidator
|
|
}
|
|
|
|
func (st *AclState) CurrentReadKeyId() string {
|
|
return st.currentReadKeyId
|
|
}
|
|
|
|
func (st *AclState) CurrentReadKey() (crypto.SymKey, error) {
|
|
key, exists := st.userReadKeys[st.currentReadKeyId]
|
|
if !exists {
|
|
return nil, ErrNoReadKey
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func (st *AclState) UserReadKeys() map[string]crypto.SymKey {
|
|
return st.userReadKeys
|
|
}
|
|
|
|
func (st *AclState) StateAtRecord(id string, pubKey crypto.PubKey) (AclUserState, error) {
|
|
userState, ok := st.statesAtRecord[id]
|
|
if !ok {
|
|
log.Errorf("missing record at id %s", id)
|
|
return AclUserState{}, ErrNoSuchRecord
|
|
}
|
|
|
|
for _, perm := range userState {
|
|
if perm.PubKey.Equals(pubKey) {
|
|
return perm, nil
|
|
}
|
|
}
|
|
return AclUserState{}, ErrNoSuchAccount
|
|
}
|
|
|
|
func (st *AclState) applyRecord(record *AclRecord) (err error) {
|
|
defer func() {
|
|
if err == nil {
|
|
st.lastRecordId = record.Id
|
|
}
|
|
}()
|
|
if st.lastRecordId != record.PrevId {
|
|
err = ErrIncorrectRecordSequence
|
|
return
|
|
}
|
|
// if the record is root record
|
|
if record.Id == st.id {
|
|
err = st.applyRoot(record)
|
|
if err != nil {
|
|
return
|
|
}
|
|
st.statesAtRecord[record.Id] = []AclUserState{
|
|
st.userStates[mapKeyFromPubKey(record.Identity)],
|
|
}
|
|
return
|
|
}
|
|
// if the model is not cached
|
|
if record.Model == nil {
|
|
aclData := &aclrecordproto.AclData{}
|
|
err = proto.Unmarshal(record.Data, aclData)
|
|
if err != nil {
|
|
return
|
|
}
|
|
record.Model = aclData
|
|
}
|
|
// applying records contents
|
|
err = st.applyChangeData(record)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// getting all states for users at record and saving them
|
|
var states []AclUserState
|
|
for _, state := range st.userStates {
|
|
states = append(states, state)
|
|
}
|
|
st.statesAtRecord[record.Id] = states
|
|
return
|
|
}
|
|
|
|
func (st *AclState) applyRoot(record *AclRecord) (err error) {
|
|
if st.key != nil && st.pubKey.Equals(record.Identity) {
|
|
err = st.saveReadKeyFromRoot(record)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// adding user to the list
|
|
userState := AclUserState{
|
|
PubKey: record.Identity,
|
|
Permissions: AclPermissions(aclrecordproto.AclUserPermissions_Owner),
|
|
}
|
|
st.currentReadKeyId = record.Id
|
|
st.userStates[mapKeyFromPubKey(record.Identity)] = userState
|
|
st.totalReadKeys++
|
|
return
|
|
}
|
|
|
|
func (st *AclState) saveReadKeyFromRoot(record *AclRecord) (err error) {
|
|
var readKey crypto.SymKey
|
|
root, ok := record.Model.(*aclrecordproto.AclRoot)
|
|
if !ok {
|
|
return ErrIncorrectRoot
|
|
}
|
|
if root.EncryptedReadKey == nil {
|
|
readKey, err = st.deriveKey()
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
readKey, err = st.decryptReadKey(root.EncryptedReadKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
st.userReadKeys[record.Id] = readKey
|
|
return
|
|
}
|
|
|
|
func (st *AclState) applyChangeData(record *AclRecord) (err error) {
|
|
model := record.Model.(*aclrecordproto.AclData)
|
|
for _, ch := range model.GetAclContent() {
|
|
if err = st.applyChangeContent(ch, record.Id, record.Identity); err != nil {
|
|
log.Info("error while applying changes: %v; ignore", zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyChangeContent(ch *aclrecordproto.AclContentValue, recordId string, authorIdentity crypto.PubKey) error {
|
|
switch {
|
|
case ch.GetPermissionChange() != nil:
|
|
return st.applyPermissionChange(ch.GetPermissionChange(), recordId, authorIdentity)
|
|
case ch.GetInvite() != nil:
|
|
return st.applyInvite(ch.GetInvite(), recordId, authorIdentity)
|
|
case ch.GetInviteRevoke() != nil:
|
|
return st.applyInviteRevoke(ch.GetInviteRevoke(), recordId, authorIdentity)
|
|
case ch.GetRequestJoin() != nil:
|
|
return st.applyRequestJoin(ch.GetRequestJoin(), recordId, authorIdentity)
|
|
case ch.GetRequestAccept() != nil:
|
|
return st.applyRequestAccept(ch.GetRequestAccept(), recordId, authorIdentity)
|
|
case ch.GetRequestDecline() != nil:
|
|
return st.applyRequestDecline(ch.GetRequestDecline(), recordId, authorIdentity)
|
|
case ch.GetAccountRemove() != nil:
|
|
return st.applyAccountRemove(ch.GetAccountRemove(), recordId, authorIdentity)
|
|
case ch.GetReadKeyChange() != nil:
|
|
return st.applyReadKeyChange(ch.GetReadKeyChange(), recordId, authorIdentity)
|
|
case ch.GetAccountRequestRemove() != nil:
|
|
return st.applyRequestRemove(ch.GetAccountRequestRemove(), recordId, authorIdentity)
|
|
default:
|
|
return ErrUnexpectedContentType
|
|
}
|
|
}
|
|
|
|
func (st *AclState) applyPermissionChange(ch *aclrecordproto.AclAccountPermissionChange, recordId string, authorIdentity crypto.PubKey) error {
|
|
chIdentity, err := st.keyStore.PubKeyFromProto(ch.Identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = st.contentValidator.ValidatePermissionChange(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stringKey := mapKeyFromPubKey(chIdentity)
|
|
state, _ := st.userStates[stringKey]
|
|
state.Permissions = AclPermissions(ch.Permissions)
|
|
st.userStates[stringKey] = state
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyInvite(ch *aclrecordproto.AclAccountInvite, recordId string, authorIdentity crypto.PubKey) error {
|
|
inviteKey, err := st.keyStore.PubKeyFromProto(ch.InviteKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = st.contentValidator.ValidateInvite(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.inviteKeys[recordId] = inviteKey
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyInviteRevoke(ch *aclrecordproto.AclAccountInviteRevoke, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateInviteRevoke(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(st.inviteKeys, ch.InviteRecordId)
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyRequestJoin(ch *aclrecordproto.AclAccountRequestJoin, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateRequestJoin(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.pendingRequests[mapKeyFromPubKey(authorIdentity)] = RequestTypeJoin
|
|
st.requestRecords[recordId] = RequestRecord{
|
|
RequestIdentity: authorIdentity,
|
|
RequestMetadata: ch.Metadata,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyRequestAccept(ch *aclrecordproto.AclAccountRequestAccept, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateRequestAccept(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acceptIdentity, err := st.keyStore.PubKeyFromProto(ch.Identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record, _ := st.requestRecords[ch.RequestRecordId]
|
|
st.userStates[mapKeyFromPubKey(acceptIdentity)] = AclUserState{
|
|
PubKey: acceptIdentity,
|
|
Permissions: AclPermissions(ch.Permissions),
|
|
RequestMetadata: record.RequestMetadata,
|
|
}
|
|
delete(st.pendingRequests, mapKeyFromPubKey(st.requestRecords[ch.RequestRecordId].RequestIdentity))
|
|
if !st.pubKey.Equals(acceptIdentity) {
|
|
return nil
|
|
}
|
|
for _, key := range ch.EncryptedReadKeys {
|
|
decrypted, err := st.key.Decrypt(key.EncryptedReadKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sym, err := crypto.UnmarshallAESKey(decrypted)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.userReadKeys[key.RecordId] = sym
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyRequestDecline(ch *aclrecordproto.AclAccountRequestDecline, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateRequestDecline(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(st.pendingRequests, mapKeyFromPubKey(st.requestRecords[ch.RequestRecordId].RequestIdentity))
|
|
delete(st.requestRecords, ch.RequestRecordId)
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyRequestRemove(ch *aclrecordproto.AclAccountRequestRemove, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateRequestRemove(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.pendingRequests[mapKeyFromPubKey(authorIdentity)] = RequestTypeRemove
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) applyAccountRemove(ch *aclrecordproto.AclAccountRemove, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateAccountRemove(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, rawIdentity := range ch.Identities {
|
|
identity, err := st.keyStore.PubKeyFromProto(rawIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
idKey := mapKeyFromPubKey(identity)
|
|
delete(st.userStates, idKey)
|
|
delete(st.pendingRequests, idKey)
|
|
}
|
|
return st.updateReadKey(ch.AccountKeys, recordId)
|
|
}
|
|
|
|
func (st *AclState) applyReadKeyChange(ch *aclrecordproto.AclReadKeyChange, recordId string, authorIdentity crypto.PubKey) error {
|
|
err := st.contentValidator.ValidateReadKeyChange(ch, authorIdentity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return st.updateReadKey(ch.AccountKeys, recordId)
|
|
}
|
|
|
|
func (st *AclState) updateReadKey(keys []*aclrecordproto.AclEncryptedReadKey, recordId string) error {
|
|
for _, accKey := range keys {
|
|
identity, _ := st.keyStore.PubKeyFromProto(accKey.Identity)
|
|
if st.pubKey.Equals(identity) {
|
|
res, err := st.decryptReadKey(accKey.EncryptedReadKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.userReadKeys[recordId] = res
|
|
}
|
|
}
|
|
st.currentReadKeyId = recordId
|
|
return nil
|
|
}
|
|
|
|
func (st *AclState) decryptReadKey(msg []byte) (crypto.SymKey, error) {
|
|
decrypted, err := st.key.Decrypt(msg)
|
|
if err != nil {
|
|
return nil, ErrFailedToDecrypt
|
|
}
|
|
key, err := crypto.UnmarshallAESKey(decrypted)
|
|
if err != nil {
|
|
return nil, ErrFailedToDecrypt
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func (st *AclState) Permissions(identity crypto.PubKey) AclPermissions {
|
|
state, exists := st.userStates[mapKeyFromPubKey(identity)]
|
|
if !exists {
|
|
return AclPermissions(aclrecordproto.AclUserPermissions_None)
|
|
}
|
|
return state.Permissions
|
|
}
|
|
|
|
func (st *AclState) UserStates() map[string]AclUserState {
|
|
return st.userStates
|
|
}
|
|
|
|
func (st *AclState) LastRecordId() string {
|
|
return st.lastRecordId
|
|
}
|
|
|
|
func (st *AclState) deriveKey() (crypto.SymKey, error) {
|
|
keyBytes, err := st.key.Raw()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return crypto.DeriveSymmetricKey(keyBytes, crypto.AnysyncSpacePath)
|
|
}
|
|
|
|
func mapKeyFromPubKey(pubKey crypto.PubKey) string {
|
|
return string(pubKey.Storage())
|
|
}
|