package mfa import ( "bytes" "context" "errors" "fmt" "strings" "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/pquerna/otp" "go.uber.org/zap" ) type ( // Manager provides interface to manage MFA devices in FrostFS container. // It should be provided with Storage interface to manage FrostFS objects // and KeyStore interface to encode and decode OTP keys inside FrostFS // objects. Manager struct { storage Storage unlocker KeyStore container cid.ID logger *zap.Logger } // KeyStore is an interface for Manager to provide keys to encode and decode // OTP keys of MFA devices. KeyStore interface { // PrivateKey returns private key of this Manager. PrivateKey() *keys.PrivateKey // PublicKeys returns list of public keys for all managers, including // this Manager. PublicKeys() []*keys.PublicKey } // Config contains parameters for Manager constructor. Config struct { Storage Storage Unlocker KeyStore Container cid.ID Logger *zap.Logger } ) // NewManager creates new instance of Manager. func NewManager(cfg Config) (*Manager, error) { if cfg.Storage == nil { return nil, errors.New("mfa storage is nil") } if cfg.Logger == nil { return nil, errors.New("mfa logger is nil") } if cfg.Unlocker == nil { return nil, errors.New("mfa key store is nil") } return &Manager{ storage: cfg.Storage, container: cfg.Container, unlocker: cfg.Unlocker, logger: cfg.Logger, }, nil } // CreateMFADevice creates new FrostFS object with encoded MFA device inside and stores it in MFA container. func (m *Manager) CreateMFADevice(ctx context.Context, device SecretDevice) error { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.CreateMFADevice") defer span.End() filePath := getTreePath(device.Namespace, device.Name) if _, err := m.storage.GetTreeNode(ctx, m.container, filePath); err == nil { return ErrMFAEntityAlreadyExists // return well known error here } else if !errors.Is(err, ErrTreeNodeNotFound) { return fmt.Errorf("get mfa node '%s' to check: %w", filePath, err) } return m.putMFADevice(ctx, device) } // GetMFADevice returns decoded MFA device from MFA container. func (m *Manager) GetMFADevice(ctx context.Context, ns, mfaName string) (*SecretDevice, error) { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetMFADevice") defer span.End() node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName)) if err != nil { return nil, fmt.Errorf("get mfa nodes: %w", err) } var objID oid.ID if err = objID.DecodeString(node.Current.Meta[OIDKey]); err != nil { return nil, fmt.Errorf("decode oid '%s': %w", node.Current.Meta[OIDKey], err) } var addr oid.Address addr.SetContainer(m.container) addr.SetObject(objID) boxData, err := m.storage.GetObject(ctx, addr) if err != nil { return nil, fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err) } mfaBox := new(MFABox) if err = mfaBox.Unmarshal(boxData); err != nil { return nil, fmt.Errorf("unmarshal box data: %w", err) } secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey()) if err != nil { return nil, fmt.Errorf("unpack mfa box: %w", err) } key, err := otp.NewKeyFromURL(secrets.URL()) if err != nil { return nil, err } dev, err := newDevice(&node.Current) if err != nil { return nil, err } return &SecretDevice{ Device: *dev, Key: key, }, nil } // GetTinyMFADevice returns MFA device metadata without OTP key from the tree of MFA container. func (m *Manager) GetTinyMFADevice(ctx context.Context, ns, mfaName string) (*Device, error) { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetTinyMFADevice") defer span.End() node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName)) if err != nil { return nil, fmt.Errorf("get mfa nodes: %w", err) } dev, err := newDevice(&node.Current) if err != nil { return nil, err } return dev, nil } // ListMFADevices lists all available MFA device metadata with specified device namespace from the tree of MFA container. func (m *Manager) ListMFADevices(ctx context.Context, ns string) ([]*Device, error) { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListMFADevices") defer span.End() list, err := m.storage.GetTreeNodes(ctx, m.container, ns) if err != nil { return nil, err } return m.formDevices(list) } // ListAllMFADevices lists all available MFA device metadata from the tree of MFA container. func (m *Manager) ListAllMFADevices(ctx context.Context) ([]*Device, error) { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListAllMFADevices") defer span.End() list, err := m.storage.GetTreeNodes(ctx, m.container, "") if err != nil { return nil, err } return m.formDevices(list) } // UpdateMFADevice updates MFA device metadata in the tree of MFA container. func (m *Manager) UpdateMFADevice(ctx context.Context, device *Device) error { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.UpdateMFADevice") defer span.End() node, err := m.storage.SetTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name), device.Meta) if err != nil { return fmt.Errorf("set mfa tree node : %w", err) } m.deleteObjects(ctx, node.Old) return nil } // DeleteMFADevice removes FrostFS object and metadata from the tree of MFA container related to device. func (m *Manager) DeleteMFADevice(ctx context.Context, device *Device) error { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.DeleteMFADevice") defer span.End() meta := device.Meta var objID oid.ID if err := objID.DecodeString(meta[OIDKey]); err != nil { return fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err) } address := oid.Address{} address.SetObject(objID) address.SetContainer(m.container) if err := m.storage.DeleteObject(ctx, address); err != nil { return fmt.Errorf("failed to delete from storage: %w", err) } old, err := m.storage.DeleteTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name)) if err != nil { m.deleteObjects(ctx, old) return fmt.Errorf("failed to delete from tree: %w", err) } return nil } // RecipherDevice creates new FrostFS object with encoded OTP key for new set // of public keys from KeyManager. If existing FrostFS object already contains // all public keys from KeyManager, then does nothing. func (m *Manager) RecipherDevice(ctx context.Context, dev *Device) error { ctx, span := tracing.StartSpanFromContext(ctx, "mfa.RecipherDevice") defer span.End() node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(dev.Namespace, dev.Name)) if err != nil { return fmt.Errorf("get mfa nodes: %w", err) } if dev.OID.EncodeToString() != node.Current.Meta[OIDKey] { // This can happen in the following cases: // * device was reciphered by other Manager // * device was removed and newly created with the same name // in both cases it must be already encrypted for new list of keys return nil } var addr oid.Address addr.SetContainer(m.container) addr.SetObject(dev.OID) boxData, err := m.storage.GetObject(ctx, addr) if err != nil { return fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err) } mfaBox := new(MFABox) if err = mfaBox.Unmarshal(boxData); err != nil { return fmt.Errorf("unmarshal box data: %w", err) } newKeys := m.unlocker.PublicKeys() oldKeys := mfaBox.GetUnlockers() if equalKeys(newKeys, oldKeys) { m.logger.Info("mfabox has already been reciphered", zap.String("device", dev.String()), zap.String("OID", dev.OID.EncodeToString())) return nil } secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey()) if err != nil { return fmt.Errorf("unpack mfa box: %w", err) } key, err := otp.NewKeyFromURL(secrets.URL()) if err != nil { return err } return m.putMFADevice(ctx, SecretDevice{ Device: *dev, Key: key, }) } func (m *Manager) putMFADevice(ctx context.Context, device SecretDevice) error { filePath := "/" + device.Namespace + "/" + device.Name box, err := PackMFABox(device.Key, m.unlocker.PublicKeys()) if err != nil { return fmt.Errorf("pack mfa box: %w", err) } boxData, err := box.Marshal() if err != nil { return fmt.Errorf("marshal mfa box: %w", err) } var owner user.ID user.IDFromKey(&owner, m.unlocker.PrivateKey().PrivateKey.PublicKey) objID, err := m.storage.CreateObject(ctx, PrmObjectCreate{ Container: m.container, Owner: owner, FilePath: filePath, Payload: boxData, }) if err != nil { return fmt.Errorf("put mfa box object: %w", err) } meta := map[string]string{ FilePathKey: filePath, OIDKey: objID.EncodeToString(), } for k, v := range device.Meta { if k != FilePathKey && k != OIDKey { meta[k] = v } } node, err := m.storage.SetTreeNode(ctx, m.container, filePath, meta) if err != nil { return fmt.Errorf("set mfa tree node : %w", err) } m.deleteObjects(ctx, node.Old) m.deleteObject(ctx, device.OID) return nil } func (m *Manager) formDevices(list []*TreeNode) ([]*Device, error) { res := make([]*Device, 0, len(list)) for _, item := range list { dev, err := newDevice(item) if err != nil { m.logger.Warn("invalid mfa device tree node", zap.Error(err)) continue } res = append(res, dev) } return res, nil } func (m *Manager) deleteObjects(ctx context.Context, nodes []*TreeNode) { var addr oid.Address addr.SetContainer(m.container) for _, node := range nodes { var objID oid.ID if err := objID.DecodeString(node.Meta[OIDKey]); err != nil { m.logger.Warn("failed to decode old multinode oid", zap.String("oid", node.Meta[OIDKey]), zap.Error(err)) } addr.SetObject(objID) if err := m.storage.DeleteObject(ctx, addr); err != nil { m.logger.Warn("failed to delete old multinode object", zap.String("address", addr.EncodeToString()), zap.Error(err)) } } } func (m *Manager) deleteObject(ctx context.Context, objID oid.ID) { if objID.Equals(oid.ID{}) { return } var addr oid.Address addr.SetContainer(m.container) addr.SetObject(objID) if err := m.storage.DeleteObject(ctx, addr); err != nil { m.logger.Warn("failed to delete object", zap.String("address", addr.EncodeToString()), zap.Error(err)) } } func newDevice(node *TreeNode) (*Device, error) { meta := node.Meta filepathArr := strings.Split(meta[FilePathKey], "/") if len(filepathArr) != 2 { return nil, fmt.Errorf("invalid device filepath: '%s'", meta[FilePathKey]) } var objID oid.ID if err := objID.DecodeString(meta[OIDKey]); err != nil { return nil, fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err) } return &Device{ Namespace: filepathArr[0], Name: filepathArr[1], OID: objID, Meta: meta, }, nil } // equalKeys returns true if 'got' contains all public keys from 'expected' slice. func equalKeys(expected []*keys.PublicKey, got []*Unlocker) bool { loop: for _, newKey := range expected { for _, svcKey := range got { if bytes.Equal(newKey.Bytes(), svcKey.GetPublicKey()) { continue loop } } return false } return true } func getTreePath(ns, mfaName string) string { return ns + "/" + mfaName }