forked from TrueCloudLab/frostfs-mfa
411 lines
11 KiB
Go
411 lines
11 KiB
Go
|
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
|
||
|
}
|