forked from TrueCloudLab/frostfs-mfa
Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
e09ead8de2 | |||
7d9494960b | |||
7bca08b339 | |||
80fb768113 | |||
1f45e3d984 | |||
7145bd89e1 | |||
2c10d9920f | |||
4514a32e8d | |||
9462aea03d |
17 changed files with 1606 additions and 5 deletions
21
.forgejo/workflows/dco.yml
Normal file
21
.forgejo/workflows/dco.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: DCO action
|
||||||
|
on: [ pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dco:
|
||||||
|
name: DCO
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Run commit format checker
|
||||||
|
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||||
|
with:
|
||||||
|
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
28
.forgejo/workflows/govulncheck.yml
Normal file
28
.forgejo/workflows/govulncheck.yml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: Vulncheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vulncheck:
|
||||||
|
name: Vulncheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Install govulncheck
|
||||||
|
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|
||||||
|
- name: Run govulncheck
|
||||||
|
run: govulncheck ./...
|
57
.forgejo/workflows/tests.yml
Normal file
57
.forgejo/workflows/tests.yml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
name: Tests and linters
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: make lint
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go_versions: [ '1.22', '1.23' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '${{ matrix.go_versions }}'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
tests-race:
|
||||||
|
name: Tests with -race
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test ./... -count=1 -race
|
|
@ -73,7 +73,6 @@ linters:
|
||||||
- gocognit
|
- gocognit
|
||||||
- contextcheck
|
- contextcheck
|
||||||
- importas
|
- importas
|
||||||
- truecloudlab-linters
|
|
||||||
- perfsprint
|
- perfsprint
|
||||||
- testifylint
|
- testifylint
|
||||||
- protogetter
|
- protogetter
|
||||||
|
|
48
Makefile
Normal file
48
Makefile
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
include mk/*
|
||||||
|
|
||||||
|
GOBIN ?= $(shell go env GOPATH)/bin
|
||||||
|
|
||||||
|
PROTOC_VERSION ?= 25.6
|
||||||
|
PROTOC_ARCH = linux-x86_64
|
||||||
|
PROTOC = ./bin/protoc/bin/protoc
|
||||||
|
|
||||||
|
UNAME = "$(shell uname)/$(shell uname -m)"
|
||||||
|
ifeq ($(UNAME), "Darwin/arm64")
|
||||||
|
PROTOC_ARCH = osx-aarch_64
|
||||||
|
endif
|
||||||
|
ifeq ($(UNAME), "Darwin/x86_64")
|
||||||
|
PROTOC_ARCH = osx-x86_64
|
||||||
|
endif
|
||||||
|
|
||||||
|
PROTOC_URL ?= "https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)-$(PROTOC_ARCH).zip"
|
||||||
|
|
||||||
|
# Prepare binaries to generate protobuf files
|
||||||
|
.PHONY: protoc-bin
|
||||||
|
protoc-bin:
|
||||||
|
ifeq (,$(wildcard ./bin/protoc))
|
||||||
|
curl --create-dirs -o ./bin/protoc.zip -L'#' $(PROTOC_URL)
|
||||||
|
unzip ./bin/protoc.zip -d ./bin/protoc
|
||||||
|
rm -r ./bin/protoc.zip ./bin/protoc/include ./bin/protoc/readme.txt
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Generate protobuf file
|
||||||
|
.PHONY: protoc
|
||||||
|
protoc: protoc-bin
|
||||||
|
@go list -f '{{.Path}}/...@{{.Version}}' -m google.golang.org/protobuf | xargs echo go install -v
|
||||||
|
@for f in `find . -type f -name '*.proto'`; do \
|
||||||
|
echo "> Processing $$f "; \
|
||||||
|
$(PROTOC) --experimental_editions --plugin=protoc-gen-go=$(GOBIN)/protoc-gen-go --go_out=. --go_opt=paths=source_relative $$f \
|
||||||
|
--plugin=protoc-gen-go-grpc=$(GOBIN)/protoc-gen-go-grpc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false,paths=source_relative $$f; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run Unit Test with go test
|
||||||
|
.PHONY: test
|
||||||
|
test: GOFLAGS ?= "-count=1"
|
||||||
|
test:
|
||||||
|
@echo "> Running go test"
|
||||||
|
@GOFLAGS="$(GOFLAGS)" go test ./...
|
||||||
|
|
||||||
|
# Clean all installed files
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf ./bin/*
|
46
go.mod
Normal file
46
go.mod
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
module git.frostfs.info/TrueCloudLab/frostfs-mfa
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d
|
||||||
|
github.com/nspcc-dev/neo-go v0.106.3
|
||||||
|
github.com/pquerna/otp v1.4.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
|
google.golang.org/protobuf v1.36.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
|
||||||
|
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
|
github.com/nspcc-dev/rfc6979 v0.2.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/net v0.30.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||||
|
google.golang.org/grpc v1.69.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
103
go.sum
Normal file
103
go.sum
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU=
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824 h1:Mxw1c/8t96vFIUOffl28lFaHKi413oCBfLMGJmF9cFA=
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824/go.mod h1:kbwB4v2o6RyOfCo9kEFeUDZIX3LKhmS0yXPrtvzkQ1g=
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d h1:YrBjEkuc+q9RdqWmpxRL3DjYrO+9/YOYYIbeq0EVnQs=
|
||||||
|
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d/go.mod h1:aQpPWfG8oyfJ2X+FenPTJpSRWZjwcP5/RAtkW+/VEX8=
|
||||||
|
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA=
|
||||||
|
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
|
||||||
|
github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc=
|
||||||
|
github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
|
github.com/nspcc-dev/neo-go v0.106.3 h1:HEyhgkjQY+HfBzotMJ12xx2VuOUphkngZ4kEkjvXDtE=
|
||||||
|
github.com/nspcc-dev/neo-go v0.106.3/go.mod h1:3vEwJ2ld12N7HRGCaH/l/7EwopplC/+8XdIdPDNmD/M=
|
||||||
|
github.com/nspcc-dev/rfc6979 v0.2.1 h1:8wWxkamHWFmO790GsewSoKUSJjVnL1fmdRpokU/RgRM=
|
||||||
|
github.com/nspcc-dev/rfc6979 v0.2.1/go.mod h1:Tk7h5kyUWkhjyO3zUgFFhy1v2vQv3BvQEntakdtqrWc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||||
|
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||||
|
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y=
|
||||||
|
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||||
|
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||||
|
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||||
|
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
15
mfa/box.go
Normal file
15
mfa/box.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal returns the wire-format of MFABox.
|
||||||
|
func (x *MFABox) Marshal() ([]byte, error) {
|
||||||
|
return proto.Marshal(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the wire-format message and put data into MFABox.
|
||||||
|
func (x *MFABox) Unmarshal(data []byte) error {
|
||||||
|
return proto.Unmarshal(data, x)
|
||||||
|
}
|
130
mfa/device.go
Normal file
130
mfa/device.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Device defines MFA Device metadata.
|
||||||
|
Device struct {
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
OID oid.ID
|
||||||
|
Meta map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretDevice is an MFA device metadata with decoded OTP Key.
|
||||||
|
SecretDevice struct {
|
||||||
|
Device
|
||||||
|
Key *otp.Key
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FilePathKey = "FilePath"
|
||||||
|
OIDKey = "OID"
|
||||||
|
|
||||||
|
PathKey = "Path"
|
||||||
|
EnableDateKey = "EnableDate"
|
||||||
|
EnabledKey = "EnabledKey"
|
||||||
|
UserIDKey = "UserIDKey"
|
||||||
|
TagPrefix = "tag-"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDevice returns new device metadata. Device is disabled by default.
|
||||||
|
func NewDevice(namespace, name, path string) *Device {
|
||||||
|
return &Device{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
Meta: map[string]string{
|
||||||
|
EnabledKey: "false",
|
||||||
|
PathKey: path,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns string representation of device.
|
||||||
|
func (d *Device) String() string {
|
||||||
|
return d.Namespace + "/" + d.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableStatus returns true if device is enabled.
|
||||||
|
func (d *Device) EnableStatus() bool {
|
||||||
|
return d.Meta[EnabledKey] == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnableStatus updates enable status of device.
|
||||||
|
// It does not modify enable date.
|
||||||
|
func (d *Device) SetEnableStatus(enabled bool) {
|
||||||
|
d.Meta[EnabledKey] = strconv.FormatBool(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableDate returns date when device was enabled.
|
||||||
|
func (d *Device) EnableDate() *time.Time {
|
||||||
|
return dateFromString(d.Meta[EnableDateKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnableDate sets date when device was enabled.
|
||||||
|
// It is not affected by 'SetEnableStatus'.
|
||||||
|
func (d *Device) SetEnableDate(date *time.Time) {
|
||||||
|
d.Meta[EnableDateKey] = convertDate(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserID sets id of the device owner. Use neo wallet address representation.
|
||||||
|
func (d *Device) SetUserID(addr string) {
|
||||||
|
d.Meta[UserIDKey] = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserID returns id of the device owner.
|
||||||
|
func (d *Device) UserID() string {
|
||||||
|
return d.Meta[UserIDKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTags adds new set of tags to the device.
|
||||||
|
// It does not remove existing set of tags.
|
||||||
|
func (d *Device) AddTags(tags [][2]string) {
|
||||||
|
for _, kv := range tags {
|
||||||
|
k, v := kv[0], kv[1]
|
||||||
|
d.Meta[TagPrefix+k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTags removes all tags with provided keys.
|
||||||
|
func (d *Device) DeleteTags(keys []string) {
|
||||||
|
for _, k := range keys {
|
||||||
|
delete(d.Meta, TagPrefix+k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags returns available set of tags of the device.
|
||||||
|
func (d *Device) Tags() map[string]string {
|
||||||
|
tags := make(map[string]string)
|
||||||
|
for k, v := range d.Meta {
|
||||||
|
if after, ok := strings.CutPrefix(k, TagPrefix); ok {
|
||||||
|
tags[after] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertDate(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateFromString(timeString string) *time.Time {
|
||||||
|
var date *time.Time
|
||||||
|
enableD, err := time.Parse(time.RFC3339, timeString)
|
||||||
|
if err == nil {
|
||||||
|
date = &enableD
|
||||||
|
}
|
||||||
|
return date
|
||||||
|
}
|
8
mfa/errors.go
Normal file
8
mfa/errors.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrMFAEntityAlreadyExists returned during MFA device creation by MFA Manager.
|
||||||
|
ErrMFAEntityAlreadyExists = errors.New("mfa entity already exists")
|
||||||
|
)
|
417
mfa/mfa.go
Normal file
417
mfa/mfa.go
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Manager provides interface to manage MFA devices in FrostFS container.
|
||||||
|
// It should be provided with Storage interface to manage FrostFS objects
|
||||||
|
// and KeyStore interface to encode and decode OTP keys inside FrostFS
|
||||||
|
// objects.
|
||||||
|
Manager struct {
|
||||||
|
storage Storage
|
||||||
|
unlocker KeyStore
|
||||||
|
settings Settings
|
||||||
|
container cid.ID
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyStore is an interface for Manager to provide keys to encode and decode
|
||||||
|
// OTP keys of MFA devices.
|
||||||
|
KeyStore interface {
|
||||||
|
// PrivateKey returns private key of this Manager.
|
||||||
|
PrivateKey() *keys.PrivateKey
|
||||||
|
// PublicKeys returns list of public keys for all managers, including
|
||||||
|
// this Manager.
|
||||||
|
PublicKeys() []*keys.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains parameters for Manager constructor.
|
||||||
|
Config struct {
|
||||||
|
Storage Storage
|
||||||
|
Unlocker KeyStore
|
||||||
|
Settings Settings
|
||||||
|
Container cid.ID
|
||||||
|
Logger *zap.Logger
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewManager creates new instance of Manager.
|
||||||
|
func NewManager(cfg Config) (*Manager, error) {
|
||||||
|
if cfg.Storage == nil {
|
||||||
|
return nil, errors.New("mfa storage is nil")
|
||||||
|
}
|
||||||
|
if cfg.Logger == nil {
|
||||||
|
return nil, errors.New("mfa logger is nil")
|
||||||
|
}
|
||||||
|
if cfg.Unlocker == nil {
|
||||||
|
return nil, errors.New("mfa key store is nil")
|
||||||
|
}
|
||||||
|
if cfg.Settings == nil {
|
||||||
|
return nil, errors.New("mfa settings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{
|
||||||
|
storage: cfg.Storage,
|
||||||
|
container: cfg.Container,
|
||||||
|
unlocker: cfg.Unlocker,
|
||||||
|
logger: cfg.Logger,
|
||||||
|
settings: cfg.Settings,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMFADevice creates new FrostFS object with encoded MFA device inside and stores it in MFA container.
|
||||||
|
func (m *Manager) CreateMFADevice(ctx context.Context, device SecretDevice) error {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.CreateMFADevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
filePath := getTreePath(device.Namespace, device.Name)
|
||||||
|
if _, err := m.storage.GetTreeNode(ctx, m.container, filePath); err == nil {
|
||||||
|
return ErrMFAEntityAlreadyExists // return well known error here
|
||||||
|
} else if !errors.Is(err, ErrTreeNodeNotFound) {
|
||||||
|
return fmt.Errorf("get mfa node '%s' to check: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.putMFADevice(ctx, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMFADevice returns decoded MFA device from MFA container.
|
||||||
|
func (m *Manager) GetMFADevice(ctx context.Context, ns, mfaName string) (*SecretDevice, error) {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetMFADevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get mfa nodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var objID oid.ID
|
||||||
|
if err = objID.DecodeString(node.Current.Meta[OIDKey]); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode oid '%s': %w", node.Current.Meta[OIDKey], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(m.container)
|
||||||
|
addr.SetObject(objID)
|
||||||
|
|
||||||
|
boxData, err := m.storage.GetObject(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaBox := new(MFABox)
|
||||||
|
if err = mfaBox.Unmarshal(boxData); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal box data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unpack mfa box: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := otp.NewKeyFromURL(secrets.URL())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, err := newDevice(&node.Current)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SecretDevice{
|
||||||
|
Device: *dev,
|
||||||
|
Key: key,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTinyMFADevice returns MFA device metadata without OTP key from the tree of MFA container.
|
||||||
|
func (m *Manager) GetTinyMFADevice(ctx context.Context, ns, mfaName string) (*Device, error) {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetTinyMFADevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get mfa nodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, err := newDevice(&node.Current)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMFADevices lists all available MFA device metadata with specified device namespace from the tree of MFA container.
|
||||||
|
func (m *Manager) ListMFADevices(ctx context.Context, ns string) ([]*Device, error) {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListMFADevices")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
list, err := m.storage.GetTreeNodes(ctx, m.container, ns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.formDevices(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllMFADevices lists all available MFA device metadata from the tree of MFA container.
|
||||||
|
func (m *Manager) ListAllMFADevices(ctx context.Context) ([]*Device, error) {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListAllMFADevices")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
list, err := m.storage.GetTreeNodes(ctx, m.container, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.formDevices(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMFADevice updates MFA device metadata in the tree of MFA container.
|
||||||
|
func (m *Manager) UpdateMFADevice(ctx context.Context, device *Device) error {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.UpdateMFADevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
node, err := m.storage.SetTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name), device.Meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("set mfa tree node : %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.deleteObjects(ctx, node.Old)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMFADevice removes FrostFS object and metadata from the tree of MFA container related to device.
|
||||||
|
func (m *Manager) DeleteMFADevice(ctx context.Context, device *Device) error {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.DeleteMFADevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
meta := device.Meta
|
||||||
|
var objID oid.ID
|
||||||
|
if err := objID.DecodeString(meta[OIDKey]); err != nil {
|
||||||
|
return fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
address := oid.Address{}
|
||||||
|
address.SetObject(objID)
|
||||||
|
address.SetContainer(m.container)
|
||||||
|
|
||||||
|
if err := m.storage.DeleteObject(ctx, address); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete from storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
old, err := m.storage.DeleteTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name))
|
||||||
|
if err != nil {
|
||||||
|
m.deleteObjects(ctx, old)
|
||||||
|
return fmt.Errorf("failed to delete from tree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecipherDevice creates new FrostFS object with encoded OTP key for new set
|
||||||
|
// of public keys from KeyManager. If existing FrostFS object already contains
|
||||||
|
// all public keys from KeyManager, then does nothing.
|
||||||
|
func (m *Manager) RecipherDevice(ctx context.Context, dev *Device) error {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.RecipherDevice")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(dev.Namespace, dev.Name))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get mfa nodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dev.OID.EncodeToString() != node.Current.Meta[OIDKey] {
|
||||||
|
// This can happen in the following cases:
|
||||||
|
// * device was reciphered by other Manager
|
||||||
|
// * device was removed and newly created with the same name
|
||||||
|
// in both cases it must be already encrypted for new list of keys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(m.container)
|
||||||
|
addr.SetObject(dev.OID)
|
||||||
|
|
||||||
|
boxData, err := m.storage.GetObject(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaBox := new(MFABox)
|
||||||
|
if err = mfaBox.Unmarshal(boxData); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal box data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys := m.unlocker.PublicKeys()
|
||||||
|
oldKeys := mfaBox.GetUnlockers()
|
||||||
|
if equalKeys(newKeys, oldKeys) {
|
||||||
|
m.logger.Info("mfabox has already been reciphered",
|
||||||
|
zap.String("device", dev.String()),
|
||||||
|
zap.String("OID", dev.OID.EncodeToString()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unpack mfa box: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := otp.NewKeyFromURL(secrets.URL())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.putMFADevice(ctx, SecretDevice{
|
||||||
|
Device: *dev,
|
||||||
|
Key: key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) putMFADevice(ctx context.Context, device SecretDevice) error {
|
||||||
|
filePath := device.Namespace + "/" + device.Name
|
||||||
|
|
||||||
|
box, err := PackMFABox(device.Key, m.unlocker.PublicKeys())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pack mfa box: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boxData, err := box.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal mfa box: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var owner user.ID
|
||||||
|
user.IDFromKey(&owner, m.unlocker.PrivateKey().PrivateKey.PublicKey)
|
||||||
|
|
||||||
|
objID, err := m.storage.CreateObject(ctx, PrmObjectCreate{
|
||||||
|
Container: m.container,
|
||||||
|
Owner: owner,
|
||||||
|
FilePath: filePath,
|
||||||
|
Payload: boxData,
|
||||||
|
CopiesNumber: m.settings.CopiesNumber(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("put mfa box object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := map[string]string{
|
||||||
|
FilePathKey: filePath,
|
||||||
|
OIDKey: objID.EncodeToString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range device.Meta {
|
||||||
|
if k != FilePathKey && k != OIDKey {
|
||||||
|
meta[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := m.storage.SetTreeNode(ctx, m.container, filePath, meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("set mfa tree node : %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.deleteObjects(ctx, node.Old)
|
||||||
|
|
||||||
|
m.deleteObject(ctx, device.OID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) formDevices(list []*TreeNode) ([]*Device, error) {
|
||||||
|
res := make([]*Device, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
dev, err := newDevice(item)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("invalid mfa device tree node", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) deleteObjects(ctx context.Context, nodes []*TreeNode) {
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(m.container)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
var objID oid.ID
|
||||||
|
if err := objID.DecodeString(node.Meta[OIDKey]); err != nil {
|
||||||
|
m.logger.Warn("failed to decode old multinode oid", zap.String("oid", node.Meta[OIDKey]), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.SetObject(objID)
|
||||||
|
if err := m.storage.DeleteObject(ctx, addr); err != nil {
|
||||||
|
m.logger.Warn("failed to delete old multinode object", zap.String("address", addr.EncodeToString()), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) deleteObject(ctx context.Context, objID oid.ID) {
|
||||||
|
if objID.Equals(oid.ID{}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(m.container)
|
||||||
|
addr.SetObject(objID)
|
||||||
|
if err := m.storage.DeleteObject(ctx, addr); err != nil {
|
||||||
|
m.logger.Warn("failed to delete object", zap.String("address", addr.EncodeToString()), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDevice(node *TreeNode) (*Device, error) {
|
||||||
|
meta := node.Meta
|
||||||
|
filepathArr := strings.Split(meta[FilePathKey], "/")
|
||||||
|
if len(filepathArr) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid device filepath: '%s'", meta[FilePathKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
var objID oid.ID
|
||||||
|
if err := objID.DecodeString(meta[OIDKey]); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Device{
|
||||||
|
Namespace: filepathArr[0],
|
||||||
|
Name: filepathArr[1],
|
||||||
|
OID: objID,
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// equalKeys returns true if 'got' contains all public keys from 'expected' slice.
|
||||||
|
func equalKeys(expected []*keys.PublicKey, got []*Unlocker) bool {
|
||||||
|
loop:
|
||||||
|
for _, newKey := range expected {
|
||||||
|
for _, svcKey := range got {
|
||||||
|
if bytes.Equal(newKey.Bytes(), svcKey.GetPublicKey()) {
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTreePath(ns, mfaName string) string {
|
||||||
|
return ns + "/" + mfaName
|
||||||
|
}
|
289
mfa/mfabox.pb.go
generated
Normal file
289
mfa/mfabox.pb.go
generated
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.4
|
||||||
|
// protoc v4.25.6
|
||||||
|
// source: mfa/mfabox.proto
|
||||||
|
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unlocker is a message that contains encrypted key which has been used during
|
||||||
|
// encryption of 'Secrets' message in 'EncryptedSecrets' field of MFABox.
|
||||||
|
type Unlocker struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// PublicKeys is 33-byte ECDSA P-256 curve public key which identifies
|
||||||
|
// unlocker who can decrypt 'Secrets'.
|
||||||
|
PublicKey []byte `protobuf:"bytes,1,opt,name=PublicKey,json=publicKey" json:"PublicKey,omitempty"`
|
||||||
|
// EncryptedSecretsKey is a binary encoded encryption key of MFA Secrets,
|
||||||
|
// encrypted by ChaCha20-Poly1305 AEAD algorithm.
|
||||||
|
EncryptedSecretsKey []byte `protobuf:"bytes,2,opt,name=EncryptedSecretsKey,json=encryptedSecretsKey" json:"EncryptedSecretsKey,omitempty"`
|
||||||
|
// Salt for HKDF function to derive key for encryption of 'EncryptedSecreteKey'.
|
||||||
|
Salt []byte `protobuf:"bytes,3,opt,name=Salt,json=salt" json:"Salt,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Unlocker) Reset() {
|
||||||
|
*x = Unlocker{}
|
||||||
|
mi := &file_mfa_mfabox_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Unlocker) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Unlocker) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Unlocker) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mfa_mfabox_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 Unlocker.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Unlocker) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mfa_mfabox_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Unlocker) GetPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.PublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Unlocker) GetEncryptedSecretsKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.EncryptedSecretsKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Unlocker) GetSalt() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Salt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MFABox struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// Unlockers are the set of messages contain key that has been used
|
||||||
|
// to encrypt 'Secrets' message in 'EncrytedSecrets' field.
|
||||||
|
Unlockers []*Unlocker `protobuf:"bytes,1,rep,name=Unlockers,json=unlockers" json:"Unlockers,omitempty"`
|
||||||
|
// ECDHPublicKey is 33-byte ECDSA P-256 curve key to derive
|
||||||
|
// unique encryption keys for every unlocker with ECDH algorithm
|
||||||
|
ECDHPublicKey []byte `protobuf:"bytes,2,opt,name=ECDHPublicKey,json=ecdhPublicKey" json:"ECDHPublicKey,omitempty"`
|
||||||
|
// EncryptedSecrets is a binary encoded 'Secrets' message, encrypted by
|
||||||
|
// ChaCha20-Poly1305 AEAD algorithm.
|
||||||
|
EncryptedSecrets []byte `protobuf:"bytes,3,opt,name=EncryptedSecrets,json=encryptedSecrets" json:"EncryptedSecrets,omitempty"`
|
||||||
|
// Salt for HKDF function to derive key for encryption of 'EncryptedSecrets'.
|
||||||
|
Salt []byte `protobuf:"bytes,4,opt,name=Salt,json=salt" json:"Salt,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) Reset() {
|
||||||
|
*x = MFABox{}
|
||||||
|
mi := &file_mfa_mfabox_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MFABox) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MFABox) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mfa_mfabox_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 MFABox.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MFABox) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mfa_mfabox_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) GetUnlockers() []*Unlocker {
|
||||||
|
if x != nil {
|
||||||
|
return x.Unlockers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) GetECDHPublicKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ECDHPublicKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) GetEncryptedSecrets() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.EncryptedSecrets
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MFABox) GetSalt() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Salt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets is a message that contains private data about MFA Device
|
||||||
|
type Secrets struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// MFAURL is a seed for virtual authenticator device.
|
||||||
|
// Format is described in https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||||
|
MFAURL *string `protobuf:"bytes,2,opt,name=MFAURL,json=mfaURL" json:"MFAURL,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Secrets) Reset() {
|
||||||
|
*x = Secrets{}
|
||||||
|
mi := &file_mfa_mfabox_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Secrets) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Secrets) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Secrets) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mfa_mfabox_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 Secrets.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Secrets) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mfa_mfabox_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Secrets) GetMFAURL() string {
|
||||||
|
if x != nil && x.MFAURL != nil {
|
||||||
|
return *x.MFAURL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_mfa_mfabox_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_mfa_mfabox_proto_rawDesc = string([]byte{
|
||||||
|
0x0a, 0x10, 0x6d, 0x66, 0x61, 0x2f, 0x6d, 0x66, 0x61, 0x62, 0x6f, 0x78, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
|
0x74, 0x6f, 0x12, 0x03, 0x6d, 0x66, 0x61, 0x22, 0x6e, 0x0a, 0x08, 0x55, 0x6e, 0x6c, 0x6f, 0x63,
|
||||||
|
0x6b, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
|
||||||
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
|
||||||
|
0x79, 0x12, 0x30, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65,
|
||||||
|
0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13,
|
||||||
|
0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
|
||||||
|
0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x61, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
|
0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x06, 0x4d, 0x46, 0x41, 0x42,
|
||||||
|
0x6f, 0x78, 0x12, 0x2b, 0x0a, 0x09, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x73, 0x18,
|
||||||
|
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x55, 0x6e, 0x6c, 0x6f,
|
||||||
|
0x63, 0x6b, 0x65, 0x72, 0x52, 0x09, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x73, 0x12,
|
||||||
|
0x24, 0x0a, 0x0d, 0x45, 0x43, 0x44, 0x48, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
|
||||||
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x63, 0x64, 0x68, 0x50, 0x75, 0x62, 0x6c,
|
||||||
|
0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
|
||||||
|
0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
|
0x10, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||||
|
0x73, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x61, 0x6c, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
|
0x04, 0x73, 0x61, 0x6c, 0x74, 0x22, 0x21, 0x0a, 0x07, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
|
||||||
|
0x12, 0x16, 0x0a, 0x06, 0x4d, 0x46, 0x41, 0x55, 0x52, 0x4c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x06, 0x6d, 0x66, 0x61, 0x55, 0x52, 0x4c, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x6d, 0x66, 0x61,
|
||||||
|
0x62, 0x08, 0x65, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07,
|
||||||
|
})
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_mfa_mfabox_proto_rawDescOnce sync.Once
|
||||||
|
file_mfa_mfabox_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_mfa_mfabox_proto_rawDescGZIP() []byte {
|
||||||
|
file_mfa_mfabox_proto_rawDescOnce.Do(func() {
|
||||||
|
file_mfa_mfabox_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mfa_mfabox_proto_rawDesc), len(file_mfa_mfabox_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_mfa_mfabox_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_mfa_mfabox_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||||
|
var file_mfa_mfabox_proto_goTypes = []any{
|
||||||
|
(*Unlocker)(nil), // 0: mfa.Unlocker
|
||||||
|
(*MFABox)(nil), // 1: mfa.MFABox
|
||||||
|
(*Secrets)(nil), // 2: mfa.Secrets
|
||||||
|
}
|
||||||
|
var file_mfa_mfabox_proto_depIdxs = []int32{
|
||||||
|
0, // 0: mfa.MFABox.Unlockers:type_name -> mfa.Unlocker
|
||||||
|
1, // [1:1] is the sub-list for method output_type
|
||||||
|
1, // [1:1] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_mfa_mfabox_proto_init() }
|
||||||
|
func file_mfa_mfabox_proto_init() {
|
||||||
|
if File_mfa_mfabox_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mfa_mfabox_proto_rawDesc), len(file_mfa_mfabox_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 3,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 0,
|
||||||
|
},
|
||||||
|
GoTypes: file_mfa_mfabox_proto_goTypes,
|
||||||
|
DependencyIndexes: file_mfa_mfabox_proto_depIdxs,
|
||||||
|
MessageInfos: file_mfa_mfabox_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_mfa_mfabox_proto = out.File
|
||||||
|
file_mfa_mfabox_proto_goTypes = nil
|
||||||
|
file_mfa_mfabox_proto_depIdxs = nil
|
||||||
|
}
|
44
mfa/mfabox.proto
Normal file
44
mfa/mfabox.proto
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
edition = "2023";
|
||||||
|
|
||||||
|
package mfa;
|
||||||
|
|
||||||
|
option go_package = "/mfa";
|
||||||
|
|
||||||
|
// Unlocker is a message that contains encrypted key which has been used during
|
||||||
|
// encryption of 'Secrets' message in 'EncryptedSecrets' field of MFABox.
|
||||||
|
message Unlocker {
|
||||||
|
// PublicKeys is 33-byte ECDSA P-256 curve public key which identifies
|
||||||
|
// unlocker who can decrypt 'Secrets'.
|
||||||
|
bytes PublicKey = 1 [json_name = "publicKey"];
|
||||||
|
|
||||||
|
// EncryptedSecretsKey is a binary encoded encryption key of MFA Secrets,
|
||||||
|
// encrypted by ChaCha20-Poly1305 AEAD algorithm.
|
||||||
|
bytes EncryptedSecretsKey = 2 [json_name = "encryptedSecretsKey"];
|
||||||
|
|
||||||
|
// Salt for HKDF function to derive key for encryption of 'EncryptedSecreteKey'.
|
||||||
|
bytes Salt = 3 [json_name = "salt"];
|
||||||
|
}
|
||||||
|
|
||||||
|
message MFABox {
|
||||||
|
// Unlockers are the set of messages contain key that has been used
|
||||||
|
// to encrypt 'Secrets' message in 'EncrytedSecrets' field.
|
||||||
|
repeated Unlocker Unlockers = 1 [json_name = "unlockers"];
|
||||||
|
|
||||||
|
// ECDHPublicKey is 33-byte ECDSA P-256 curve key to derive
|
||||||
|
// unique encryption keys for every unlocker with ECDH algorithm
|
||||||
|
bytes ECDHPublicKey = 2 [json_name = "ecdhPublicKey"];
|
||||||
|
|
||||||
|
// EncryptedSecrets is a binary encoded 'Secrets' message, encrypted by
|
||||||
|
// ChaCha20-Poly1305 AEAD algorithm.
|
||||||
|
bytes EncryptedSecrets = 3 [json_name = "encryptedSecrets"];
|
||||||
|
|
||||||
|
// Salt for HKDF function to derive key for encryption of 'EncryptedSecrets'.
|
||||||
|
bytes Salt = 4 [json_name = "salt"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets is a message that contains private data about MFA Device
|
||||||
|
message Secrets {
|
||||||
|
// MFAURL is a seed for virtual authenticator device.
|
||||||
|
// Format is described in https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||||
|
string MFAURL = 2 [json_name = "mfaURL"];
|
||||||
|
}
|
229
mfa/pack.go
Normal file
229
mfa/pack.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
secretLength = 32
|
||||||
|
saltLength = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackMFABox encrypts OTP Key in a MFABox. Holders of unlocker private keys
|
||||||
|
// can unpack this object and decrypt OTP Key.
|
||||||
|
func PackMFABox(secret *otp.Key, unlockerKeys []*keys.PublicKey) (*MFABox, error) {
|
||||||
|
if len(unlockerKeys) == 0 {
|
||||||
|
return nil, errors.New("no unlocker keys provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First step: generate encryption key and encrypt secret data with it.
|
||||||
|
secretURL := secret.URL()
|
||||||
|
|
||||||
|
// prepare MFA secret for encryption
|
||||||
|
data, err := proto.Marshal(&Secrets{MFAURL: &secretURL})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate symmetric key to encrypt MFA secret
|
||||||
|
secretEncryptionKey, err := generateRandomBytes(secretLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate secrets encryption key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt MFA secret with ChaCha20-Poly1305 AEAD algorithm
|
||||||
|
encryptedSecrets, hkdfsalt, err := encryptData(data, secretEncryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encrypt secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second step: for each unlocker, encrypt secret encryption key, so
|
||||||
|
// each unlocker could decrypt encryption key and then decrypt MFA secret with it.
|
||||||
|
|
||||||
|
// generate ECDSA P-256 curve private key to derive unique encryption
|
||||||
|
// key for every unlocker with ECDH algorithm.
|
||||||
|
ecdhKey, err := keys.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create private key for ECDH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockers := make([]*Unlocker, len(unlockerKeys))
|
||||||
|
for i := range unlockerKeys {
|
||||||
|
unlockers[i], err = packUnlocker(secretEncryptionKey, ecdhKey, unlockerKeys[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create unlocker: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MFABox{
|
||||||
|
Unlockers: unlockers,
|
||||||
|
ECDHPublicKey: ecdhKey.PublicKey().Bytes(),
|
||||||
|
EncryptedSecrets: encryptedSecrets,
|
||||||
|
Salt: hkdfsalt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackMFABox decrypts OTP key using unlocker key.
|
||||||
|
func UnpackMFABox(box *MFABox, unlockerKey *keys.PrivateKey) (*otp.Key, error) {
|
||||||
|
unlockerPublicKey := unlockerKey.PublicKey().Bytes()
|
||||||
|
ecdhKey, err := keys.NewPublicKeyFromBytes(box.GetECDHPublicKey(), elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse ECDH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First step: find unlocker message for unlocker key
|
||||||
|
var suitableUnlocker *Unlocker
|
||||||
|
for _, unlocker := range box.GetUnlockers() {
|
||||||
|
if bytes.Equal(unlockerPublicKey, unlocker.GetPublicKey()) {
|
||||||
|
suitableUnlocker = unlocker
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if suitableUnlocker == nil {
|
||||||
|
return nil, fmt.Errorf("no unlocker for %x", unlockerPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second step: decrypt encryption key of MFA secret
|
||||||
|
secretEncryptionKey, err := unpackUnlocker(suitableUnlocker, ecdhKey, unlockerKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unpack unlocker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third step: decrypt MFA secret
|
||||||
|
data, err := decryptData(box.GetEncryptedSecrets(), secretEncryptionKey, box.GetSalt())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := new(Secrets)
|
||||||
|
if err = proto.Unmarshal(data, secrets); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := otp.NewKeyFromURL(secrets.GetMFAURL())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse OTP key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packUnlocker(data []byte, ecdhKey *keys.PrivateKey, unlockerKey *keys.PublicKey) (*Unlocker, error) {
|
||||||
|
// derive unique encryption key for unlocker with ECDH algorithm
|
||||||
|
uniqueUnlockerKey, err := deriveECDH(ecdhKey, unlockerKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate ECDH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt data based on unique encryption key
|
||||||
|
encryptedData, salt, err := encryptData(data, uniqueUnlockerKey)
|
||||||
|
|
||||||
|
return &Unlocker{
|
||||||
|
PublicKey: unlockerKey.Bytes(),
|
||||||
|
EncryptedSecretsKey: encryptedData,
|
||||||
|
Salt: salt,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackUnlocker(unlocker *Unlocker, ecdhKey *keys.PublicKey, unlockerKey *keys.PrivateKey) ([]byte, error) {
|
||||||
|
// derive unique encryption key for unlocker with ECDH algorithm
|
||||||
|
uniqueUnlockerKey, err := deriveECDH(unlockerKey, ecdhKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate ECDH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptData(unlocker.GetEncryptedSecretsKey(), uniqueUnlockerKey, unlocker.GetSalt())
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptData(data, encryptionKey []byte) (encryptedData []byte, salt []byte, err error) {
|
||||||
|
// generate salt for HKDF key derive function
|
||||||
|
salt, err = generateRandomBytes(saltLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("generate HKDF salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ChaCha20-Poly1305 AEAD cipher based on
|
||||||
|
// a key derived from encryptionKey and random salt
|
||||||
|
enc, err := getCipher(encryptionKey, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("prepare AEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random nonce to encrypt data
|
||||||
|
nonce := make([]byte, enc.NonceSize())
|
||||||
|
_, err = rand.Read(nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("generate random nonce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.Seal(nonce, nonce, data, nil), salt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptData(encryptedData, encryptionKey, salt []byte) (data []byte, err error) {
|
||||||
|
// get ChaCha20-Poly1305 AEAD cipher based on
|
||||||
|
// a key derived from encryptionKey and random salt
|
||||||
|
dec, err := getCipher(encryptionKey, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare AEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ld, ns := len(encryptedData), dec.NonceSize()
|
||||||
|
if ld < ns {
|
||||||
|
return nil, fmt.Errorf("data size %d, should be greater than nonce size %d", ld, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, cypher := encryptedData[:dec.NonceSize()], encryptedData[dec.NonceSize():]
|
||||||
|
|
||||||
|
return dec.Open(nil, nonce, cypher, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCipher(secret, salt []byte) (cipher.AEAD, error) {
|
||||||
|
key, err := deriveHKDF(secret, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("derive key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chacha20poly1305.NewX(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveECDH(prv *keys.PrivateKey, pub *keys.PublicKey) ([]byte, error) {
|
||||||
|
prvECDH, err := prv.ECDH()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ECDH private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubECDH, err := (*ecdsa.PublicKey)(pub).ECDH()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ECDH public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prvECDH.ECDH(pubECDH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveHKDF(secret, salt []byte) ([]byte, error) {
|
||||||
|
hash := sha256.New
|
||||||
|
kdf := hkdf.New(hash, secret, salt, nil)
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := io.ReadFull(kdf, key)
|
||||||
|
return key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomBytes(length int) ([]byte, error) {
|
||||||
|
b := make([]byte, length)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
return b, err
|
||||||
|
}
|
105
mfa/pack_test.go
Normal file
105
mfa/pack_test.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackUnpackBox(t *testing.T) {
|
||||||
|
unlockerKeys := generateKeys(t, 3)
|
||||||
|
unlockerPublicKeys := make([]*keys.PublicKey, len(unlockerKeys))
|
||||||
|
for i, key := range unlockerKeys {
|
||||||
|
unlockerPublicKeys[i] = key.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
otpKey, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
Issuer: "iam-" + hex.EncodeToString(unlockerPublicKeys[0].Bytes()),
|
||||||
|
AccountName: "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM (devenv)",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
box, err := PackMFABox(otpKey, unlockerPublicKeys)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// make sure secrets are encrypted
|
||||||
|
secrets := new(Secrets)
|
||||||
|
err = proto.Unmarshal(box.GetEncryptedSecrets(), secrets)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// make sure MFA secret encryption key is encrypted
|
||||||
|
_, err = decryptData(box.EncryptedSecrets, box.Unlockers[0].EncryptedSecretsKey, box.Salt)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
for _, unlockerKey := range unlockerKeys {
|
||||||
|
otpKeyFromBox, err := UnpackMFABox(box, unlockerKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, otpKey.URL(), otpKeyFromBox.URL())
|
||||||
|
}
|
||||||
|
|
||||||
|
randomKey := generateKeys(t, 1)
|
||||||
|
_, err = UnpackMFABox(box, randomKey[0])
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProprietaryCompatibility(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
binary string // base64 encoded MFA Box
|
||||||
|
otpURL string // secret
|
||||||
|
unlockerPrivateKeys []string // hex-encoded private keys
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// this is MFA box created with proprietary code with HKDF salt
|
||||||
|
binary: "Cn8KIQNSH3wIalCMX35hjaEV9sYCJYJ9QC1EFy1eTH/ZaTdaIxJIl6m1UcOuwmtkkcwIqLbX+DNiIvzkGw29YiWbyycvWh048nf4phBKXkzIMy7GXzKJz1n3BWV7q9QPezpLkjeU1Nn2u1czyXrnGhCJ7VGazNvBg50zsokWEXXWCn8KIQK/IVlNTlmdqA0+XxuUR5KjRvEOYVmyO9JjUMSGaFb0ERJIlmYTw2HcLvscJCLiLLGBh+lXRaFUOqCYgKwwT5G352cYJnMejeQ6QlIlzygHTn9N+RHvh4Hmy5Pt1SUik6hVvfil/9LsX8T0GhDHh7f4v3mjmSgU4T4mjX7lCn8KIQLnAI+bca1SzSSPtsp9n6k2+uC4pVPw4rY0eYKUu6fJ4hJI5Pb7qVzFMDQcTLxLrA0vIQ4DEnwGt78QMdZaHM4FqosIMO0C6TLoTUOyXSgK/scF785JEH0freFiOh+fR5OqQNCrLHMjzoL4GhA2xmq4WmFuRaToK9pm4B0YEiEDWC0ggRO0nfUoYBDO58amY6Y13Zs4h05gjLXIp23y8r4avwEoZqY9aG/sHlso1pRtMtSUJdFGpsONIQvhG3nnJHEy1vbDiVzmThK9Onn2huX5vGJ4iKZX8sj2pd9/yo+twqKA9osNQDlVnJikd5wapnPWKKxjAN22pIQC3TJV0AXJdtklg39YO9H0cz5r2ZDFTWOlCZdOnBxoDW3CyxUX+lQMA5GJ/v+zVPQpCu8no8z1NdLNbiIHI6Gn6e0hfSENMeizH3QvKLLaAqR7VUK3dHNAhyHquIe05xt28RImSnZewCIQf3et/IfqVLm8QU9kcfGIFA==",
|
||||||
|
otpURL: "otpauth://totp/iam-test:NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM?algorithm=SHA1&digits=6&issuer=iam-test&period=30&secret=2EIZ6JTGWBZHYEMTFQGTCWLUIGHMNU2S",
|
||||||
|
unlockerPrivateKeys: []string{
|
||||||
|
"b4e6b0cfd844f454a1cd0c726455356aa2120743d112805cfb05a67202b6a7a4",
|
||||||
|
"679f5eeab1a885ae7c8c2992149145265f46458cacc8191f6b6902aa7e7efdc8",
|
||||||
|
"e3ac3501472ab144dfc99d61b69e3fccc6ffa63519a728307624e9aa1e99350d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this is MFA box created with proprietary code without HKDF salt
|
||||||
|
binary: "Cm0KIQPjDLGDyyM2KTMx7JxNGYCcY+8V74wgFehEc10gxPde4RJIGWiM6mxo9q0EF8rp/pdFULNy9O+BQc89NIess3z5nSmWFFZxx7j/yKF22hRxLQYenxciHBC0PhbNGXoHG7H4q1a9rhlftcdbCm0KIQOxT6G7TamFLQnktNXXkPrVEXgO4w6yEJ7SQScsXc30EBJI7PSxnqIZmyufYiYkhmwhtP9OSziIA2mFbSz+Wwp45XLKkLYVsT+1Bvj2fKLPZdb5LIPielfRXZpoCrHo8jSDigFae5zxBfsSCm0KIQPEdTuOmmavHwgjPCCgh2ldCTNpubMNzcUc7zakNTDB9hJIl25dRwdxqdV1secWXnhlX6QGWOy8jIswE3MFP0mHhE5yy9jctpIhQKctOCtdsDdMZ081T72omxEc8wRdOK83/cE54P8G7cjPEiECRnc7bPPWVRZtcd5xc4Vvu4LXak16RwA09x4oKsYMB/MavwELRmMpyMwoFgixJrCdFg7PFML4SEb9LE2sj361psAS4kslwIDWMWMPJr110dtzMIL2wOl9AlfpHqHd6CRDhNIv5zOUTyvLSiFZbhC9l8mjEd672uBMlvkSZALOGTnsIigp31a1nDlbyuo9YuGddYGHVyg0tpN3RtHnklL/Bp3rdVGbjPqdo6stp8993syJ1+8mxAE0SvnN/AkuwMODlAHbtieoENVxZWHTtPX0FuFbv9v69I5Sm30RkuKqruPFlw==",
|
||||||
|
otpURL: "otpauth://totp/iam-test:NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM?algorithm=SHA1&digits=6&issuer=iam-test&period=30&secret=2EIZ6JTGWBZHYEMTFQGTCWLUIGHMNU2S",
|
||||||
|
unlockerPrivateKeys: []string{
|
||||||
|
"17cbdf19c6ebb8c8e7753c489ee7851bf6fe9d157f5dc382bd55a84547cfe050",
|
||||||
|
"a7b0d0e3e88f2dafa73606d39cadfcf18d1a91d606db70e95815f15b9bd34ea6",
|
||||||
|
"b7f93214f7897a863cd9e0bdc3dfb1d7bc1fa49672d5e930d7682b22e1341076",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
boxData, err := base64.StdEncoding.DecodeString(tc.binary)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var box = new(MFABox)
|
||||||
|
require.NoError(t, box.Unmarshal(boxData))
|
||||||
|
|
||||||
|
for _, keyStr := range tc.unlockerPrivateKeys {
|
||||||
|
key, err := keys.NewPrivateKeyFromHex(keyStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
otpKey, err := UnpackMFABox(box, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.otpURL, otpKey.URL())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKeys(t *testing.T, n int) []*keys.PrivateKey {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
res := make([]*keys.PrivateKey, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
res[i], err = keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
62
mfa/storage.go
Normal file
62
mfa/storage.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package mfa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Storage is an interface for Manager to manage FrostFS objects
|
||||||
|
// and metadata in tree service.
|
||||||
|
Storage interface {
|
||||||
|
// CreateObject creates new FrostFS object.
|
||||||
|
CreateObject(context.Context, PrmObjectCreate) (oid.ID, error)
|
||||||
|
// GetObject returns payload of FrostFS object.
|
||||||
|
GetObject(context.Context, oid.Address) ([]byte, error)
|
||||||
|
// DeleteObject deletes FrostFS object.
|
||||||
|
DeleteObject(context.Context, oid.Address) error
|
||||||
|
// SetTreeNode creates or updates specified tree node the tree service and returns updated data.
|
||||||
|
SetTreeNode(ctx context.Context, cnrID cid.ID, name string, meta map[string]string) (*TreeMultiNode, error)
|
||||||
|
// GetTreeNode returns data about latest and remaining versions of specified tree node.
|
||||||
|
// Must return 'ErrTreeNodeNotFound' if tree does not exist.
|
||||||
|
GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*TreeMultiNode, error)
|
||||||
|
// DeleteTreeNode removes all specified tree nodes from the tree and returns copy of it.
|
||||||
|
DeleteTreeNode(ctx context.Context, cnrID cid.ID, name string) ([]*TreeNode, error)
|
||||||
|
// GetTreeNodes returns all available tree nodes with specified prefix.
|
||||||
|
GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix string) ([]*TreeNode, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings interface {
|
||||||
|
// CopiesNumber returns list of copies number for MFA object.
|
||||||
|
CopiesNumber() []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeNode contains metadata of node in the tree service.
|
||||||
|
TreeNode struct {
|
||||||
|
Meta map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeMultiNode contains metadata of latest and all available versions
|
||||||
|
// of node in the tree service.
|
||||||
|
TreeMultiNode struct {
|
||||||
|
Current TreeNode
|
||||||
|
Old []*TreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmObjectCreate contains parameters to create new FrostFS object.
|
||||||
|
PrmObjectCreate struct {
|
||||||
|
Container cid.ID
|
||||||
|
Owner user.ID
|
||||||
|
FilePath string
|
||||||
|
Payload []byte
|
||||||
|
CopiesNumber []uint32
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTreeNodeNotFound = errors.New("tree node not found")
|
||||||
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
GO_VERSION ?= 1.22
|
GO_VERSION ?= 1.22
|
||||||
LINT_VERSION ?= 1.56.1
|
LINT_VERSION ?= 1.62.0
|
||||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.5
|
TRUECLOUDLAB_LINT_VERSION ?= 0.0.8
|
||||||
BIN ?= bin
|
BIN ?= bin
|
||||||
OUTPUT_LINT_DIR ?= $(abspath $(BIN))/linters
|
OUTPUT_LINT_DIR ?= $(abspath $(BIN))/linters
|
||||||
LINT_DIR ?= $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
LINT_DIR ?= $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
||||||
|
@ -11,14 +11,14 @@ TMP_DIR := .cache
|
||||||
# Install linters
|
# Install linters
|
||||||
$(LINT_DIR):
|
$(LINT_DIR):
|
||||||
@rm -rf $(OUTPUT_LINT_DIR)
|
@rm -rf $(OUTPUT_LINT_DIR)
|
||||||
@mkdir $(OUTPUT_LINT_DIR)
|
@mkdir -p $(OUTPUT_LINT_DIR)
|
||||||
@mkdir -p $(TMP_DIR)
|
@mkdir -p $(TMP_DIR)
|
||||||
@rm -rf $(TMP_DIR)/linters
|
@rm -rf $(TMP_DIR)/linters
|
||||||
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
||||||
@@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
@@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
||||||
@rm -rf $(TMP_DIR)/linters
|
@rm -rf $(TMP_DIR)/linters
|
||||||
@rmdir $(TMP_DIR) 2>/dev/null || true
|
@rmdir $(TMP_DIR) 2>/dev/null || true
|
||||||
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
||||||
|
|
||||||
# Run linters
|
# Run linters
|
||||||
lint: $(LINT_DIR)
|
lint: $(LINT_DIR)
|
||||||
|
|
Loading…
Add table
Reference in a new issue