diff --git a/cmd/metacrypt/server.go b/cmd/metacrypt/server.go index 9c4f4ad..440e875 100644 --- a/cmd/metacrypt/server.go +++ b/cmd/metacrypt/server.go @@ -16,6 +16,7 @@ import ( "git.wntrmute.dev/kyle/metacrypt/internal/db" "git.wntrmute.dev/kyle/metacrypt/internal/engine" "git.wntrmute.dev/kyle/metacrypt/internal/engine/ca" + "git.wntrmute.dev/kyle/metacrypt/internal/engine/user" "git.wntrmute.dev/kyle/metacrypt/internal/grpcserver" "git.wntrmute.dev/kyle/metacrypt/internal/policy" "git.wntrmute.dev/kyle/metacrypt/internal/seal" @@ -74,6 +75,7 @@ func runServer(cmd *cobra.Command, args []string) error { policyEngine := policy.NewEngine(b) engineRegistry := engine.NewRegistry(b, logger) engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine) + engineRegistry.RegisterFactory(engine.EngineTypeUser, user.NewUserEngine) srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version) grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger) diff --git a/gen/metacrypt/v2/user.pb.go b/gen/metacrypt/v2/user.pb.go new file mode 100644 index 0000000..a4f8447 --- /dev/null +++ b/gen/metacrypt/v2/user.pb.go @@ -0,0 +1,1109 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.20.3 +// source: proto/metacrypt/v2/user.proto + +package metacryptv2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type UserRegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserRegisterRequest) Reset() { + *x = UserRegisterRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserRegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRegisterRequest) ProtoMessage() {} + +func (x *UserRegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRegisterRequest.ProtoReflect.Descriptor instead. +func (*UserRegisterRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{0} +} + +func (x *UserRegisterRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +type UserRegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Algorithm string `protobuf:"bytes,3,opt,name=algorithm,proto3" json:"algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserRegisterResponse) Reset() { + *x = UserRegisterResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserRegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRegisterResponse) ProtoMessage() {} + +func (x *UserRegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRegisterResponse.ProtoReflect.Descriptor instead. +func (*UserRegisterResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{1} +} + +func (x *UserRegisterResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UserRegisterResponse) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *UserRegisterResponse) GetAlgorithm() string { + if x != nil { + return x.Algorithm + } + return "" +} + +type UserProvisionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserProvisionRequest) Reset() { + *x = UserProvisionRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserProvisionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserProvisionRequest) ProtoMessage() {} + +func (x *UserProvisionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserProvisionRequest.ProtoReflect.Descriptor instead. +func (*UserProvisionRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{2} +} + +func (x *UserProvisionRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserProvisionRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type UserProvisionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Algorithm string `protobuf:"bytes,3,opt,name=algorithm,proto3" json:"algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserProvisionResponse) Reset() { + *x = UserProvisionResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserProvisionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserProvisionResponse) ProtoMessage() {} + +func (x *UserProvisionResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserProvisionResponse.ProtoReflect.Descriptor instead. +func (*UserProvisionResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{3} +} + +func (x *UserProvisionResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UserProvisionResponse) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *UserProvisionResponse) GetAlgorithm() string { + if x != nil { + return x.Algorithm + } + return "" +} + +type UserGetPublicKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserGetPublicKeyRequest) Reset() { + *x = UserGetPublicKeyRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserGetPublicKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserGetPublicKeyRequest) ProtoMessage() {} + +func (x *UserGetPublicKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserGetPublicKeyRequest.ProtoReflect.Descriptor instead. +func (*UserGetPublicKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{4} +} + +func (x *UserGetPublicKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserGetPublicKeyRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type UserGetPublicKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Algorithm string `protobuf:"bytes,3,opt,name=algorithm,proto3" json:"algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserGetPublicKeyResponse) Reset() { + *x = UserGetPublicKeyResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserGetPublicKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserGetPublicKeyResponse) ProtoMessage() {} + +func (x *UserGetPublicKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserGetPublicKeyResponse.ProtoReflect.Descriptor instead. +func (*UserGetPublicKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{5} +} + +func (x *UserGetPublicKeyResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UserGetPublicKeyResponse) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *UserGetPublicKeyResponse) GetAlgorithm() string { + if x != nil { + return x.Algorithm + } + return "" +} + +type UserListUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserListUsersRequest) Reset() { + *x = UserListUsersRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserListUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserListUsersRequest) ProtoMessage() {} + +func (x *UserListUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserListUsersRequest.ProtoReflect.Descriptor instead. +func (*UserListUsersRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{6} +} + +func (x *UserListUsersRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +type UserListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserListUsersResponse) Reset() { + *x = UserListUsersResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserListUsersResponse) ProtoMessage() {} + +func (x *UserListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserListUsersResponse.ProtoReflect.Descriptor instead. +func (*UserListUsersResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{7} +} + +func (x *UserListUsersResponse) GetUsers() []string { + if x != nil { + return x.Users + } + return nil +} + +type UserEncryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Plaintext string `protobuf:"bytes,2,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + Recipients []string `protobuf:"bytes,4,rep,name=recipients,proto3" json:"recipients,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserEncryptRequest) Reset() { + *x = UserEncryptRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserEncryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserEncryptRequest) ProtoMessage() {} + +func (x *UserEncryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserEncryptRequest.ProtoReflect.Descriptor instead. +func (*UserEncryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{8} +} + +func (x *UserEncryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserEncryptRequest) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +func (x *UserEncryptRequest) GetMetadata() string { + if x != nil { + return x.Metadata + } + return "" +} + +func (x *UserEncryptRequest) GetRecipients() []string { + if x != nil { + return x.Recipients + } + return nil +} + +type UserEncryptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Envelope string `protobuf:"bytes,1,opt,name=envelope,proto3" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserEncryptResponse) Reset() { + *x = UserEncryptResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserEncryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserEncryptResponse) ProtoMessage() {} + +func (x *UserEncryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserEncryptResponse.ProtoReflect.Descriptor instead. +func (*UserEncryptResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{9} +} + +func (x *UserEncryptResponse) GetEnvelope() string { + if x != nil { + return x.Envelope + } + return "" +} + +type UserDecryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Envelope string `protobuf:"bytes,2,opt,name=envelope,proto3" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserDecryptRequest) Reset() { + *x = UserDecryptRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserDecryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserDecryptRequest) ProtoMessage() {} + +func (x *UserDecryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserDecryptRequest.ProtoReflect.Descriptor instead. +func (*UserDecryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{10} +} + +func (x *UserDecryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserDecryptRequest) GetEnvelope() string { + if x != nil { + return x.Envelope + } + return "" +} + +type UserDecryptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` + Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserDecryptResponse) Reset() { + *x = UserDecryptResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserDecryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserDecryptResponse) ProtoMessage() {} + +func (x *UserDecryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserDecryptResponse.ProtoReflect.Descriptor instead. +func (*UserDecryptResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{11} +} + +func (x *UserDecryptResponse) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +func (x *UserDecryptResponse) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *UserDecryptResponse) GetMetadata() string { + if x != nil { + return x.Metadata + } + return "" +} + +type UserReEncryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Envelope string `protobuf:"bytes,2,opt,name=envelope,proto3" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserReEncryptRequest) Reset() { + *x = UserReEncryptRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserReEncryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserReEncryptRequest) ProtoMessage() {} + +func (x *UserReEncryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserReEncryptRequest.ProtoReflect.Descriptor instead. +func (*UserReEncryptRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{12} +} + +func (x *UserReEncryptRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserReEncryptRequest) GetEnvelope() string { + if x != nil { + return x.Envelope + } + return "" +} + +type UserReEncryptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Envelope string `protobuf:"bytes,1,opt,name=envelope,proto3" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserReEncryptResponse) Reset() { + *x = UserReEncryptResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserReEncryptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserReEncryptResponse) ProtoMessage() {} + +func (x *UserReEncryptResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserReEncryptResponse.ProtoReflect.Descriptor instead. +func (*UserReEncryptResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{13} +} + +func (x *UserReEncryptResponse) GetEnvelope() string { + if x != nil { + return x.Envelope + } + return "" +} + +type UserRotateKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserRotateKeyRequest) Reset() { + *x = UserRotateKeyRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserRotateKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRotateKeyRequest) ProtoMessage() {} + +func (x *UserRotateKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRotateKeyRequest.ProtoReflect.Descriptor instead. +func (*UserRotateKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{14} +} + +func (x *UserRotateKeyRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +type UserRotateKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Algorithm string `protobuf:"bytes,3,opt,name=algorithm,proto3" json:"algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserRotateKeyResponse) Reset() { + *x = UserRotateKeyResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserRotateKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRotateKeyResponse) ProtoMessage() {} + +func (x *UserRotateKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRotateKeyResponse.ProtoReflect.Descriptor instead. +func (*UserRotateKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{15} +} + +func (x *UserRotateKeyResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UserRotateKeyResponse) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *UserRotateKeyResponse) GetAlgorithm() string { + if x != nil { + return x.Algorithm + } + return "" +} + +type UserDeleteUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mount string `protobuf:"bytes,1,opt,name=mount,proto3" json:"mount,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserDeleteUserRequest) Reset() { + *x = UserDeleteUserRequest{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserDeleteUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserDeleteUserRequest) ProtoMessage() {} + +func (x *UserDeleteUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserDeleteUserRequest.ProtoReflect.Descriptor instead. +func (*UserDeleteUserRequest) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{16} +} + +func (x *UserDeleteUserRequest) GetMount() string { + if x != nil { + return x.Mount + } + return "" +} + +func (x *UserDeleteUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type UserDeleteUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserDeleteUserResponse) Reset() { + *x = UserDeleteUserResponse{} + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserDeleteUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserDeleteUserResponse) ProtoMessage() {} + +func (x *UserDeleteUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_metacrypt_v2_user_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserDeleteUserResponse.ProtoReflect.Descriptor instead. +func (*UserDeleteUserResponse) Descriptor() ([]byte, []int) { + return file_proto_metacrypt_v2_user_proto_rawDescGZIP(), []int{17} +} + +var File_proto_metacrypt_v2_user_proto protoreflect.FileDescriptor + +const file_proto_metacrypt_v2_user_proto_rawDesc = "" + + "\n" + + "\x1dproto/metacrypt/v2/user.proto\x12\fmetacrypt.v2\"+\n" + + "\x13UserRegisterRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\"o\n" + + "\x14UserRegisterResponse\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" + + "\n" + + "public_key\x18\x02 \x01(\tR\tpublicKey\x12\x1c\n" + + "\talgorithm\x18\x03 \x01(\tR\talgorithm\"H\n" + + "\x14UserProvisionRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\"p\n" + + "\x15UserProvisionResponse\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" + + "\n" + + "public_key\x18\x02 \x01(\tR\tpublicKey\x12\x1c\n" + + "\talgorithm\x18\x03 \x01(\tR\talgorithm\"K\n" + + "\x17UserGetPublicKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\"s\n" + + "\x18UserGetPublicKeyResponse\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" + + "\n" + + "public_key\x18\x02 \x01(\tR\tpublicKey\x12\x1c\n" + + "\talgorithm\x18\x03 \x01(\tR\talgorithm\",\n" + + "\x14UserListUsersRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\"-\n" + + "\x15UserListUsersResponse\x12\x14\n" + + "\x05users\x18\x01 \x03(\tR\x05users\"\x84\x01\n" + + "\x12UserEncryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1c\n" + + "\tplaintext\x18\x02 \x01(\tR\tplaintext\x12\x1a\n" + + "\bmetadata\x18\x03 \x01(\tR\bmetadata\x12\x1e\n" + + "\n" + + "recipients\x18\x04 \x03(\tR\n" + + "recipients\"1\n" + + "\x13UserEncryptResponse\x12\x1a\n" + + "\benvelope\x18\x01 \x01(\tR\benvelope\"F\n" + + "\x12UserDecryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1a\n" + + "\benvelope\x18\x02 \x01(\tR\benvelope\"g\n" + + "\x13UserDecryptResponse\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\x12\x16\n" + + "\x06sender\x18\x02 \x01(\tR\x06sender\x12\x1a\n" + + "\bmetadata\x18\x03 \x01(\tR\bmetadata\"H\n" + + "\x14UserReEncryptRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1a\n" + + "\benvelope\x18\x02 \x01(\tR\benvelope\"3\n" + + "\x15UserReEncryptResponse\x12\x1a\n" + + "\benvelope\x18\x01 \x01(\tR\benvelope\",\n" + + "\x14UserRotateKeyRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\"p\n" + + "\x15UserRotateKeyResponse\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" + + "\n" + + "public_key\x18\x02 \x01(\tR\tpublicKey\x12\x1c\n" + + "\talgorithm\x18\x03 \x01(\tR\talgorithm\"I\n" + + "\x15UserDeleteUserRequest\x12\x14\n" + + "\x05mount\x18\x01 \x01(\tR\x05mount\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\"\x18\n" + + "\x16UserDeleteUserResponse2\x90\x06\n" + + "\vUserService\x12Q\n" + + "\bRegister\x12!.metacrypt.v2.UserRegisterRequest\x1a\".metacrypt.v2.UserRegisterResponse\x12T\n" + + "\tProvision\x12\".metacrypt.v2.UserProvisionRequest\x1a#.metacrypt.v2.UserProvisionResponse\x12]\n" + + "\fGetPublicKey\x12%.metacrypt.v2.UserGetPublicKeyRequest\x1a&.metacrypt.v2.UserGetPublicKeyResponse\x12T\n" + + "\tListUsers\x12\".metacrypt.v2.UserListUsersRequest\x1a#.metacrypt.v2.UserListUsersResponse\x12N\n" + + "\aEncrypt\x12 .metacrypt.v2.UserEncryptRequest\x1a!.metacrypt.v2.UserEncryptResponse\x12N\n" + + "\aDecrypt\x12 .metacrypt.v2.UserDecryptRequest\x1a!.metacrypt.v2.UserDecryptResponse\x12T\n" + + "\tReEncrypt\x12\".metacrypt.v2.UserReEncryptRequest\x1a#.metacrypt.v2.UserReEncryptResponse\x12T\n" + + "\tRotateKey\x12\".metacrypt.v2.UserRotateKeyRequest\x1a#.metacrypt.v2.UserRotateKeyResponse\x12W\n" + + "\n" + + "DeleteUser\x12#.metacrypt.v2.UserDeleteUserRequest\x1a$.metacrypt.v2.UserDeleteUserResponseB>Z metacrypt.v2.UserRegisterRequest + 2, // 1: metacrypt.v2.UserService.Provision:input_type -> metacrypt.v2.UserProvisionRequest + 4, // 2: metacrypt.v2.UserService.GetPublicKey:input_type -> metacrypt.v2.UserGetPublicKeyRequest + 6, // 3: metacrypt.v2.UserService.ListUsers:input_type -> metacrypt.v2.UserListUsersRequest + 8, // 4: metacrypt.v2.UserService.Encrypt:input_type -> metacrypt.v2.UserEncryptRequest + 10, // 5: metacrypt.v2.UserService.Decrypt:input_type -> metacrypt.v2.UserDecryptRequest + 12, // 6: metacrypt.v2.UserService.ReEncrypt:input_type -> metacrypt.v2.UserReEncryptRequest + 14, // 7: metacrypt.v2.UserService.RotateKey:input_type -> metacrypt.v2.UserRotateKeyRequest + 16, // 8: metacrypt.v2.UserService.DeleteUser:input_type -> metacrypt.v2.UserDeleteUserRequest + 1, // 9: metacrypt.v2.UserService.Register:output_type -> metacrypt.v2.UserRegisterResponse + 3, // 10: metacrypt.v2.UserService.Provision:output_type -> metacrypt.v2.UserProvisionResponse + 5, // 11: metacrypt.v2.UserService.GetPublicKey:output_type -> metacrypt.v2.UserGetPublicKeyResponse + 7, // 12: metacrypt.v2.UserService.ListUsers:output_type -> metacrypt.v2.UserListUsersResponse + 9, // 13: metacrypt.v2.UserService.Encrypt:output_type -> metacrypt.v2.UserEncryptResponse + 11, // 14: metacrypt.v2.UserService.Decrypt:output_type -> metacrypt.v2.UserDecryptResponse + 13, // 15: metacrypt.v2.UserService.ReEncrypt:output_type -> metacrypt.v2.UserReEncryptResponse + 15, // 16: metacrypt.v2.UserService.RotateKey:output_type -> metacrypt.v2.UserRotateKeyResponse + 17, // 17: metacrypt.v2.UserService.DeleteUser:output_type -> metacrypt.v2.UserDeleteUserResponse + 9, // [9:18] is the sub-list for method output_type + 0, // [0:9] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_metacrypt_v2_user_proto_init() } +func file_proto_metacrypt_v2_user_proto_init() { + if File_proto_metacrypt_v2_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_user_proto_rawDesc), len(file_proto_metacrypt_v2_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_metacrypt_v2_user_proto_goTypes, + DependencyIndexes: file_proto_metacrypt_v2_user_proto_depIdxs, + MessageInfos: file_proto_metacrypt_v2_user_proto_msgTypes, + }.Build() + File_proto_metacrypt_v2_user_proto = out.File + file_proto_metacrypt_v2_user_proto_goTypes = nil + file_proto_metacrypt_v2_user_proto_depIdxs = nil +} diff --git a/gen/metacrypt/v2/user_grpc.pb.go b/gen/metacrypt/v2/user_grpc.pb.go new file mode 100644 index 0000000..23f2fcf --- /dev/null +++ b/gen/metacrypt/v2/user_grpc.pb.go @@ -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", +} diff --git a/internal/engine/user/types.go b/internal/engine/user/types.go new file mode 100644 index 0000000..35d4a5d --- /dev/null +++ b/internal/engine/user/types.go @@ -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"` +} diff --git a/internal/engine/user/user.go b/internal/engine/user/user.go new file mode 100644 index 0000000..6a1881b --- /dev/null +++ b/internal/engine/user/user.go @@ -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") + } +} diff --git a/internal/engine/user/user_test.go b/internal/engine/user/user_test.go new file mode 100644 index 0000000..f10a4ab --- /dev/null +++ b/internal/engine/user/user_test.go @@ -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"]) + } +} diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 5cc93f9..d37d855 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -81,6 +81,7 @@ func (s *GRPCServer) Start() error { pb.RegisterPKIServiceServer(s.srv, &pkiServer{s: s}) pb.RegisterCAServiceServer(s.srv, &caServer{s: s}) pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s}) + pb.RegisterUserServiceServer(s.srv, &userServer{s: s}) pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s}) lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr) @@ -133,6 +134,15 @@ func sealRequiredMethods() map[string]bool { "/metacrypt.v2.PolicyService/ListPolicies": true, "/metacrypt.v2.PolicyService/GetPolicy": 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/SetConfig": true, "/metacrypt.v2.ACMEService/ListAccounts": true, @@ -163,6 +173,15 @@ func authRequiredMethods() map[string]bool { "/metacrypt.v2.PolicyService/ListPolicies": true, "/metacrypt.v2.PolicyService/GetPolicy": 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/SetConfig": true, "/metacrypt.v2.ACMEService/ListAccounts": true, @@ -183,6 +202,8 @@ func adminRequiredMethods() map[string]bool { "/metacrypt.v2.CAService/DeleteCert": true, "/metacrypt.v2.PolicyService/CreatePolicy": true, "/metacrypt.v2.PolicyService/DeletePolicy": true, + "/metacrypt.v2.UserService/Provision": true, + "/metacrypt.v2.UserService/DeleteUser": true, "/metacrypt.v2.ACMEService/SetConfig": true, "/metacrypt.v2.ACMEService/ListAccounts": true, "/metacrypt.v2.ACMEService/ListOrders": true, diff --git a/internal/grpcserver/user.go b/internal/grpcserver/user.go new file mode 100644 index 0000000..ca247d6 --- /dev/null +++ b/internal/grpcserver/user.go @@ -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 +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 5822d54..980c0c5 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -45,6 +45,17 @@ func (s *Server) registerRoutes(r chi.Router) { r.Get("/v1/pki/{mount}/issuer/{name}", s.requireUnseal(s.handlePKIIssuer)) r.Get("/v1/pki/{mount}/issuer/{name}/crl", s.requireUnseal(s.handlePKICRL)) + // 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/rule", s.requireAuth(s.handlePolicyRule)) s.registerACMERoutes(r) @@ -608,10 +619,261 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) { return caEng, nil } +// --- 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) +} + +func (s *Server) writeEngineError(w http.ResponseWriter, err error) { + status := http.StatusInternalServerError + 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(), "too many"): + status = http.StatusBadRequest + } + http.Error(w, `{"error":"`+err.Error()+`"}`, status) +} + // operationAction maps an engine operation name to a policy action ("read" or "write"). func operationAction(op string) string { switch op { - case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer": + case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer", + "get-public-key", "list-users": return "read" default: return "write" diff --git a/proto/metacrypt/v2/user.proto b/proto/metacrypt/v2/user.proto new file mode 100644 index 0000000..626171e --- /dev/null +++ b/proto/metacrypt/v2/user.proto @@ -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 {}