Merge branch 'worktree-agent-a98b5183'
# Conflicts: # cmd/metacrypt/server.go # internal/grpcserver/server.go # internal/server/routes.go
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
|||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/user"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/grpcserver"
|
"git.wntrmute.dev/kyle/metacrypt/internal/grpcserver"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||||
@@ -78,6 +79,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
||||||
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
|
||||||
engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
|
||||||
|
engineRegistry.RegisterFactory(engine.EngineTypeUser, user.NewUserEngine)
|
||||||
|
|
||||||
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
|
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
|
||||||
grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger)
|
grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger)
|
||||||
|
|||||||
1109
gen/metacrypt/v2/user.pb.go
Normal file
1109
gen/metacrypt/v2/user.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
451
gen/metacrypt/v2/user_grpc.pb.go
Normal file
451
gen/metacrypt/v2/user_grpc.pb.go
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v3.20.3
|
||||||
|
// source: proto/metacrypt/v2/user.proto
|
||||||
|
|
||||||
|
package metacryptv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserService_Register_FullMethodName = "/metacrypt.v2.UserService/Register"
|
||||||
|
UserService_Provision_FullMethodName = "/metacrypt.v2.UserService/Provision"
|
||||||
|
UserService_GetPublicKey_FullMethodName = "/metacrypt.v2.UserService/GetPublicKey"
|
||||||
|
UserService_ListUsers_FullMethodName = "/metacrypt.v2.UserService/ListUsers"
|
||||||
|
UserService_Encrypt_FullMethodName = "/metacrypt.v2.UserService/Encrypt"
|
||||||
|
UserService_Decrypt_FullMethodName = "/metacrypt.v2.UserService/Decrypt"
|
||||||
|
UserService_ReEncrypt_FullMethodName = "/metacrypt.v2.UserService/ReEncrypt"
|
||||||
|
UserService_RotateKey_FullMethodName = "/metacrypt.v2.UserService/RotateKey"
|
||||||
|
UserService_DeleteUser_FullMethodName = "/metacrypt.v2.UserService/DeleteUser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserServiceClient is the client API for UserService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// UserService provides typed, authenticated access to user-to-user encryption
|
||||||
|
// engine operations. All RPCs require the service to be unsealed and
|
||||||
|
// authentication.
|
||||||
|
type UserServiceClient interface {
|
||||||
|
// Register self-registers the caller, creating a keypair. No-op if exists.
|
||||||
|
Register(ctx context.Context, in *UserRegisterRequest, opts ...grpc.CallOption) (*UserRegisterResponse, error)
|
||||||
|
// Provision creates a keypair for a given username. Admin only.
|
||||||
|
Provision(ctx context.Context, in *UserProvisionRequest, opts ...grpc.CallOption) (*UserProvisionResponse, error)
|
||||||
|
// GetPublicKey returns the public key for a given username.
|
||||||
|
GetPublicKey(ctx context.Context, in *UserGetPublicKeyRequest, opts ...grpc.CallOption) (*UserGetPublicKeyResponse, error)
|
||||||
|
// ListUsers returns all registered usernames.
|
||||||
|
ListUsers(ctx context.Context, in *UserListUsersRequest, opts ...grpc.CallOption) (*UserListUsersResponse, error)
|
||||||
|
// Encrypt encrypts plaintext for one or more recipients.
|
||||||
|
Encrypt(ctx context.Context, in *UserEncryptRequest, opts ...grpc.CallOption) (*UserEncryptResponse, error)
|
||||||
|
// Decrypt decrypts an envelope addressed to the caller.
|
||||||
|
Decrypt(ctx context.Context, in *UserDecryptRequest, opts ...grpc.CallOption) (*UserDecryptResponse, error)
|
||||||
|
// ReEncrypt decrypts and re-encrypts an envelope with current keys.
|
||||||
|
ReEncrypt(ctx context.Context, in *UserReEncryptRequest, opts ...grpc.CallOption) (*UserReEncryptResponse, error)
|
||||||
|
// RotateKey generates a new keypair for the caller, replacing the old one.
|
||||||
|
RotateKey(ctx context.Context, in *UserRotateKeyRequest, opts ...grpc.CallOption) (*UserRotateKeyResponse, error)
|
||||||
|
// DeleteUser removes a user's keys. Admin only.
|
||||||
|
DeleteUser(ctx context.Context, in *UserDeleteUserRequest, opts ...grpc.CallOption) (*UserDeleteUserResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
|
||||||
|
return &userServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) Register(ctx context.Context, in *UserRegisterRequest, opts ...grpc.CallOption) (*UserRegisterResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserRegisterResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_Register_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) Provision(ctx context.Context, in *UserProvisionRequest, opts ...grpc.CallOption) (*UserProvisionResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserProvisionResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_Provision_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) GetPublicKey(ctx context.Context, in *UserGetPublicKeyRequest, opts ...grpc.CallOption) (*UserGetPublicKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserGetPublicKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_GetPublicKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) ListUsers(ctx context.Context, in *UserListUsersRequest, opts ...grpc.CallOption) (*UserListUsersResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserListUsersResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) Encrypt(ctx context.Context, in *UserEncryptRequest, opts ...grpc.CallOption) (*UserEncryptResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserEncryptResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_Encrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) Decrypt(ctx context.Context, in *UserDecryptRequest, opts ...grpc.CallOption) (*UserDecryptResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserDecryptResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_Decrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) ReEncrypt(ctx context.Context, in *UserReEncryptRequest, opts ...grpc.CallOption) (*UserReEncryptResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserReEncryptResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_ReEncrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) RotateKey(ctx context.Context, in *UserRotateKeyRequest, opts ...grpc.CallOption) (*UserRotateKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserRotateKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_RotateKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userServiceClient) DeleteUser(ctx context.Context, in *UserDeleteUserRequest, opts ...grpc.CallOption) (*UserDeleteUserResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UserDeleteUserResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserServiceServer is the server API for UserService service.
|
||||||
|
// All implementations must embed UnimplementedUserServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// UserService provides typed, authenticated access to user-to-user encryption
|
||||||
|
// engine operations. All RPCs require the service to be unsealed and
|
||||||
|
// authentication.
|
||||||
|
type UserServiceServer interface {
|
||||||
|
// Register self-registers the caller, creating a keypair. No-op if exists.
|
||||||
|
Register(context.Context, *UserRegisterRequest) (*UserRegisterResponse, error)
|
||||||
|
// Provision creates a keypair for a given username. Admin only.
|
||||||
|
Provision(context.Context, *UserProvisionRequest) (*UserProvisionResponse, error)
|
||||||
|
// GetPublicKey returns the public key for a given username.
|
||||||
|
GetPublicKey(context.Context, *UserGetPublicKeyRequest) (*UserGetPublicKeyResponse, error)
|
||||||
|
// ListUsers returns all registered usernames.
|
||||||
|
ListUsers(context.Context, *UserListUsersRequest) (*UserListUsersResponse, error)
|
||||||
|
// Encrypt encrypts plaintext for one or more recipients.
|
||||||
|
Encrypt(context.Context, *UserEncryptRequest) (*UserEncryptResponse, error)
|
||||||
|
// Decrypt decrypts an envelope addressed to the caller.
|
||||||
|
Decrypt(context.Context, *UserDecryptRequest) (*UserDecryptResponse, error)
|
||||||
|
// ReEncrypt decrypts and re-encrypts an envelope with current keys.
|
||||||
|
ReEncrypt(context.Context, *UserReEncryptRequest) (*UserReEncryptResponse, error)
|
||||||
|
// RotateKey generates a new keypair for the caller, replacing the old one.
|
||||||
|
RotateKey(context.Context, *UserRotateKeyRequest) (*UserRotateKeyResponse, error)
|
||||||
|
// DeleteUser removes a user's keys. Admin only.
|
||||||
|
DeleteUser(context.Context, *UserDeleteUserRequest) (*UserDeleteUserResponse, error)
|
||||||
|
mustEmbedUnimplementedUserServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedUserServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedUserServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedUserServiceServer) Register(context.Context, *UserRegisterRequest) (*UserRegisterResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Register not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) Provision(context.Context, *UserProvisionRequest) (*UserProvisionResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Provision not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) GetPublicKey(context.Context, *UserGetPublicKeyRequest) (*UserGetPublicKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetPublicKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) ListUsers(context.Context, *UserListUsersRequest) (*UserListUsersResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) Encrypt(context.Context, *UserEncryptRequest) (*UserEncryptResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Encrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) Decrypt(context.Context, *UserDecryptRequest) (*UserDecryptResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Decrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) ReEncrypt(context.Context, *UserReEncryptRequest) (*UserReEncryptResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ReEncrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) RotateKey(context.Context, *UserRotateKeyRequest) (*UserRotateKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *UserDeleteUserRequest) (*UserDeleteUserResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
|
||||||
|
func (UnimplementedUserServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to UserServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeUserServiceServer interface {
|
||||||
|
mustEmbedUnimplementedUserServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedUserServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&UserService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserRegisterRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).Register(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_Register_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).Register(ctx, req.(*UserRegisterRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_Provision_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserProvisionRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).Provision(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_Provision_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).Provision(ctx, req.(*UserProvisionRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_GetPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserGetPublicKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).GetPublicKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_GetPublicKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).GetPublicKey(ctx, req.(*UserGetPublicKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserListUsersRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).ListUsers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_ListUsers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).ListUsers(ctx, req.(*UserListUsersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserEncryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).Encrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_Encrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).Encrypt(ctx, req.(*UserEncryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserDecryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).Decrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_Decrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).Decrypt(ctx, req.(*UserDecryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_ReEncrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserReEncryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).ReEncrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_ReEncrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).ReEncrypt(ctx, req.(*UserReEncryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_RotateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserRotateKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).RotateKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_RotateKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).RotateKey(ctx, req.(*UserRotateKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UserDeleteUserRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserServiceServer).DeleteUser(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserService_DeleteUser_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserServiceServer).DeleteUser(ctx, req.(*UserDeleteUserRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "metacrypt.v2.UserService",
|
||||||
|
HandlerType: (*UserServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Register",
|
||||||
|
Handler: _UserService_Register_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Provision",
|
||||||
|
Handler: _UserService_Provision_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetPublicKey",
|
||||||
|
Handler: _UserService_GetPublicKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListUsers",
|
||||||
|
Handler: _UserService_ListUsers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Encrypt",
|
||||||
|
Handler: _UserService_Encrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Decrypt",
|
||||||
|
Handler: _UserService_Decrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ReEncrypt",
|
||||||
|
Handler: _UserService_ReEncrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateKey",
|
||||||
|
Handler: _UserService_RotateKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteUser",
|
||||||
|
Handler: _UserService_DeleteUser_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/metacrypt/v2/user.proto",
|
||||||
|
}
|
||||||
32
internal/engine/user/types.go
Normal file
32
internal/engine/user/types.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// UserConfig is the user engine configuration stored in the barrier.
|
||||||
|
type UserConfig struct {
|
||||||
|
KeyAlgorithm string `json:"key_algorithm"` // x25519, ecdh-p256, ecdh-p384
|
||||||
|
SymAlgorithm string `json:"sym_algorithm"` // aes256-gcm
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserKeyConfig is per-user key configuration stored in the barrier.
|
||||||
|
type UserKeyConfig struct {
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
AutoProvisioned bool `json:"auto_provisioned"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipientEntry holds the HKDF salt and wrapped DEK for one recipient.
|
||||||
|
type recipientEntry struct {
|
||||||
|
Salt string `json:"salt"` // base64(32-byte HKDF salt)
|
||||||
|
WrappedDEK string `json:"wrapped_dek"` // base64(nonce + encrypted DEK + tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envelope is the multi-recipient encrypted message format.
|
||||||
|
type envelope struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Sender string `json:"sender"`
|
||||||
|
SymAlgorithm string `json:"sym_algorithm"`
|
||||||
|
Ciphertext string `json:"ciphertext"` // base64(nonce + encrypted payload + tag)
|
||||||
|
Metadata string `json:"metadata,omitempty"`
|
||||||
|
Recipients map[string]*recipientEntry `json:"recipients"`
|
||||||
|
}
|
||||||
971
internal/engine/user/user.go
Normal file
971
internal/engine/user/user.go
Normal file
@@ -0,0 +1,971 @@
|
|||||||
|
// Package user implements the user-to-user encryption engine.
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxRecipients = 100
|
||||||
|
nonceSize = 12
|
||||||
|
keySize = 32
|
||||||
|
hkdfInfoPrefix = "metacrypt-user-v1:"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSealed = errors.New("user: engine is sealed")
|
||||||
|
ErrForbidden = errors.New("user: forbidden")
|
||||||
|
ErrUnauthorized = errors.New("user: authentication required")
|
||||||
|
ErrUserNotFound = errors.New("user: user not found")
|
||||||
|
ErrUserExists = errors.New("user: user already exists")
|
||||||
|
ErrTooMany = errors.New("user: too many recipients")
|
||||||
|
ErrInvalidEnvelope = errors.New("user: invalid envelope")
|
||||||
|
ErrRecipientNotFound = errors.New("user: recipient entry not found in envelope")
|
||||||
|
ErrDecryptionFailed = errors.New("user: decryption failed")
|
||||||
|
ErrInvalidAlgorithm = errors.New("user: unsupported algorithm")
|
||||||
|
ErrNoRecipients = errors.New("user: no recipients specified")
|
||||||
|
)
|
||||||
|
|
||||||
|
// userState holds in-memory state for a loaded user.
|
||||||
|
type userState struct {
|
||||||
|
privKey *ecdh.PrivateKey
|
||||||
|
pubKey *ecdh.PublicKey
|
||||||
|
config *UserKeyConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserEngine implements the user-to-user encryption engine.
|
||||||
|
type UserEngine struct {
|
||||||
|
barrier barrier.Barrier
|
||||||
|
config *UserConfig
|
||||||
|
users map[string]*userState
|
||||||
|
mountPath string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserEngine creates a new user engine instance.
|
||||||
|
func NewUserEngine() engine.Engine {
|
||||||
|
return &UserEngine{
|
||||||
|
users: make(map[string]*userState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) Type() engine.EngineType {
|
||||||
|
return engine.EngineTypeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error {
|
||||||
|
cfg := &UserConfig{
|
||||||
|
KeyAlgorithm: "x25519",
|
||||||
|
SymAlgorithm: "aes256-gcm",
|
||||||
|
}
|
||||||
|
if v, ok := config["key_algorithm"].(string); ok && v != "" {
|
||||||
|
cfg.KeyAlgorithm = v
|
||||||
|
}
|
||||||
|
if v, ok := config["sym_algorithm"].(string); ok && v != "" {
|
||||||
|
cfg.SymAlgorithm = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateKeyAlgorithm(cfg.KeyAlgorithm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg.SymAlgorithm != "aes256-gcm" {
|
||||||
|
return fmt.Errorf("user: unsupported symmetric algorithm: %s", cfg.SymAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("user: marshal config: %w", err)
|
||||||
|
}
|
||||||
|
if err := b.Put(ctx, mountPath+"config.json", data); err != nil {
|
||||||
|
return fmt.Errorf("user: store config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.barrier = b
|
||||||
|
e.config = cfg
|
||||||
|
e.mountPath = mountPath
|
||||||
|
e.users = make(map[string]*userState)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
e.barrier = b
|
||||||
|
e.mountPath = mountPath
|
||||||
|
|
||||||
|
// Load config.
|
||||||
|
data, err := b.Get(ctx, mountPath+"config.json")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("user: load config: %w", err)
|
||||||
|
}
|
||||||
|
var cfg UserConfig
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return fmt.Errorf("user: parse config: %w", err)
|
||||||
|
}
|
||||||
|
e.config = &cfg
|
||||||
|
e.users = make(map[string]*userState)
|
||||||
|
|
||||||
|
// Load all user keys.
|
||||||
|
prefix := mountPath + "users/"
|
||||||
|
paths, err := b.List(ctx, prefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil // no users yet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover unique usernames from paths like "alice/config.json", "alice/priv.key".
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, p := range paths {
|
||||||
|
parts := strings.SplitN(p, "/", 2)
|
||||||
|
if len(parts) > 0 && parts[0] != "" {
|
||||||
|
seen[parts[0]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for username := range seen {
|
||||||
|
if err := e.loadUser(ctx, username); err != nil {
|
||||||
|
return fmt.Errorf("user: load user %q: %w", username, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) Seal() error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
// Zeroize all private keys.
|
||||||
|
for _, u := range e.users {
|
||||||
|
if u.privKey != nil {
|
||||||
|
raw := u.privKey.Bytes()
|
||||||
|
crypto.Zeroize(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.users = nil
|
||||||
|
e.config = nil
|
||||||
|
e.barrier = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) HandleRequest(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
switch req.Operation {
|
||||||
|
case "register":
|
||||||
|
return e.handleRegister(ctx, req)
|
||||||
|
case "provision":
|
||||||
|
return e.handleProvision(ctx, req)
|
||||||
|
case "get-public-key":
|
||||||
|
return e.handleGetPublicKey(ctx, req)
|
||||||
|
case "list-users":
|
||||||
|
return e.handleListUsers(ctx, req)
|
||||||
|
case "encrypt":
|
||||||
|
return e.handleEncrypt(ctx, req)
|
||||||
|
case "decrypt":
|
||||||
|
return e.handleDecrypt(ctx, req)
|
||||||
|
case "re-encrypt":
|
||||||
|
return e.handleReEncrypt(ctx, req)
|
||||||
|
case "rotate-key":
|
||||||
|
return e.handleRotateKey(ctx, req)
|
||||||
|
case "delete-user":
|
||||||
|
return e.handleDeleteUser(ctx, req)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("user: unknown operation: %s", req.Operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Operation handlers ---
|
||||||
|
|
||||||
|
func (e *UserEngine) handleRegister(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
username := req.CallerInfo.Username
|
||||||
|
e.mu.RLock()
|
||||||
|
if u, ok := e.users[username]; ok {
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
e.mu.RUnlock()
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock.
|
||||||
|
if u, ok := e.users[username]; ok {
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := e.createUser(ctx, username, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleProvision(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !req.CallerInfo.IsAdmin {
|
||||||
|
return nil, ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := req.Data["username"].(string)
|
||||||
|
if username == "" {
|
||||||
|
return nil, fmt.Errorf("user: username is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
// No-op if exists.
|
||||||
|
if u, ok := e.users[username]; ok {
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := e.createUser(ctx, username, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleGetPublicKey(_ context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !req.CallerInfo.IsUser() {
|
||||||
|
return nil, ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := req.Data["username"].(string)
|
||||||
|
if username == "" {
|
||||||
|
return nil, fmt.Errorf("user: username is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
|
u, ok := e.users[username]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": u.config.Algorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleListUsers(_ context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !req.CallerInfo.IsUser() {
|
||||||
|
return nil, ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
|
usernames := make([]interface{}, 0, len(e.users))
|
||||||
|
for name := range e.users {
|
||||||
|
usernames = append(usernames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"users": usernames,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleEncrypt(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !req.CallerInfo.IsUser() {
|
||||||
|
return nil, ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, _ := req.Data["plaintext"].(string)
|
||||||
|
if plaintext == "" {
|
||||||
|
return nil, fmt.Errorf("user: plaintext is required")
|
||||||
|
}
|
||||||
|
metadata, _ := req.Data["metadata"].(string)
|
||||||
|
|
||||||
|
recipientNames, err := extractRecipients(req.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(recipientNames) == 0 {
|
||||||
|
return nil, ErrNoRecipients
|
||||||
|
}
|
||||||
|
if len(recipientNames) > maxRecipients {
|
||||||
|
return nil, ErrTooMany
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := req.CallerInfo.Username
|
||||||
|
|
||||||
|
// Policy check for each recipient.
|
||||||
|
if req.CheckPolicy != nil {
|
||||||
|
for _, r := range recipientNames {
|
||||||
|
resource := fmt.Sprintf("user/%s/recipient/%s", e.mountPath, r)
|
||||||
|
effect, matched := req.CheckPolicy(resource, "write")
|
||||||
|
if matched && effect == "deny" {
|
||||||
|
return nil, fmt.Errorf("user: forbidden: policy denies encryption to recipient %s", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
|
||||||
|
// Auto-provision sender if not registered.
|
||||||
|
if _, ok := e.users[sender]; !ok {
|
||||||
|
if _, err := e.createUser(ctx, sender, true); err != nil {
|
||||||
|
e.mu.Unlock()
|
||||||
|
return nil, fmt.Errorf("user: auto-provision sender: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-provision recipients without keys.
|
||||||
|
for _, r := range recipientNames {
|
||||||
|
if _, ok := e.users[r]; !ok {
|
||||||
|
if _, err := e.createUser(ctx, r, true); err != nil {
|
||||||
|
e.mu.Unlock()
|
||||||
|
return nil, fmt.Errorf("user: auto-provision recipient %s: %w", r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
senderState := e.users[sender]
|
||||||
|
recipientStates := make(map[string]*userState, len(recipientNames))
|
||||||
|
for _, r := range recipientNames {
|
||||||
|
recipientStates[r] = e.users[r]
|
||||||
|
}
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
// Generate random DEK.
|
||||||
|
dek := make([]byte, keySize)
|
||||||
|
if _, err := rand.Read(dek); err != nil {
|
||||||
|
return nil, fmt.Errorf("user: generate DEK: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(dek)
|
||||||
|
|
||||||
|
// Encrypt plaintext with DEK.
|
||||||
|
var aad []byte
|
||||||
|
if metadata != "" {
|
||||||
|
aad = []byte(metadata)
|
||||||
|
}
|
||||||
|
ct, err := encryptAESGCM(dek, []byte(plaintext), aad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: encrypt plaintext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap DEK for each recipient.
|
||||||
|
recipients := make(map[string]*recipientEntry, len(recipientNames))
|
||||||
|
for _, rName := range recipientNames {
|
||||||
|
rState := recipientStates[rName]
|
||||||
|
entry, wrapErr := wrapDEKForRecipient(senderState.privKey, rState.pubKey, dek, sender, rName)
|
||||||
|
if wrapErr != nil {
|
||||||
|
return nil, fmt.Errorf("user: wrap DEK for %s: %w", rName, wrapErr)
|
||||||
|
}
|
||||||
|
recipients[rName] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &envelope{
|
||||||
|
Version: 1,
|
||||||
|
Sender: sender,
|
||||||
|
SymAlgorithm: e.config.SymAlgorithm,
|
||||||
|
Ciphertext: base64.StdEncoding.EncodeToString(ct),
|
||||||
|
Metadata: metadata,
|
||||||
|
Recipients: recipients,
|
||||||
|
}
|
||||||
|
|
||||||
|
envJSON, err := json.Marshal(env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: marshal envelope: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envB64 := base64.StdEncoding.EncodeToString(envJSON)
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"envelope": envB64,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleDecrypt(_ context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
caller := req.CallerInfo.Username
|
||||||
|
|
||||||
|
envelopeB64, _ := req.Data["envelope"].(string)
|
||||||
|
if envelopeB64 == "" {
|
||||||
|
return nil, fmt.Errorf("user: envelope is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := parseEnvelope(envelopeB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self-only: caller must be a recipient.
|
||||||
|
entry, ok := env.Recipients[caller]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrRecipientNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
callerState, callerExists := e.users[caller]
|
||||||
|
senderState, senderExists := e.users[env.Sender]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
if !callerExists {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
if !senderExists {
|
||||||
|
return nil, fmt.Errorf("user: sender %q not found", env.Sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap DEK.
|
||||||
|
dek, err := unwrapDEK(callerState.privKey, senderState.pubKey, entry, env.Sender, caller)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: unwrap DEK: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(dek)
|
||||||
|
|
||||||
|
// Decrypt ciphertext.
|
||||||
|
ct, err := base64.StdEncoding.DecodeString(env.Ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidEnvelope
|
||||||
|
}
|
||||||
|
|
||||||
|
var aad []byte
|
||||||
|
if env.Metadata != "" {
|
||||||
|
aad = []byte(env.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := decryptAESGCM(dek, ct, aad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrDecryptionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"plaintext": string(plaintext),
|
||||||
|
"sender": env.Sender,
|
||||||
|
}
|
||||||
|
if env.Metadata != "" {
|
||||||
|
resp["metadata"] = env.Metadata
|
||||||
|
}
|
||||||
|
return &engine.Response{Data: resp}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleReEncrypt(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
caller := req.CallerInfo.Username
|
||||||
|
|
||||||
|
envelopeB64, _ := req.Data["envelope"].(string)
|
||||||
|
if envelopeB64 == "" {
|
||||||
|
return nil, fmt.Errorf("user: envelope is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := parseEnvelope(envelopeB64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self-only: caller must be a recipient.
|
||||||
|
entry, ok := env.Recipients[caller]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrRecipientNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
callerState, callerExists := e.users[caller]
|
||||||
|
senderState, senderExists := e.users[env.Sender]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
if !callerExists {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
if !senderExists {
|
||||||
|
return nil, fmt.Errorf("user: sender %q not found", env.Sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap DEK using old keys.
|
||||||
|
dek, err := unwrapDEK(callerState.privKey, senderState.pubKey, entry, env.Sender, caller)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: unwrap DEK: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(dek)
|
||||||
|
|
||||||
|
// Verify we can decrypt (validates envelope integrity).
|
||||||
|
ct, err := base64.StdEncoding.DecodeString(env.Ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidEnvelope
|
||||||
|
}
|
||||||
|
var aad []byte
|
||||||
|
if env.Metadata != "" {
|
||||||
|
aad = []byte(env.Metadata)
|
||||||
|
}
|
||||||
|
plaintext, err := decryptAESGCM(dek, ct, aad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrDecryptionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new DEK and re-encrypt.
|
||||||
|
newDEK := make([]byte, keySize)
|
||||||
|
if _, err := rand.Read(newDEK); err != nil {
|
||||||
|
return nil, fmt.Errorf("user: generate new DEK: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(newDEK)
|
||||||
|
|
||||||
|
newCT, err := encryptAESGCM(newDEK, plaintext, aad)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: re-encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-wrap for same recipients with current keys, using caller as new sender.
|
||||||
|
e.mu.RLock()
|
||||||
|
newRecipients := make(map[string]*recipientEntry, len(env.Recipients))
|
||||||
|
for rName := range env.Recipients {
|
||||||
|
rState, exists := e.users[rName]
|
||||||
|
if !exists {
|
||||||
|
e.mu.RUnlock()
|
||||||
|
return nil, fmt.Errorf("user: recipient %q not found for re-encrypt", rName)
|
||||||
|
}
|
||||||
|
re, wrapErr := wrapDEKForRecipient(callerState.privKey, rState.pubKey, newDEK, caller, rName)
|
||||||
|
if wrapErr != nil {
|
||||||
|
e.mu.RUnlock()
|
||||||
|
return nil, fmt.Errorf("user: re-wrap DEK for %s: %w", rName, wrapErr)
|
||||||
|
}
|
||||||
|
newRecipients[rName] = re
|
||||||
|
}
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
newEnv := &envelope{
|
||||||
|
Version: 1,
|
||||||
|
Sender: caller,
|
||||||
|
SymAlgorithm: env.SymAlgorithm,
|
||||||
|
Ciphertext: base64.StdEncoding.EncodeToString(newCT),
|
||||||
|
Metadata: env.Metadata,
|
||||||
|
Recipients: newRecipients,
|
||||||
|
}
|
||||||
|
|
||||||
|
envJSON, err := json.Marshal(newEnv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: marshal envelope: %w", err)
|
||||||
|
}
|
||||||
|
envB64 := base64.StdEncoding.EncodeToString(envJSON)
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"envelope": envB64,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleRotateKey(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
caller := req.CallerInfo.Username
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
oldState, ok := e.users[caller]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new keypair.
|
||||||
|
priv, err := generateKey(e.config.KeyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: rotate key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store new keys in barrier.
|
||||||
|
if err := e.storeUserKeys(ctx, caller, priv, oldState.config.AutoProvisioned); err != nil {
|
||||||
|
return nil, fmt.Errorf("user: rotate key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeroize old key.
|
||||||
|
oldRaw := oldState.privKey.Bytes()
|
||||||
|
crypto.Zeroize(oldRaw)
|
||||||
|
|
||||||
|
// Update in-memory state.
|
||||||
|
e.users[caller] = &userState{
|
||||||
|
privKey: priv,
|
||||||
|
pubKey: priv.PublicKey(),
|
||||||
|
config: &UserKeyConfig{
|
||||||
|
Algorithm: e.config.KeyAlgorithm,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
AutoProvisioned: oldState.config.AutoProvisioned,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pubB64 := base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes())
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"username": caller,
|
||||||
|
"public_key": pubB64,
|
||||||
|
"algorithm": e.config.KeyAlgorithm,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UserEngine) handleDeleteUser(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||||
|
if req.CallerInfo == nil {
|
||||||
|
return nil, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !req.CallerInfo.IsAdmin {
|
||||||
|
return nil, ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := req.Data["username"].(string)
|
||||||
|
if username == "" {
|
||||||
|
return nil, fmt.Errorf("user: username is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
oldState, ok := e.users[username]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeroize private key.
|
||||||
|
oldRaw := oldState.privKey.Bytes()
|
||||||
|
crypto.Zeroize(oldRaw)
|
||||||
|
|
||||||
|
// Delete from barrier.
|
||||||
|
prefix := e.mountPath + "users/" + username + "/"
|
||||||
|
paths, err := e.barrier.List(ctx, prefix)
|
||||||
|
if err == nil {
|
||||||
|
for _, p := range paths {
|
||||||
|
_ = e.barrier.Delete(ctx, prefix+p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(e.users, username)
|
||||||
|
|
||||||
|
return &engine.Response{Data: map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helpers ---
|
||||||
|
|
||||||
|
// createUser generates a new keypair and stores it. Caller must hold e.mu write lock.
|
||||||
|
func (e *UserEngine) createUser(ctx context.Context, username string, autoProvisioned bool) (*userState, error) {
|
||||||
|
priv, err := generateKey(e.config.KeyAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate key for %s: %w", username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.storeUserKeys(ctx, username, priv, autoProvisioned); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := &userState{
|
||||||
|
privKey: priv,
|
||||||
|
pubKey: priv.PublicKey(),
|
||||||
|
config: &UserKeyConfig{
|
||||||
|
Algorithm: e.config.KeyAlgorithm,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
AutoProvisioned: autoProvisioned,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
e.users[username] = u
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeUserKeys persists user key material to the barrier. Caller must hold e.mu write lock.
|
||||||
|
func (e *UserEngine) storeUserKeys(ctx context.Context, username string, priv *ecdh.PrivateKey, autoProvisioned bool) error {
|
||||||
|
prefix := e.mountPath + "users/" + username + "/"
|
||||||
|
|
||||||
|
// Store private key.
|
||||||
|
if err := e.barrier.Put(ctx, prefix+"priv.key", priv.Bytes()); err != nil {
|
||||||
|
return fmt.Errorf("store private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store public key.
|
||||||
|
if err := e.barrier.Put(ctx, prefix+"pub.key", priv.PublicKey().Bytes()); err != nil {
|
||||||
|
return fmt.Errorf("store public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store config.
|
||||||
|
cfg := &UserKeyConfig{
|
||||||
|
Algorithm: e.config.KeyAlgorithm,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
AutoProvisioned: autoProvisioned,
|
||||||
|
}
|
||||||
|
cfgData, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal key config: %w", err)
|
||||||
|
}
|
||||||
|
if err := e.barrier.Put(ctx, prefix+"config.json", cfgData); err != nil {
|
||||||
|
return fmt.Errorf("store key config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadUser loads a single user's keys from the barrier into memory. Caller must hold e.mu write lock.
|
||||||
|
func (e *UserEngine) loadUser(ctx context.Context, username string) error {
|
||||||
|
prefix := e.mountPath + "users/" + username + "/"
|
||||||
|
|
||||||
|
// Load config first to know the algorithm.
|
||||||
|
cfgData, err := e.barrier.Get(ctx, prefix+"config.json")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
var cfg UserKeyConfig
|
||||||
|
if err := json.Unmarshal(cfgData, &cfg); err != nil {
|
||||||
|
return fmt.Errorf("parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load private key.
|
||||||
|
privBytes, err := e.barrier.Get(ctx, prefix+"priv.key")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
curve, err := curveForAlgorithm(cfg.Algorithm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := curve.NewPrivateKey(privBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.users[username] = &userState{
|
||||||
|
privKey: priv,
|
||||||
|
pubKey: priv.PublicKey(),
|
||||||
|
config: &cfg,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cryptographic helpers ---
|
||||||
|
|
||||||
|
func generateKey(algorithm string) (*ecdh.PrivateKey, error) {
|
||||||
|
curve, err := curveForAlgorithm(algorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return curve.GenerateKey(rand.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func curveForAlgorithm(algorithm string) (ecdh.Curve, error) {
|
||||||
|
switch algorithm {
|
||||||
|
case "x25519":
|
||||||
|
return ecdh.X25519(), nil
|
||||||
|
case "ecdh-p256":
|
||||||
|
return ecdh.P256(), nil
|
||||||
|
case "ecdh-p384":
|
||||||
|
return ecdh.P384(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrInvalidAlgorithm, algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKeyAlgorithm(alg string) error {
|
||||||
|
_, err := curveForAlgorithm(alg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapDEKForRecipient(senderPriv *ecdh.PrivateKey, recipientPub *ecdh.PublicKey, dek []byte, sender, recipient string) (*recipientEntry, error) {
|
||||||
|
shared, err := senderPriv.ECDH(recipientPub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ECDH: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(shared)
|
||||||
|
|
||||||
|
// Generate HKDF salt.
|
||||||
|
salt := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return nil, fmt.Errorf("generate salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive wrapping key.
|
||||||
|
info := []byte(hkdfInfoPrefix + sender + ":" + recipient)
|
||||||
|
wrappingKey := make([]byte, keySize)
|
||||||
|
hkdfReader := hkdf.New(sha256.New, shared, salt, info)
|
||||||
|
if _, err := io.ReadFull(hkdfReader, wrappingKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("HKDF: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(wrappingKey)
|
||||||
|
|
||||||
|
// Wrap DEK with AES-256-GCM.
|
||||||
|
wrapped, err := encryptAESGCM(wrappingKey, dek, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap DEK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &recipientEntry{
|
||||||
|
Salt: base64.StdEncoding.EncodeToString(salt),
|
||||||
|
WrappedDEK: base64.StdEncoding.EncodeToString(wrapped),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unwrapDEK(callerPriv *ecdh.PrivateKey, senderPub *ecdh.PublicKey, entry *recipientEntry, sender, caller string) ([]byte, error) {
|
||||||
|
shared, err := callerPriv.ECDH(senderPub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ECDH: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(shared)
|
||||||
|
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(entry.Salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := []byte(hkdfInfoPrefix + sender + ":" + caller)
|
||||||
|
wrappingKey := make([]byte, keySize)
|
||||||
|
hkdfReader := hkdf.New(sha256.New, shared, salt, info)
|
||||||
|
if _, err := io.ReadFull(hkdfReader, wrappingKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("HKDF: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(wrappingKey)
|
||||||
|
|
||||||
|
wrapped, err := base64.StdEncoding.DecodeString(entry.WrappedDEK)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode wrapped DEK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dek, err := decryptAESGCM(wrappingKey, wrapped, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unwrap DEK: %w", err)
|
||||||
|
}
|
||||||
|
return dek, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptAESGCM(key, plaintext, aad []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := make([]byte, nonceSize)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ct := gcm.Seal(nil, nonce, plaintext, aad)
|
||||||
|
// Return nonce + ciphertext+tag.
|
||||||
|
result := make([]byte, nonceSize+len(ct))
|
||||||
|
copy(result, nonce)
|
||||||
|
copy(result[nonceSize:], ct)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptAESGCM(key, data, aad []byte) ([]byte, error) {
|
||||||
|
if len(data) < nonceSize+16 { // nonce + at least one AES block
|
||||||
|
return nil, ErrInvalidEnvelope
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := data[:nonceSize]
|
||||||
|
ct := data[nonceSize:]
|
||||||
|
return gcm.Open(nil, nonce, ct, aad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvelope(b64 string) (*envelope, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: base64 decode: %s", ErrInvalidEnvelope, err)
|
||||||
|
}
|
||||||
|
var env envelope
|
||||||
|
if err := json.Unmarshal(data, &env); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: json unmarshal: %s", ErrInvalidEnvelope, err)
|
||||||
|
}
|
||||||
|
if env.Version != 1 {
|
||||||
|
return nil, fmt.Errorf("%w: unsupported version %d", ErrInvalidEnvelope, env.Version)
|
||||||
|
}
|
||||||
|
if env.Sender == "" || len(env.Recipients) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: missing sender or recipients", ErrInvalidEnvelope)
|
||||||
|
}
|
||||||
|
return &env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRecipients(data map[string]interface{}) ([]string, error) {
|
||||||
|
raw, ok := data["recipients"]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
names := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
s, ok := item.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("user: recipient must be a string")
|
||||||
|
}
|
||||||
|
names = append(names, s)
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
case []string:
|
||||||
|
return v, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("user: invalid recipients format")
|
||||||
|
}
|
||||||
|
}
|
||||||
739
internal/engine/user/user_test.go
Normal file
739
internal/engine/user/user_test.go
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// memBarrier is an in-memory barrier for testing.
|
||||||
|
type memBarrier struct {
|
||||||
|
data map[string][]byte
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemBarrier() *memBarrier {
|
||||||
|
return &memBarrier{data: make(map[string][]byte)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBarrier) Unseal(_ []byte) error { return nil }
|
||||||
|
func (m *memBarrier) Seal() error { return nil }
|
||||||
|
func (m *memBarrier) IsSealed() bool { return false }
|
||||||
|
|
||||||
|
func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
v, ok := m.data[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, barrier.ErrNotFound
|
||||||
|
}
|
||||||
|
cp := make([]byte, len(v))
|
||||||
|
copy(cp, v)
|
||||||
|
return cp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBarrier) Put(_ context.Context, path string, value []byte) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
cp := make([]byte, len(value))
|
||||||
|
copy(cp, value)
|
||||||
|
m.data[path] = cp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBarrier) Delete(_ context.Context, path string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
delete(m.data, path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
var paths []string
|
||||||
|
for k := range m.data {
|
||||||
|
if strings.HasPrefix(k, prefix) {
|
||||||
|
paths = append(paths, strings.TrimPrefix(k, prefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminCaller() *engine.CallerInfo {
|
||||||
|
return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCaller(name string) *engine.CallerInfo {
|
||||||
|
return &engine.CallerInfo{Username: name, Roles: []string{"user"}, IsAdmin: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func guestCaller() *engine.CallerInfo {
|
||||||
|
return &engine.CallerInfo{Username: "guest", Roles: []string{"guest"}, IsAdmin: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupEngine(t *testing.T) (*UserEngine, *memBarrier) {
|
||||||
|
t.Helper()
|
||||||
|
b := newMemBarrier()
|
||||||
|
eng := NewUserEngine().(*UserEngine) //nolint:errcheck
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"key_algorithm": "x25519",
|
||||||
|
"sym_algorithm": "aes256-gcm",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := eng.Initialize(ctx, b, "engine/user/test/", config); err != nil {
|
||||||
|
t.Fatalf("Initialize: %v", err)
|
||||||
|
}
|
||||||
|
return eng, b
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeAndUnseal(t *testing.T) {
|
||||||
|
eng, b := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Register a user.
|
||||||
|
resp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Data["username"] != "alice" {
|
||||||
|
t.Fatalf("expected username alice, got %v", resp.Data["username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seal and unseal.
|
||||||
|
if err := eng.Seal(); err != nil {
|
||||||
|
t.Fatalf("seal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eng2 := NewUserEngine().(*UserEngine) //nolint:errcheck
|
||||||
|
if err := eng2.Unseal(ctx, b, "engine/user/test/"); err != nil {
|
||||||
|
t.Fatalf("unseal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify alice's key is loaded.
|
||||||
|
resp, err = eng2.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"username": "alice"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get-public-key after unseal: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Data["username"] != "alice" {
|
||||||
|
t.Fatalf("expected alice, got %v", resp.Data["username"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterCreatesKeypair(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
resp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, ok := resp.Data["public_key"].(string)
|
||||||
|
if !ok || pubKey == "" {
|
||||||
|
t.Fatal("expected non-empty public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode to verify it's valid base64.
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode public key: %v", err)
|
||||||
|
}
|
||||||
|
if len(raw) != 32 { // X25519 public key is 32 bytes
|
||||||
|
t.Fatalf("expected 32-byte public key, got %d", len(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterIdempotent(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
resp1, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp2, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp1.Data["public_key"] != resp2.Data["public_key"] {
|
||||||
|
t.Fatal("register should be idempotent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptSingleRecipient(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Register alice and bob.
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice encrypts to bob.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "hello bob",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope, ok := encResp.Data["envelope"].(string)
|
||||||
|
if !ok || envelope == "" {
|
||||||
|
t.Fatal("expected non-empty envelope")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob decrypts.
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decResp.Data["plaintext"] != "hello bob" {
|
||||||
|
t.Fatalf("expected 'hello bob', got %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
if decResp.Data["sender"] != "alice" {
|
||||||
|
t.Fatalf("expected sender alice, got %v", decResp.Data["sender"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptWithMetadata(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "secret message",
|
||||||
|
"metadata": "important context",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt: %v", err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "secret message" {
|
||||||
|
t.Fatalf("plaintext mismatch: %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
if decResp.Data["metadata"] != "important context" {
|
||||||
|
t.Fatalf("metadata mismatch: %v", decResp.Data["metadata"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiRecipientEncryptDecrypt(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
users := []string{"alice", "bob", "charlie"}
|
||||||
|
for _, name := range users {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice encrypts to bob and charlie.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "hello everyone",
|
||||||
|
"recipients": []interface{}{"bob", "charlie"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope := encResp.Data["envelope"].(string)
|
||||||
|
|
||||||
|
// Both bob and charlie can decrypt.
|
||||||
|
for _, name := range []string{"bob", "charlie"} {
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
Data: map[string]interface{}{"envelope": envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt by %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "hello everyone" {
|
||||||
|
t.Fatalf("%s: expected 'hello everyone', got %v", name, decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice (not a recipient) cannot decrypt.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{"envelope": envelope},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when non-recipient decrypts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReEncrypt(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice encrypts to bob.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "secret",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob re-encrypts.
|
||||||
|
reEncResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "re-encrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("re-encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob can decrypt re-encrypted envelope.
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": reEncResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt re-encrypted: %v", err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "secret" {
|
||||||
|
t.Fatalf("expected 'secret', got %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
if decResp.Data["sender"] != "bob" {
|
||||||
|
t.Fatalf("expected sender bob after re-encrypt, got %v", decResp.Data["sender"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRotateKey(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice encrypts to bob.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "before rotation",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob rotates key.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "rotate-key",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rotate-key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old envelope should fail to decrypt (sender's pubkey is used to unwrap,
|
||||||
|
// but the DEK was wrapped with old recipient key).
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decrypt to fail after key rotation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// New encrypt/decrypt should work.
|
||||||
|
encResp2, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "after rotation",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt after rotation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp2.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt after rotation: %v", err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "after rotation" {
|
||||||
|
t.Fatalf("expected 'after rotation', got %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoProvisionOnEncrypt(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Encrypt without pre-registering anyone.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "auto-provision test",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both alice and bob should be auto-provisioned. bob can decrypt.
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt: %v", err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "auto-provision test" {
|
||||||
|
t.Fatalf("expected 'auto-provision test', got %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionAdminOnly(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Non-admin cannot provision.
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "provision",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{"username": "bob"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin provision")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin can provision.
|
||||||
|
resp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "provision",
|
||||||
|
CallerInfo: adminCaller(),
|
||||||
|
Data: map[string]interface{}{"username": "bob"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin provision: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Data["username"] != "bob" {
|
||||||
|
t.Fatalf("expected bob, got %v", resp.Data["username"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptSelfOnly(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob", "charlie"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice encrypts to bob only.
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "for bob only",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charlie cannot decrypt.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("charlie"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when charlie tries to decrypt bob's message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGuestRejected(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "list-users",
|
||||||
|
CallerInfo: guestCaller(),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected guest to be rejected from list-users")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: guestCaller(),
|
||||||
|
Data: map[string]interface{}{"username": "alice"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected guest to be rejected from get-public-key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Register bob.
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-admin cannot delete.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "delete-user",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"username": "bob"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin can delete.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "delete-user",
|
||||||
|
CallerInfo: adminCaller(),
|
||||||
|
Data: map[string]interface{}{"username": "bob"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin delete: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bob should no longer exist.
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{"username": "bob"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected user not found after delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxRecipientsLimit(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build 101 recipients.
|
||||||
|
recipients := make([]interface{}, 101)
|
||||||
|
for i := range recipients {
|
||||||
|
recipients[i] = "user" + strings.Repeat("x", 5) + string(rune('a'+i%26)) + string(rune('0'+i/26))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "test",
|
||||||
|
"recipients": recipients,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for too many recipients")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "too many recipients") {
|
||||||
|
t.Fatalf("expected 'too many recipients' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListUsers(t *testing.T) {
|
||||||
|
eng, _ := setupEngine(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "list-users",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list-users: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users, ok := resp.Data["users"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected users list")
|
||||||
|
}
|
||||||
|
if len(users) != 2 {
|
||||||
|
t.Fatalf("expected 2 users, got %d", len(users))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestP256Algorithm(t *testing.T) {
|
||||||
|
b := newMemBarrier()
|
||||||
|
eng := NewUserEngine().(*UserEngine) //nolint:errcheck
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"key_algorithm": "ecdh-p256",
|
||||||
|
}
|
||||||
|
if err := eng.Initialize(ctx, b, "engine/user/p256/", config); err != nil {
|
||||||
|
t.Fatalf("Initialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{"alice", "bob"} {
|
||||||
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: userCaller(name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: userCaller("alice"),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"plaintext": "p256 test",
|
||||||
|
"recipients": []interface{}{"bob"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: userCaller("bob"),
|
||||||
|
Data: map[string]interface{}{"envelope": encResp.Data["envelope"]},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt: %v", err)
|
||||||
|
}
|
||||||
|
if decResp.Data["plaintext"] != "p256 test" {
|
||||||
|
t.Fatalf("expected 'p256 test', got %v", decResp.Data["plaintext"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ func (s *GRPCServer) Start() error {
|
|||||||
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
|
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
|
||||||
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
|
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
|
||||||
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
|
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
|
||||||
|
pb.RegisterUserServiceServer(s.srv, &userServer{s: s})
|
||||||
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
||||||
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
|
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
|
||||||
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
|
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
|
||||||
@@ -136,6 +137,15 @@ func sealRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
||||||
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
||||||
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
||||||
|
"/metacrypt.v2.UserService/Register": true,
|
||||||
|
"/metacrypt.v2.UserService/Provision": true,
|
||||||
|
"/metacrypt.v2.UserService/GetPublicKey": true,
|
||||||
|
"/metacrypt.v2.UserService/ListUsers": true,
|
||||||
|
"/metacrypt.v2.UserService/Encrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/Decrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/ReEncrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.UserService/DeleteUser": true,
|
||||||
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
@@ -202,6 +212,15 @@ func authRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
||||||
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
||||||
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
||||||
|
"/metacrypt.v2.UserService/Register": true,
|
||||||
|
"/metacrypt.v2.UserService/Provision": true,
|
||||||
|
"/metacrypt.v2.UserService/GetPublicKey": true,
|
||||||
|
"/metacrypt.v2.UserService/ListUsers": true,
|
||||||
|
"/metacrypt.v2.UserService/Encrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/Decrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/ReEncrypt": true,
|
||||||
|
"/metacrypt.v2.UserService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.UserService/DeleteUser": true,
|
||||||
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
"/metacrypt.v2.ACMEService/CreateEAB": true,
|
||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
@@ -258,6 +277,9 @@ func adminRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
||||||
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
||||||
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
||||||
|
// User.
|
||||||
|
"/metacrypt.v2.UserService/Provision": true,
|
||||||
|
"/metacrypt.v2.UserService/DeleteUser": true,
|
||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||||
|
|||||||
273
internal/grpcserver/user.go
Normal file
273
internal/grpcserver/user.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/user"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userServer struct {
|
||||||
|
pb.UnimplementedUserServiceServer
|
||||||
|
s *GRPCServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) callerInfo(ctx context.Context) *engine.CallerInfo {
|
||||||
|
ti := tokenInfoFromContext(ctx)
|
||||||
|
if ti == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &engine.CallerInfo{
|
||||||
|
Username: ti.Username,
|
||||||
|
Roles: ti.Roles,
|
||||||
|
IsAdmin: ti.IsAdmin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) policyChecker(ctx context.Context) engine.PolicyChecker {
|
||||||
|
caller := us.callerInfo(ctx)
|
||||||
|
if caller == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(resource, action string) (string, bool) {
|
||||||
|
pReq := &policy.Request{
|
||||||
|
Username: caller.Username,
|
||||||
|
Roles: caller.Roles,
|
||||||
|
Resource: resource,
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
effect, matched, err := us.s.policy.Match(ctx, pReq)
|
||||||
|
if err != nil {
|
||||||
|
return string(policy.EffectDeny), false
|
||||||
|
}
|
||||||
|
return string(effect), matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) handleRequest(ctx context.Context, mount, operation string, req *engine.Request) (*engine.Response, error) {
|
||||||
|
resp, err := us.s.engines.HandleRequest(ctx, mount, req)
|
||||||
|
if err != nil {
|
||||||
|
st := codes.Internal
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, engine.ErrMountNotFound):
|
||||||
|
st = codes.NotFound
|
||||||
|
case errors.Is(err, user.ErrUserNotFound):
|
||||||
|
st = codes.NotFound
|
||||||
|
case errors.Is(err, user.ErrUserExists):
|
||||||
|
st = codes.AlreadyExists
|
||||||
|
case errors.Is(err, user.ErrUnauthorized):
|
||||||
|
st = codes.Unauthenticated
|
||||||
|
case errors.Is(err, user.ErrForbidden):
|
||||||
|
st = codes.PermissionDenied
|
||||||
|
case errors.Is(err, user.ErrTooMany):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, user.ErrNoRecipients):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, user.ErrInvalidEnvelope):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, user.ErrRecipientNotFound):
|
||||||
|
st = codes.NotFound
|
||||||
|
case errors.Is(err, user.ErrDecryptionFailed):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case strings.Contains(err.Error(), "forbidden"):
|
||||||
|
st = codes.PermissionDenied
|
||||||
|
case strings.Contains(err.Error(), "not found"):
|
||||||
|
st = codes.NotFound
|
||||||
|
}
|
||||||
|
us.s.logger.Error("grpc: user "+operation, "mount", mount, "error", err)
|
||||||
|
return nil, status.Error(st, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) Register(ctx context.Context, req *pb.UserRegisterRequest) (*pb.UserRegisterResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "register", &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
username, _ := resp.Data["username"].(string)
|
||||||
|
pubKey, _ := resp.Data["public_key"].(string)
|
||||||
|
algorithm, _ := resp.Data["algorithm"].(string)
|
||||||
|
us.s.logger.Info("audit: user registered", "mount", req.Mount, "username", username)
|
||||||
|
return &pb.UserRegisterResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) Provision(ctx context.Context, req *pb.UserProvisionRequest) (*pb.UserProvisionResponse, error) {
|
||||||
|
if req.Mount == "" || req.Username == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "provision", &engine.Request{
|
||||||
|
Operation: "provision",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"username": req.Username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
username, _ := resp.Data["username"].(string)
|
||||||
|
pubKey, _ := resp.Data["public_key"].(string)
|
||||||
|
algorithm, _ := resp.Data["algorithm"].(string)
|
||||||
|
us.s.logger.Info("audit: user provisioned", "mount", req.Mount, "username", username, "by", callerUsername(ctx))
|
||||||
|
return &pb.UserProvisionResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) GetPublicKey(ctx context.Context, req *pb.UserGetPublicKeyRequest) (*pb.UserGetPublicKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Username == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "get-public-key", &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"username": req.Username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
username, _ := resp.Data["username"].(string)
|
||||||
|
pubKey, _ := resp.Data["public_key"].(string)
|
||||||
|
algorithm, _ := resp.Data["algorithm"].(string)
|
||||||
|
return &pb.UserGetPublicKeyResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) ListUsers(ctx context.Context, req *pb.UserListUsersRequest) (*pb.UserListUsersResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "list-users", &engine.Request{
|
||||||
|
Operation: "list-users",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw, _ := resp.Data["users"].([]interface{})
|
||||||
|
users := make([]string, 0, len(raw))
|
||||||
|
for _, v := range raw {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
users = append(users, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &pb.UserListUsersResponse{Users: users}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) Encrypt(ctx context.Context, req *pb.UserEncryptRequest) (*pb.UserEncryptResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
if req.Plaintext == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "plaintext is required")
|
||||||
|
}
|
||||||
|
if len(req.Recipients) == 0 {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "recipients are required")
|
||||||
|
}
|
||||||
|
recipients := make([]interface{}, len(req.Recipients))
|
||||||
|
for i, r := range req.Recipients {
|
||||||
|
recipients[i] = r
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"plaintext": req.Plaintext,
|
||||||
|
"recipients": recipients,
|
||||||
|
}
|
||||||
|
if req.Metadata != "" {
|
||||||
|
data["metadata"] = req.Metadata
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "encrypt", &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
CheckPolicy: us.policyChecker(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
envelope, _ := resp.Data["envelope"].(string)
|
||||||
|
us.s.logger.Info("audit: user encrypt", "mount", req.Mount, "recipients", req.Recipients, "username", callerUsername(ctx))
|
||||||
|
return &pb.UserEncryptResponse{Envelope: envelope}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) Decrypt(ctx context.Context, req *pb.UserDecryptRequest) (*pb.UserDecryptResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
if req.Envelope == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "envelope is required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "decrypt", &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plaintext, _ := resp.Data["plaintext"].(string)
|
||||||
|
sender, _ := resp.Data["sender"].(string)
|
||||||
|
metadata, _ := resp.Data["metadata"].(string)
|
||||||
|
return &pb.UserDecryptResponse{Plaintext: plaintext, Sender: sender, Metadata: metadata}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) ReEncrypt(ctx context.Context, req *pb.UserReEncryptRequest) (*pb.UserReEncryptResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
if req.Envelope == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "envelope is required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "re-encrypt", &engine.Request{
|
||||||
|
Operation: "re-encrypt",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
envelope, _ := resp.Data["envelope"].(string)
|
||||||
|
return &pb.UserReEncryptResponse{Envelope: envelope}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) RotateKey(ctx context.Context, req *pb.UserRotateKeyRequest) (*pb.UserRotateKeyResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
resp, err := us.handleRequest(ctx, req.Mount, "rotate-key", &engine.Request{
|
||||||
|
Operation: "rotate-key",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
username, _ := resp.Data["username"].(string)
|
||||||
|
pubKey, _ := resp.Data["public_key"].(string)
|
||||||
|
algorithm, _ := resp.Data["algorithm"].(string)
|
||||||
|
us.s.logger.Info("audit: user key rotated", "mount", req.Mount, "username", username)
|
||||||
|
return &pb.UserRotateKeyResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *userServer) DeleteUser(ctx context.Context, req *pb.UserDeleteUserRequest) (*pb.UserDeleteUserResponse, error) {
|
||||||
|
if req.Mount == "" || req.Username == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
||||||
|
}
|
||||||
|
_, err := us.handleRequest(ctx, req.Mount, "delete-user", &engine.Request{
|
||||||
|
Operation: "delete-user",
|
||||||
|
CallerInfo: us.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"username": req.Username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
us.s.logger.Info("audit: user deleted", "mount", req.Mount, "username", req.Username, "by", callerUsername(ctx))
|
||||||
|
return &pb.UserDeleteUserResponse{}, nil
|
||||||
|
}
|
||||||
@@ -69,6 +69,17 @@ func (s *Server) registerRoutes(r chi.Router) {
|
|||||||
r.Post("/v1/barrier/rotate-key", s.requireAdmin(s.handleRotateKey))
|
r.Post("/v1/barrier/rotate-key", s.requireAdmin(s.handleRotateKey))
|
||||||
r.Post("/v1/barrier/migrate", s.requireAdmin(s.handleBarrierMigrate))
|
r.Post("/v1/barrier/migrate", s.requireAdmin(s.handleBarrierMigrate))
|
||||||
|
|
||||||
|
// User-to-user encryption routes (auth required).
|
||||||
|
r.Post("/v1/user/{mount}/register", s.requireAuth(s.handleUserRegister))
|
||||||
|
r.Post("/v1/user/{mount}/provision", s.requireAdmin(s.handleUserProvision))
|
||||||
|
r.Get("/v1/user/{mount}/keys", s.requireAuth(s.handleUserListUsers))
|
||||||
|
r.Get("/v1/user/{mount}/keys/{username}", s.requireAuth(s.handleUserGetPublicKey))
|
||||||
|
r.Delete("/v1/user/{mount}/keys/{username}", s.requireAdmin(s.handleUserDeleteUser))
|
||||||
|
r.Post("/v1/user/{mount}/encrypt", s.requireAuth(s.handleUserEncrypt))
|
||||||
|
r.Post("/v1/user/{mount}/decrypt", s.requireAuth(s.handleUserDecrypt))
|
||||||
|
r.Post("/v1/user/{mount}/re-encrypt", s.requireAuth(s.handleUserReEncrypt))
|
||||||
|
r.Post("/v1/user/{mount}/rotate", s.requireAuth(s.handleUserRotateKey))
|
||||||
|
|
||||||
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
||||||
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
||||||
|
|
||||||
@@ -298,30 +309,34 @@ func (s *Server) handleEngineUnmount(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// adminOnlyOperations lists engine operations that require admin role.
|
// adminOnlyOperations lists engine operations that require admin role.
|
||||||
// Keys are "engineType:operation" to avoid name collisions across engines
|
// This enforces the same gates as the typed REST routes, ensuring the
|
||||||
// (e.g. transit "rotate-key" is admin-only but user "rotate-key" is user-self).
|
// generic endpoint cannot bypass admin requirements.
|
||||||
var adminOnlyOperations = map[string]bool{
|
var adminOnlyOperations = map[string]bool{
|
||||||
// CA engine.
|
// CA engine.
|
||||||
"ca:import-root": true,
|
"import-root": true,
|
||||||
"ca:create-issuer": true,
|
"create-issuer": true,
|
||||||
"ca:delete-issuer": true,
|
"delete-issuer": true,
|
||||||
"ca:revoke-cert": true,
|
"revoke-cert": true,
|
||||||
"ca:delete-cert": true,
|
"delete-cert": true,
|
||||||
// Transit engine.
|
// Transit engine.
|
||||||
"transit:create-key": true,
|
"create-key": true,
|
||||||
"transit:delete-key": true,
|
"delete-key": true,
|
||||||
"transit:rotate-key": true,
|
"rotate-key": true,
|
||||||
"transit:update-key-config": true,
|
"update-key-config": true,
|
||||||
"transit:trim-key": true,
|
"trim-key": true,
|
||||||
// SSH CA engine.
|
// SSH CA engine.
|
||||||
"sshca:create-profile": true,
|
"create-profile": true,
|
||||||
"sshca:update-profile": true,
|
"update-profile": true,
|
||||||
"sshca:delete-profile": true,
|
"delete-profile": true,
|
||||||
"sshca:revoke-cert": true,
|
|
||||||
"sshca:delete-cert": true,
|
|
||||||
// User engine.
|
// User engine.
|
||||||
"user:provision": true,
|
"provision": true,
|
||||||
"user:delete-user": true,
|
"delete-user": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// adminExemptOperations lists engineType:operation pairs that are exempt from
|
||||||
|
// the unqualified admin-only check (e.g. user:rotate-key is user-self, not admin).
|
||||||
|
var adminExemptOperations = map[string]bool{
|
||||||
|
"user:rotate-key": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -342,17 +357,21 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
info := TokenInfoFromContext(r.Context())
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
|
||||||
// Resolve engine type from mount to qualify the admin-only lookup.
|
// Resolve engine type from mount.
|
||||||
mount, err := s.engines.GetMount(req.Mount)
|
mount, err := s.engines.GetMount(req.Mount)
|
||||||
if err != nil {
|
|
||||||
http.Error(w, `{"error":"mount not found"}`, http.StatusNotFound)
|
// Enforce admin requirement for operations that are admin-only.
|
||||||
return
|
// Check exemptions for engine-specific overrides (e.g. user:rotate-key is user-self).
|
||||||
|
if adminOnlyOperations[req.Operation] && !info.IsAdmin {
|
||||||
|
exempt := err == nil && adminExemptOperations[string(mount.Type)+":"+req.Operation]
|
||||||
|
if !exempt {
|
||||||
|
http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce admin requirement for operations that have admin-only typed routes.
|
if err != nil {
|
||||||
// Key is "engineType:operation" to avoid cross-engine name collisions.
|
http.Error(w, `{"error":"mount not found"}`, http.StatusNotFound)
|
||||||
if adminOnlyOperations[string(mount.Type)+":"+req.Operation] && !info.IsAdmin {
|
|
||||||
http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,24 +822,7 @@ func (s *Server) transitRequest(w http.ResponseWriter, r *http.Request, mount, o
|
|||||||
Data: data,
|
Data: data,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
s.writeEngineError(w, err)
|
||||||
switch {
|
|
||||||
case errors.Is(err, engine.ErrMountNotFound):
|
|
||||||
status = http.StatusNotFound
|
|
||||||
case strings.Contains(err.Error(), "forbidden"):
|
|
||||||
status = http.StatusForbidden
|
|
||||||
case strings.Contains(err.Error(), "authentication required"):
|
|
||||||
status = http.StatusUnauthorized
|
|
||||||
case strings.Contains(err.Error(), "not found"):
|
|
||||||
status = http.StatusNotFound
|
|
||||||
case strings.Contains(err.Error(), "not allowed"):
|
|
||||||
status = http.StatusForbidden
|
|
||||||
case strings.Contains(err.Error(), "unsupported"):
|
|
||||||
status = http.StatusBadRequest
|
|
||||||
case strings.Contains(err.Error(), "invalid"):
|
|
||||||
status = http.StatusBadRequest
|
|
||||||
}
|
|
||||||
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, resp.Data)
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
@@ -1033,6 +1035,239 @@ func (s *Server) handleTransitGetPublicKey(w http.ResponseWriter, r *http.Reques
|
|||||||
s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name})
|
s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- User-to-User Encryption Handlers ---
|
||||||
|
|
||||||
|
func (s *Server) handleUserRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "register",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserProvision(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
var req struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Username == "" {
|
||||||
|
http.Error(w, `{"error":"username is required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "provision",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{"username": req.Username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "list-users",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserGetPublicKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
username := chi.URLParam(r, "username")
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{"username": username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
username := chi.URLParam(r, "username")
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
_, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "delete-user",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{"username": username},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
var req struct {
|
||||||
|
Plaintext string `json:"plaintext"`
|
||||||
|
Metadata string `json:"metadata"`
|
||||||
|
Recipients []string `json:"recipients"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recipients := make([]interface{}, len(req.Recipients))
|
||||||
|
for i, r := range req.Recipients {
|
||||||
|
recipients[i] = r
|
||||||
|
}
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
|
||||||
|
policyChecker := func(resource, action string) (string, bool) {
|
||||||
|
pReq := &policy.Request{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
Resource: resource,
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
|
||||||
|
if pErr != nil {
|
||||||
|
return string(policy.EffectDeny), false
|
||||||
|
}
|
||||||
|
return string(eff), matched
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"plaintext": req.Plaintext,
|
||||||
|
"recipients": recipients,
|
||||||
|
}
|
||||||
|
if req.Metadata != "" {
|
||||||
|
data["metadata"] = req.Metadata
|
||||||
|
}
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
CheckPolicy: policyChecker,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
var req struct {
|
||||||
|
Envelope string `json:"envelope"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserReEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
var req struct {
|
||||||
|
Envelope string `json:"envelope"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "re-encrypt",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUserRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mountName := chi.URLParam(r, "mount")
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||||
|
Operation: "rotate-key",
|
||||||
|
CallerInfo: &engine.CallerInfo{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
IsAdmin: info.IsAdmin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.writeEngineError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
// operationAction maps an engine operation name to a policy action.
|
// operationAction maps an engine operation name to a policy action.
|
||||||
func operationAction(op string) string {
|
func operationAction(op string) string {
|
||||||
switch op {
|
switch op {
|
||||||
@@ -1045,7 +1280,7 @@ func operationAction(op string) string {
|
|||||||
// Granular cryptographic operations (including batch variants).
|
// Granular cryptographic operations (including batch variants).
|
||||||
case "encrypt", "batch-encrypt":
|
case "encrypt", "batch-encrypt":
|
||||||
return policy.ActionEncrypt
|
return policy.ActionEncrypt
|
||||||
case "decrypt", "batch-decrypt", "rewrap", "batch-rewrap":
|
case "decrypt", "batch-decrypt", "rewrap", "batch-rewrap", "re-encrypt":
|
||||||
return policy.ActionDecrypt
|
return policy.ActionDecrypt
|
||||||
case "sign", "sign-host", "sign-user":
|
case "sign", "sign-host", "sign-user":
|
||||||
return policy.ActionSign
|
return policy.ActionSign
|
||||||
@@ -1481,10 +1716,17 @@ func (s *Server) writeEngineError(w http.ResponseWriter, err error) {
|
|||||||
status = http.StatusForbidden
|
status = http.StatusForbidden
|
||||||
case errors.Is(err, sshca.ErrUnauthorized):
|
case errors.Is(err, sshca.ErrUnauthorized):
|
||||||
status = http.StatusUnauthorized
|
status = http.StatusUnauthorized
|
||||||
case strings.Contains(err.Error(), "forbidden"):
|
case strings.Contains(err.Error(), "forbidden"),
|
||||||
|
strings.Contains(err.Error(), "not allowed"):
|
||||||
status = http.StatusForbidden
|
status = http.StatusForbidden
|
||||||
|
case strings.Contains(err.Error(), "authentication required"):
|
||||||
|
status = http.StatusUnauthorized
|
||||||
case strings.Contains(err.Error(), "not found"):
|
case strings.Contains(err.Error(), "not found"):
|
||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
|
case strings.Contains(err.Error(), "unsupported"),
|
||||||
|
strings.Contains(err.Error(), "invalid"),
|
||||||
|
strings.Contains(err.Error(), "too many"):
|
||||||
|
status = http.StatusBadRequest
|
||||||
}
|
}
|
||||||
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
||||||
}
|
}
|
||||||
|
|||||||
143
proto/metacrypt/v2/user.proto
Normal file
143
proto/metacrypt/v2/user.proto
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package metacrypt.v2;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2";
|
||||||
|
|
||||||
|
// UserService provides typed, authenticated access to user-to-user encryption
|
||||||
|
// engine operations. All RPCs require the service to be unsealed and
|
||||||
|
// authentication.
|
||||||
|
service UserService {
|
||||||
|
// Register self-registers the caller, creating a keypair. No-op if exists.
|
||||||
|
rpc Register(UserRegisterRequest) returns (UserRegisterResponse);
|
||||||
|
|
||||||
|
// Provision creates a keypair for a given username. Admin only.
|
||||||
|
rpc Provision(UserProvisionRequest) returns (UserProvisionResponse);
|
||||||
|
|
||||||
|
// GetPublicKey returns the public key for a given username.
|
||||||
|
rpc GetPublicKey(UserGetPublicKeyRequest) returns (UserGetPublicKeyResponse);
|
||||||
|
|
||||||
|
// ListUsers returns all registered usernames.
|
||||||
|
rpc ListUsers(UserListUsersRequest) returns (UserListUsersResponse);
|
||||||
|
|
||||||
|
// Encrypt encrypts plaintext for one or more recipients.
|
||||||
|
rpc Encrypt(UserEncryptRequest) returns (UserEncryptResponse);
|
||||||
|
|
||||||
|
// Decrypt decrypts an envelope addressed to the caller.
|
||||||
|
rpc Decrypt(UserDecryptRequest) returns (UserDecryptResponse);
|
||||||
|
|
||||||
|
// ReEncrypt decrypts and re-encrypts an envelope with current keys.
|
||||||
|
rpc ReEncrypt(UserReEncryptRequest) returns (UserReEncryptResponse);
|
||||||
|
|
||||||
|
// RotateKey generates a new keypair for the caller, replacing the old one.
|
||||||
|
rpc RotateKey(UserRotateKeyRequest) returns (UserRotateKeyResponse);
|
||||||
|
|
||||||
|
// DeleteUser removes a user's keys. Admin only.
|
||||||
|
rpc DeleteUser(UserDeleteUserRequest) returns (UserDeleteUserResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Register ---
|
||||||
|
|
||||||
|
message UserRegisterRequest {
|
||||||
|
string mount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserRegisterResponse {
|
||||||
|
string username = 1;
|
||||||
|
string public_key = 2;
|
||||||
|
string algorithm = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Provision ---
|
||||||
|
|
||||||
|
message UserProvisionRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string username = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserProvisionResponse {
|
||||||
|
string username = 1;
|
||||||
|
string public_key = 2;
|
||||||
|
string algorithm = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GetPublicKey ---
|
||||||
|
|
||||||
|
message UserGetPublicKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string username = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserGetPublicKeyResponse {
|
||||||
|
string username = 1;
|
||||||
|
string public_key = 2;
|
||||||
|
string algorithm = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ListUsers ---
|
||||||
|
|
||||||
|
message UserListUsersRequest {
|
||||||
|
string mount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserListUsersResponse {
|
||||||
|
repeated string users = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Encrypt ---
|
||||||
|
|
||||||
|
message UserEncryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string plaintext = 2;
|
||||||
|
string metadata = 3;
|
||||||
|
repeated string recipients = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserEncryptResponse {
|
||||||
|
string envelope = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Decrypt ---
|
||||||
|
|
||||||
|
message UserDecryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string envelope = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserDecryptResponse {
|
||||||
|
string plaintext = 1;
|
||||||
|
string sender = 2;
|
||||||
|
string metadata = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ReEncrypt ---
|
||||||
|
|
||||||
|
message UserReEncryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string envelope = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserReEncryptResponse {
|
||||||
|
string envelope = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RotateKey ---
|
||||||
|
|
||||||
|
message UserRotateKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserRotateKeyResponse {
|
||||||
|
string username = 1;
|
||||||
|
string public_key = 2;
|
||||||
|
string algorithm = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DeleteUser ---
|
||||||
|
|
||||||
|
message UserDeleteUserRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string username = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserDeleteUserResponse {}
|
||||||
Reference in New Issue
Block a user