From 125ff0ffac2120c69219851c4408252de9a5a9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Ujv=C3=A1ri?= <58116288+bosi95@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:03:25 +0200 Subject: [PATCH] Dynamicaccess service for ACT (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add act.go with TODOs feat: Add Act interface feat: Add Marshal, Unmarshal skeleton feat: Refactor AccessType to iota feat: Add upload feat: Rename GenerateAccessControlManifest -> create feat: Add saltLengthIs32 feat: Add Mrshal, Unmarshal impl feat: Add Marshal Unmarshal feat: Remove ManifestEntry json annotations feat: Modify to public finc/method feat: Add ErrSaltLength Add pkg/dynamicaccess Refactor interfaces and implement default structs Refactor typo Refactor History package to use NewHistory() function Add Act interface and default implementation Add ACT use cases to act_ucs.md Add new files and implement interfaces, refactor packeges Update act_ucs.md base usecases Refactor access logic and add mock implementations*** Add DiffieHellman implementation and remove Keystore*** Refactor NewAccessLogic function Replace encryption.go to pkg/encryption Refactor packages Update act_ucs.md Update act_ucs.md Update act_ucs.md Update act_ucs.md Update act_ucs.md * Diffie-Hellman (#3) * Use DiffieHellmanMock * Adds a comment about Get * Add support for ECDSA public key in DiffieHellman.SharedSecret function * Update defaultAct implementation * Adds pseudo code for Access Logic * Update default Act creation; Fix basic Act tests * Refactor access logic to use new ActMock implementation * feat(history): test mockups wip * Refactor DiffieHellman implementation * changes pseudocode for Diffie-Hellmann read * Co-authored-by: Bálint Ujvári * DiffieHellman mock generates a real sherd secret * Refactor Act * Adds manifest lookup * Extend act_test * Adds unit tests, some values are mocked * Refactor act mock impl with map[string]map[string]string * Add check mock implementation for DiffieHellman interface * Add Load, Store to Act interface. Refactor Act interface * refactor act, diffieHellman mocks, tests * Add TestLoadStore function to act_test.go * Remove unnecessary code in Load function * Add history mock and History lookup test * Act refactor Co-authored-by: Bálint Ujvári * Refactor Add method to return Act interface * Change Get method return type to []byte --------- Co-authored-by: Ferenc Sárai Co-authored-by: Peter Ott Co-authored-by: Bálint Ujvári Co-authored-by: Levente Kiss Co-authored-by: Roland Seres Co-authored-by: Kexort Co-authored-by: Bálint Ujvári * Acces Logic (#8) * Use DiffieHellmanMock * Adds a comment about Get * Add support for ECDSA public key in DiffieHellman.SharedSecret function * Update defaultAct implementation * Adds pseudo code for Access Logic * Update default Act creation; Fix basic Act tests * Refactor access logic to use new ActMock implementation * feat(history): test mockups wip * Refactor DiffieHellman implementation * changes pseudocode for Diffie-Hellmann read * Co-authored-by: Bálint Ujvári * DiffieHellman mock generates a real sherd secret * Refactor Act * Adds manifest lookup * Extend act_test * Adds unit tests, some values are mocked * Refactor act mock impl with map[string]map[string]string * Add check mock implementation for DiffieHellman interface * started Add * changed some sig * save * new grantee addition handling * mod * changed helper function visibilities * some mod with grantee * test mod * save * no error in actInit * Add_New_Grantee_To_Content * comment * copied act_test.go * no compiler errors on our side * Adds Add_New_Grantee_To_Content and ActInit * almost complete grantee container * maybe complete grantee container * Solves merge conflict * access-logic-merge * fix merge issues * Added context & details to use cases (#6) ZH #106 Added context & details to use cases * Add grantee management (#10) * Add grantee management * Added controller test * Fix test fixture, refactor accesslogic * Add UploadHandler --------- Co-authored-by: Bálint Ujvári * (refactor): from `Get` to `Lookup` to improve clarity and consistency. The changes have been made in the `accesslogic.go`, `act.go`, `act_test.go`, `history_test.go`, and `mock/act.go` files. (#13) Co-authored-by: Ferenc Sárai * Act params rename doc (#14) * (refactor): ACT interface params + add doc comments * Revert "(refactor): ACT interface params + add doc comments" This reverts commit ee8da04fe7468a4fa65bd390fa17f72f2e93d301. * (refactor): ACT interface params + add doc comments * (refactor): Add error to ACT interface methods --------- Co-authored-by: Ferenc Sárai * Move and refactor ACT diffieHellman to Session. Add Key and NewFromKeystore functions. (#16) * Act swarm address (#15) * (refactor): ACT interface params + add doc comments * Revert "(refactor): ACT interface params + add doc comments" This reverts commit ee8da04fe7468a4fa65bd390fa17f72f2e93d301. * (refactor): ACT interface params + add doc comments * (refactor): Add error to ACT interface methods * Add in-memory storage and implement Store and Load methods * Move and refactor ACT diffieHellman to Session. Add Key and NewFromKeystore functions. --------- Co-authored-by: Ferenc Sárai Co-authored-by: Bálint Ujvári * (rename): defaultAct to inMemoryAct (#17) * (refactor): ACT interface params + add doc comments * Revert "(refactor): ACT interface params + add doc comments" This reverts commit ee8da04fe7468a4fa65bd390fa17f72f2e93d301. * (refactor): ACT interface params + add doc comments * (refactor): Add error to ACT interface methods * Add in-memory storage and implement Store and Load methods * *refactor) Rename defaultAct to inMemroryAct --------- Co-authored-by: Ferenc Sárai * (refactor): Update controller_test.go to use NewInMemoryAct, modify Session.Key to return correct dimensional byte slice (#18) * (refactor): Update controller_test.go to use NewInMemoryAct, modify Session.Key to return two-dimensional byte slice * (refactor:) Refactor session Key function to use append instead of index-based assignment --------- Co-authored-by: Ferenc Sárai * Act access logic merge (#19) * grantee container and access logc tests are passed * refactored access logic and grantee container * PR 19 comments resolving * Refactor * Refactor * Act kvs merge (#22) * grantee container and access logc tests are passed * refactored access logic and grantee container * PR 19 comments resolving * Refactor * Refactor * working manifest ACT with basic tests * (refactor:) Refactor act_test * (refactor:) Refactor kvs -> kvs.manifest, kvs.memory * (refactror:) kvs * refactor kvs contsructors --------- Co-authored-by: Roland Seres Co-authored-by: Bálint Ujvári Co-authored-by: Ferenc Sárai * Session refactor (#24) * pr comment fix * add comment to session.NewFromKeystore * Access logic refactor (#25) Refactors access logic --------- Co-authored-by: Peter Ott Co-authored-by: Ferenc Sárai Co-authored-by: Bálint Ujvári Co-authored-by: Peter Ott * (refactor:) PR comments (#23) * grantee-refactor * Dried up code, related to AddPublisher - AddNewGranteeToContent * Refactor * removed getEncryptedAccessKey * Renamed AddGrentees, RemoveGrantees, etc to Add, Remove, etc * (refactor:) PR comments * (refactor:) compile check * removed encrypted_ref, grantee check (validation) * changed interface * comments * some more comments * refactor kvs and add load and store * (refactor:) Use ref * renamed defaultGrantee to granteeList * removed null encrypted test in in TestGet_Error * refactor kvs: pass kvs IF argument instead of storing it * Refactor according to the result of the workshop * refactor kvs IF and mock * fix merge errors and Logic/get_error test * (test:) Add test for put/get after kvs.Save --------- Co-authored-by: Roland Seres Co-authored-by: Peter Ott Co-authored-by: Ferenc Sárai Co-authored-by: Bálint Ujvári Co-authored-by: Peter Ott * Add referenced mock kvs (#26) * add controller upload test * compile * Add test for grantee * Add Upload test * Implement controller logic, move grantee management * Act kvs test (#27) * (test:) Refactor tests * (fix:) Save reset counter --------- Co-authored-by: Ferenc Sárai * feat: add history lookup and add * feat: expose mantaray manifest * Small refactor + al test (#28) Adds TestDecryptRefWithGrantee_Success and replaces generateFixPrivateKey with getPrivKey Co-authored-by: Peter Ott * chore: tests + minor fixes * chore: minor test change * feat: history with reference * chore: debugging * Persist grantee list on swarm (#30) * Persist grantee list on swarm * accesslogic refactor * Refactor grantee list tests Co-authored-by: Roland Seres * Merging Swarm 2.0 master (#32) * fix(stamper): global lock stamper across multiple upload sessions (#4578) * fix: strategy and fetch timeout parsing (#4579) * feat: neighborhood suggester config (#4580) * feat: add codeql.yml (#4334) * feat: add reserveSizeWithinRadius to status protocol (#4585) * fix: missing 200 response (#4526) * feat: pinned reference integrity check API (#4573) * fix(redundancy/getter): wait for recovery and return error (#4581) * fix(pushsync): store the chunk locally when no peers are available fo… (#4597) * fix(redundancy): on by default when downloading (#4602) * fix: add missing openapi spec (#4598) * feat: bzz resource info API (#4588) * fix(redundancy): bzz unit test (#4603) * feat: redundancy ci (#4591) * chore: bump github.com/quic-go/quic-go from 0.38.1 to 0.38.2 (#4534) * feat: split input file to chunks with specified redundancy (#4600) * perf(getter): cancel inflight requests if enough chunks are fetched for recovery (#4608) * fix: store dir error info (#4605) * chore: remove repetitive words (#4611) * fix: use neighborhood suggester only on mainnet (#4612) * feat: alternative withdrawal address (#4606) * fix(seg65) (#4604) * fix(getter): redundancy getter cleanup (#4610) * feat: v2 (#4615) * fix(pin_integrity): changed route and added openapi (#4616) * fix: missing v2 in the makefile and goreleaser (#4622) * chore: package update * Update package imports to use the v2 version of the modules (#33) Co-authored-by: Ferenc Sárai * fix walkfn with key sort * feat: new option to walk nodes of mantaray in sequence * feat: add latest timestamp check * chore: uncomment wip stuff * chore: requested changes * test: fix to latest adjustment * Add ctrl logic * Add dac service * Continue add ACT handler * chore: use ZeroAddress * chore: make var name more general * connect api test with dac service * refactor ctrl based on history v2 * Fix: controller upload download flow + basic tests * hacked mock dac service for simple upload and download * Insert act uploadhandler into /bzz endpoint and remove uphandler * Refactor controller and api; enrypt and rLevel passed on during up/download * Connect Get,Head,Post endpoints with ACT * Add: act to devnode * devnode: close dac during shutdown * pass decrypted ref in r.ctx * set address ctx as swarm address * refactor: call actEncrpytionHandler in every endpoint * typo and comment fix in dynamicaccess * Add: mock dynamicaccess service and api tests * Add: TestDacEachEndpointWithAct; fixed some review comments * Add ACT head test for endpoints * CHG: first encrypt via ACT then upload normal reference * FIX: apiservice.dac nil error --------- Co-authored-by: Ferenc Sárai Co-authored-by: Ferenc Sárai Co-authored-by: Peter Ott Co-authored-by: Levente Kiss Co-authored-by: Roland Seres Co-authored-by: Kexort Co-authored-by: Bálint Ujvári Co-authored-by: András Arányi Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com> Co-authored-by: Peter Ott --- cmd/bee/cmd/start.go | 7 +- pkg/api/api.go | 10 + pkg/api/api_test.go | 7 + pkg/api/bytes.go | 47 +- pkg/api/bzz.go | 54 +- pkg/api/chunk.go | 28 +- pkg/api/chunk_address.go | 9 +- pkg/api/dirs.go | 13 +- pkg/api/dynamicaccess.go | 108 +++ pkg/api/dynamicaccess_test.go | 804 ++++++++++++++++++++++ pkg/api/export_test.go | 2 + pkg/api/feed.go | 19 +- pkg/api/router.go | 19 +- pkg/api/soc.go | 17 +- pkg/dynamicaccess/accesslogic.go | 40 +- pkg/dynamicaccess/accesslogic_test.go | 52 +- pkg/dynamicaccess/controller.go | 208 +++++- pkg/dynamicaccess/controller_test.go | 129 ++-- pkg/dynamicaccess/grantee.go | 10 +- pkg/dynamicaccess/grantee_manager.go | 47 -- pkg/dynamicaccess/grantee_manager_test.go | 38 - pkg/dynamicaccess/grantee_test.go | 9 +- pkg/dynamicaccess/mock/service.go | 162 +++++ pkg/dynamicaccess/service.go | 39 ++ pkg/kvs/kvs.go | 28 +- pkg/kvs/kvs_test.go | 51 +- pkg/kvs/mock/kvs.go | 7 +- pkg/node/devnode.go | 13 + pkg/node/node.go | 13 + pkg/soc/testing/soc.go | 36 + 30 files changed, 1707 insertions(+), 319 deletions(-) create mode 100644 pkg/api/dynamicaccess.go create mode 100644 pkg/api/dynamicaccess_test.go delete mode 100644 pkg/dynamicaccess/grantee_manager.go delete mode 100644 pkg/dynamicaccess/grantee_manager_test.go create mode 100644 pkg/dynamicaccess/mock/service.go create mode 100644 pkg/dynamicaccess/service.go diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 4e967285428..80778bf517c 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -27,6 +27,7 @@ import ( chaincfg "github.com/ethersphere/bee/v2/pkg/config" "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/ethersphere/bee/v2/pkg/crypto/clef" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" "github.com/ethersphere/bee/v2/pkg/keystore" filekeystore "github.com/ethersphere/bee/v2/pkg/keystore/file" memkeystore "github.com/ethersphere/bee/v2/pkg/keystore/mem" @@ -292,7 +293,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo neighborhoodSuggester = c.config.GetString(optionNameNeighborhoodSuggester) } - b, err := node.NewBee(ctx, c.config.GetString(optionNameP2PAddr), signerConfig.publicKey, signerConfig.signer, networkID, logger, signerConfig.libp2pPrivateKey, signerConfig.pssPrivateKey, &node.Options{ + b, err := node.NewBee(ctx, c.config.GetString(optionNameP2PAddr), signerConfig.publicKey, signerConfig.signer, networkID, logger, signerConfig.libp2pPrivateKey, signerConfig.pssPrivateKey, signerConfig.session, &node.Options{ DataDir: c.config.GetString(optionNameDataDir), CacheCapacity: c.config.GetUint64(optionNameCacheCapacity), DBOpenFilesLimit: c.config.GetUint64(optionNameDBOpenFilesLimit), @@ -372,6 +373,7 @@ type signerConfig struct { publicKey *ecdsa.PublicKey libp2pPrivateKey *ecdsa.PrivateKey pssPrivateKey *ecdsa.PrivateKey + session dynamicaccess.Session } func waitForClef(logger log.Logger, maxRetries uint64, endpoint string) (externalSigner *external.ExternalSigner, err error) { @@ -402,6 +404,7 @@ func (c *command) configureSigner(cmd *cobra.Command, logger log.Logger) (config var signer crypto.Signer var password string var publicKey *ecdsa.PublicKey + var session dynamicaccess.Session if p := c.config.GetString(optionNamePassword); p != "" { password = p } else if pf := c.config.GetString(optionNamePasswordFile); pf != "" { @@ -474,6 +477,7 @@ func (c *command) configureSigner(cmd *cobra.Command, logger log.Logger) (config } signer = crypto.NewDefaultSigner(swarmPrivateKey) publicKey = &swarmPrivateKey.PublicKey + session = dynamicaccess.NewDefaultSession(swarmPrivateKey) } logger.Info("swarm public key", "public_key", hex.EncodeToString(crypto.EncodeSecp256k1PublicKey(publicKey))) @@ -512,6 +516,7 @@ func (c *command) configureSigner(cmd *cobra.Command, logger log.Logger) (config publicKey: publicKey, libp2pPrivateKey: libp2pPrivateKey, pssPrivateKey: pssPrivateKey, + session: session, }, nil } diff --git a/pkg/api/api.go b/pkg/api/api.go index 72100cc0d9b..00373c28186 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -30,6 +30,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/accounting" "github.com/ethersphere/bee/v2/pkg/auth" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" "github.com/ethersphere/bee/v2/pkg/feeds" "github.com/ethersphere/bee/v2/pkg/file/pipeline" "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" @@ -85,6 +86,10 @@ const ( SwarmRedundancyFallbackModeHeader = "Swarm-Redundancy-Fallback-Mode" SwarmChunkRetrievalTimeoutHeader = "Swarm-Chunk-Retrieval-Timeout" SwarmLookAheadBufferSizeHeader = "Swarm-Lookahead-Buffer-Size" + SwarmActHeader = "Swarm-Act" + SwarmActTimestampHeader = "Swarm-Act-Timestamp" + SwarmActPublisherHeader = "Swarm-Act-Publisher" + SwarmActHistoryAddressHeader = "Swarm-Act-History-Address" ImmutableHeader = "Immutable" GasPriceHeader = "Gas-Price" @@ -117,6 +122,8 @@ var ( errBatchUnusable = errors.New("batch not usable") errUnsupportedDevNodeOperation = errors.New("operation not supported in dev mode") errOperationSupportedOnlyInFullMode = errors.New("operation is supported only in full mode") + errActDownload = errors.New("act download failed") + errActUpload = errors.New("act upload failed") ) // Storer interface provides the functionality required from the local storage @@ -147,6 +154,7 @@ type Service struct { feedFactory feeds.Factory signer crypto.Signer post postage.Service + dac dynamicaccess.Service postageContract postagecontract.Interface probe *Probe metricsRegistry *prometheus.Registry @@ -245,6 +253,7 @@ type ExtraOptions struct { Pss pss.Interface FeedFactory feeds.Factory Post postage.Service + Dac dynamicaccess.Service PostageContract postagecontract.Interface Staking staking.Contract Steward steward.Interface @@ -328,6 +337,7 @@ func (s *Service) Configure(signer crypto.Signer, auth auth.Authenticator, trace s.pss = e.Pss s.feedFactory = e.FeedFactory s.post = e.Post + s.dac = e.Dac s.postageContract = e.PostageContract s.steward = e.Steward s.stakingContract = e.Staking diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 164709b7ca9..00dd73fdd01 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -28,6 +28,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/auth" mockauth "github.com/ethersphere/bee/v2/pkg/auth/mock" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" + mockdac "github.com/ethersphere/bee/v2/pkg/dynamicaccess/mock" "github.com/ethersphere/bee/v2/pkg/feeds" "github.com/ethersphere/bee/v2/pkg/file/pipeline" "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" @@ -102,6 +104,7 @@ type testServerOptions struct { PostageContract postagecontract.Interface StakingContract staking.Contract Post postage.Service + Dac dynamicaccess.Service Steward steward.Interface WsHeaders http.Header Authenticator auth.Authenticator @@ -152,6 +155,9 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. if o.Post == nil { o.Post = mockpost.New() } + if o.Dac == nil { + o.Dac = mockdac.New() + } if o.BatchStore == nil { o.BatchStore = mockbatchstore.New(mockbatchstore.WithAcceptAllExistsFunc()) // default is with accept-all Exists() func } @@ -198,6 +204,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. Pss: o.Pss, FeedFactory: o.Feeds, Post: o.Post, + Dac: o.Dac, PostageContract: o.PostageContract, Steward: o.Steward, SyncStatus: o.SyncStatus, diff --git a/pkg/api/bytes.go b/pkg/api/bytes.go index 9b5d9b902d6..dc3735a497b 100644 --- a/pkg/api/bytes.go +++ b/pkg/api/bytes.go @@ -33,12 +33,14 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { defer span.Finish() headers := struct { - BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` - SwarmTag uint64 `map:"Swarm-Tag"` - Pin bool `map:"Swarm-Pin"` - Deferred *bool `map:"Swarm-Deferred-Upload"` - Encrypt bool `map:"Swarm-Encrypt"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + SwarmTag uint64 `map:"Swarm-Tag"` + Pin bool `map:"Swarm-Pin"` + Deferred *bool `map:"Swarm-Deferred-Upload"` + Encrypt bool `map:"Swarm-Encrypt"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + Act bool `map:"Swarm-Act"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) @@ -100,7 +102,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { } p := requestPipelineFn(putter, headers.Encrypt, headers.RLevel) - address, err := p(ctx, r.Body) + reference, err := p(ctx, r.Body) if err != nil { logger.Debug("split write all failed", "error", err) logger.Error(nil, "split write all failed") @@ -114,9 +116,18 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { return } - span.SetTag("root_address", address) + encryptedReference := reference + if headers.Act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, reference, headers.HistoryAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } + // TODO: what should be the root_address ? (eref vs ref) + span.SetTag("root_address", reference) - err = putter.Done(address) + err = putter.Done(reference) if err != nil { logger.Debug("done split failed", "error", err) logger.Error(nil, "done split failed") @@ -133,7 +144,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) jsonhttp.Created(w, bytesPostResponse{ - Reference: address, + Reference: encryptedReference, }) } @@ -149,11 +160,16 @@ func (s *Service) bytesGetHandler(w http.ResponseWriter, r *http.Request) { return } + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + additionalHeaders := http.Header{ ContentTypeHeader: {"application/octet-stream"}, } - s.downloadHandler(logger, w, r, paths.Address, additionalHeaders, true, false) + s.downloadHandler(logger, w, r, address, additionalHeaders, true, false) } func (s *Service) bytesHeadHandler(w http.ResponseWriter, r *http.Request) { @@ -167,11 +183,16 @@ func (s *Service) bytesHeadHandler(w http.ResponseWriter, r *http.Request) { return } + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + getter := s.storer.Download(true) - ch, err := getter.Get(r.Context(), paths.Address) + ch, err := getter.Get(r.Context(), address) if err != nil { - logger.Debug("get root chunk failed", "chunk_address", paths.Address, "error", err) + logger.Debug("get root chunk failed", "chunk_address", address, "error", err) logger.Error(nil, "get rook chunk failed") w.WriteHeader(http.StatusNotFound) return diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 65ded851f12..241e30cf165 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -63,14 +63,16 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { defer span.Finish() headers := struct { - ContentType string `map:"Content-Type,mimeMediaType" validate:"required"` - BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` - SwarmTag uint64 `map:"Swarm-Tag"` - Pin bool `map:"Swarm-Pin"` - Deferred *bool `map:"Swarm-Deferred-Upload"` - Encrypt bool `map:"Swarm-Encrypt"` - IsDir bool `map:"Swarm-Collection"` - RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + ContentType string `map:"Content-Type,mimeMediaType" validate:"required"` + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + SwarmTag uint64 `map:"Swarm-Tag"` + Pin bool `map:"Swarm-Pin"` + Deferred *bool `map:"Swarm-Deferred-Upload"` + Encrypt bool `map:"Swarm-Encrypt"` + IsDir bool `map:"Swarm-Collection"` + RLevel redundancy.Level `map:"Swarm-Redundancy-Level"` + Act bool `map:"Swarm-Act"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) @@ -132,10 +134,10 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) { } if headers.IsDir || headers.ContentType == multiPartFormData { - s.dirUploadHandler(ctx, logger, span, ow, r, putter, r.Header.Get(ContentTypeHeader), headers.Encrypt, tag, headers.RLevel) + s.dirUploadHandler(ctx, logger, span, ow, r, putter, r.Header.Get(ContentTypeHeader), headers.Encrypt, tag, headers.RLevel, headers.Act, headers.HistoryAddress) return } - s.fileUploadHandler(ctx, logger, span, ow, r, putter, headers.Encrypt, tag, headers.RLevel) + s.fileUploadHandler(ctx, logger, span, ow, r, putter, headers.Encrypt, tag, headers.RLevel, headers.Act, headers.HistoryAddress) } // fileUploadResponse is returned when an HTTP request to upload a file is successful @@ -155,6 +157,8 @@ func (s *Service) fileUploadHandler( encrypt bool, tagID uint64, rLevel redundancy.Level, + act bool, + historyAddress *swarm.Address, ) { queries := struct { FileName string `map:"name" validate:"startsnotwith=/"` @@ -260,6 +264,15 @@ func (s *Service) fileUploadHandler( } logger.Debug("store", "manifest_reference", manifestReference) + encryptedReference := manifestReference + if act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, manifestReference, historyAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } + err = putter.Done(manifestReference) if err != nil { logger.Debug("done split failed", "error", err) @@ -268,7 +281,7 @@ func (s *Service) fileUploadHandler( ext.LogError(span, err, olog.String("action", "putter.Done")) return } - + // TODO: what should be the root_address ? (eref vs ref) span.LogFields(olog.Bool("success", true)) span.SetTag("root_address", manifestReference) @@ -276,10 +289,11 @@ func (s *Service) fileUploadHandler( w.Header().Set(SwarmTagHeader, fmt.Sprint(tagID)) span.SetTag("tagID", tagID) } - w.Header().Set(ETagHeader, fmt.Sprintf("%q", manifestReference.String())) + w.Header().Set(ETagHeader, fmt.Sprintf("%q", encryptedReference.String())) w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) + // TODO: do we need to return reference as well ? jsonhttp.Created(w, bzzUploadResponse{ - Reference: manifestReference, + Reference: encryptedReference, }) } @@ -295,11 +309,16 @@ func (s *Service) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) { return } + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + if strings.HasSuffix(paths.Path, "/") { paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some. } - s.serveReference(logger, paths.Address, paths.Path, w, r, false) + s.serveReference(logger, address, paths.Path, w, r, false) } func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { @@ -314,11 +333,16 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) { return } + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + if strings.HasSuffix(paths.Path, "/") { paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some. } - s.serveReference(logger, paths.Address, paths.Path, w, r, true) + s.serveReference(logger, address, paths.Path, w, r, true) } func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool) { diff --git a/pkg/api/chunk.go b/pkg/api/chunk.go index a572cacdcdf..13738f70d39 100644 --- a/pkg/api/chunk.go +++ b/pkg/api/chunk.go @@ -30,8 +30,10 @@ func (s *Service) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { logger := s.logger.WithName("post_chunk").Build() headers := struct { - BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` - SwarmTag uint64 `map:"Swarm-Tag"` + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + SwarmTag uint64 `map:"Swarm-Tag"` + Act bool `map:"Swarm-Act"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) @@ -139,6 +141,15 @@ func (s *Service) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { } } + encryptedReference := chunk.Address() + if headers.Act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, chunk.Address(), headers.HistoryAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } + err = putter.Put(r.Context(), chunk) if err != nil { logger.Debug("chunk upload: write chunk failed", "chunk_address", chunk.Address(), "error", err) @@ -165,7 +176,7 @@ func (s *Service) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) - jsonhttp.Created(w, chunkAddressResponse{Reference: chunk.Address()}) + jsonhttp.Created(w, chunkAddressResponse{Reference: encryptedReference}) } func (s *Service) chunkGetHandler(w http.ResponseWriter, r *http.Request) { @@ -192,15 +203,20 @@ func (s *Service) chunkGetHandler(w http.ResponseWriter, r *http.Request) { return } - chunk, err := s.storer.Download(cache).Get(r.Context(), paths.Address) + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + + chunk, err := s.storer.Download(cache).Get(r.Context(), address) if err != nil { if errors.Is(err, storage.ErrNotFound) { - loggerV1.Debug("chunk not found", "address", paths.Address) + loggerV1.Debug("chunk not found", "address", address) jsonhttp.NotFound(w, "chunk not found") return } - logger.Debug("read chunk failed", "chunk_address", paths.Address, "error", err) + logger.Debug("read chunk failed", "chunk_address", address, "error", err) logger.Error(nil, "read chunk failed") jsonhttp.InternalServerError(w, "read chunk failed") return diff --git a/pkg/api/chunk_address.go b/pkg/api/chunk_address.go index 6f214a0ea03..c49c980c2e7 100644 --- a/pkg/api/chunk_address.go +++ b/pkg/api/chunk_address.go @@ -23,9 +23,14 @@ func (s *Service) hasChunkHandler(w http.ResponseWriter, r *http.Request) { return } - has, err := s.storer.ChunkStore().Has(r.Context(), paths.Address) + address := paths.Address + if v := getAddressFromContext(r.Context()); !v.Equal(swarm.ZeroAddress) { + address = v + } + + has, err := s.storer.ChunkStore().Has(r.Context(), address) if err != nil { - logger.Debug("has chunk failed", "chunk_address", paths.Address, "error", err) + logger.Debug("has chunk failed", "chunk_address", address, "error", err) jsonhttp.BadRequest(w, err) return } diff --git a/pkg/api/dirs.go b/pkg/api/dirs.go index f54a02807c9..1ec5e6cde40 100644 --- a/pkg/api/dirs.go +++ b/pkg/api/dirs.go @@ -47,6 +47,8 @@ func (s *Service) dirUploadHandler( encrypt bool, tag uint64, rLevel redundancy.Level, + act bool, + historyAddress *swarm.Address, ) { if r.Body == http.NoBody { logger.Error(nil, "request has no body") @@ -98,6 +100,15 @@ func (s *Service) dirUploadHandler( return } + encryptedReference := reference + if act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, reference, historyAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } + err = putter.Done(reference) if err != nil { logger.Debug("store dir failed", "error", err) @@ -113,7 +124,7 @@ func (s *Service) dirUploadHandler( } w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) jsonhttp.Created(w, bzzUploadResponse{ - Reference: reference, + Reference: encryptedReference, }) } diff --git a/pkg/api/dynamicaccess.go b/pkg/api/dynamicaccess.go new file mode 100644 index 00000000000..7a05b1ad38c --- /dev/null +++ b/pkg/api/dynamicaccess.go @@ -0,0 +1,108 @@ +package api + +import ( + "context" + "crypto/ecdsa" + "net/http" + + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/log" + storer "github.com/ethersphere/bee/v2/pkg/storer" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/gorilla/mux" +) + +type addressKey struct{} + +// getAddressFromContext is a helper function to extract the address from the context +func getAddressFromContext(ctx context.Context) swarm.Address { + v, ok := ctx.Value(addressKey{}).(swarm.Address) + if ok { + return v + } + return swarm.ZeroAddress +} + +// setAddress sets the swarm address in the context +func setAddressInContext(ctx context.Context, address swarm.Address) context.Context { + return context.WithValue(ctx, addressKey{}, address) +} + +func (s *Service) actDecryptionHandler() func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("acthandler").Build() + paths := struct { + Address swarm.Address `map:"address,resolve" validate:"required"` + }{} + if response := s.mapStructure(mux.Vars(r), &paths); response != nil { + response("invalid path params", logger, w) + return + } + + headers := struct { + Timestamp *int64 `map:"Swarm-Act-Timestamp"` + Publisher *ecdsa.PublicKey `map:"Swarm-Act-Publisher"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` + }{} + if response := s.mapStructure(r.Header, &headers); response != nil { + response("invalid header params", logger, w) + return + } + + // Try to download the file wihtout decryption, if the act headers are not present + if headers.Publisher == nil || headers.Timestamp == nil || headers.HistoryAddress == nil { + h.ServeHTTP(w, r) + return + } + ctx := r.Context() + reference, err := s.dac.DownloadHandler(ctx, *headers.Timestamp, paths.Address, headers.Publisher, *headers.HistoryAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActDownload) + return + } + h.ServeHTTP(w, r.WithContext(setAddressInContext(ctx, reference))) + }) + } + +} + +// TODO: is ctx needed in ctrl upload ? +func (s *Service) actEncryptionHandler( + ctx context.Context, + logger log.Logger, + w http.ResponseWriter, + putter storer.PutterSession, + reference swarm.Address, + historyAddress *swarm.Address, +) (swarm.Address, error) { + publisherPublicKey := &s.publicKey + kvsReference, historyReference, encryptedReference, err := s.dac.UploadHandler(ctx, reference, publisherPublicKey, historyAddress) + if err != nil { + logger.Debug("act failed to encrypt reference", "error", err) + logger.Error(nil, "act failed to encrypt reference") + return swarm.ZeroAddress, err + } + err = putter.Done(historyReference) + if err != nil { + logger.Debug("done split history failed", "error", err) + logger.Error(nil, "done split history failed") + return swarm.ZeroAddress, err + } + err = putter.Done(encryptedReference) + if err != nil { + logger.Debug("done split encrypted reference failed", "error", err) + logger.Error(nil, "done split encrypted reference failed") + return swarm.ZeroAddress, err + } + err = putter.Done(kvsReference) + if err != nil { + logger.Debug("done split kvs reference failed", "error", err) + logger.Error(nil, "done split kvs reference failed") + return swarm.ZeroAddress, err + } + + w.Header().Set(SwarmActHistoryAddressHeader, historyReference.String()) + + return encryptedReference, nil +} diff --git a/pkg/api/dynamicaccess_test.go b/pkg/api/dynamicaccess_test.go new file mode 100644 index 00000000000..4657b1f8bbe --- /dev/null +++ b/pkg/api/dynamicaccess_test.go @@ -0,0 +1,804 @@ +// Copyright 2020 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api_test + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethersphere/bee/v2/pkg/api" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" + mockdac "github.com/ethersphere/bee/v2/pkg/dynamicaccess/mock" + "github.com/ethersphere/bee/v2/pkg/file/loadsave" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" + "github.com/ethersphere/bee/v2/pkg/log" + mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" + testingsoc "github.com/ethersphere/bee/v2/pkg/soc/testing" + mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" + "github.com/ethersphere/bee/v2/pkg/swarm" + "gitlab.com/nolash/go-mockbytes" +) + +func prepareHistoryFixture(storer api.Storer) (dynamicaccess.History, swarm.Address) { + ctx := context.Background() + ls := loadsave.New(storer.ChunkStore(), storer.Cache(), pipelineFactory(storer.Cache(), false, redundancy.NONE)) + + h, _ := dynamicaccess.NewHistory(ls, nil) + + testActRef1 := swarm.NewAddress([]byte("39a5ea87b141fe44aa609c3327ecd891")) + firstTime := time.Date(1994, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + h.Add(ctx, testActRef1, &firstTime) + + testActRef2 := swarm.NewAddress([]byte("39a5ea87b141fe44aa609c3327ecd892")) + secondTime := time.Date(2000, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + h.Add(ctx, testActRef2, &secondTime) + + testActRef3 := swarm.NewAddress([]byte("39a5ea87b141fe44aa609c3327ecd893")) + thirdTime := time.Date(2015, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + h.Add(ctx, testActRef3, &thirdTime) + + testActRef4 := swarm.NewAddress([]byte("39a5ea87b141fe44aa609c3327ecd894")) + fourthTime := time.Date(2020, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + h.Add(ctx, testActRef4, &fourthTime) + + testActRef5 := swarm.NewAddress([]byte("39a5ea87b141fe44aa609c3327ecd895")) + fifthTime := time.Date(2030, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + h.Add(ctx, testActRef5, &fifthTime) + + ref, _ := h.Store(ctx) + return h, ref +} + +// TODO: feed test +// nolint:paralleltest,tparallel +// TestDacWithoutActHeader [positive tests]: +// On each endpoint: upload w/ "Swarm-Act" header then download and check the decrypted data +func TestDacEachEndpointWithAct(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + testfile = "testfile1" + storerMock = mockstorer.New() + logger = log.Noop + now = time.Now().Unix() + chunk = swarm.NewChunk( + swarm.MustParseHexAddress("0025737be11979e91654dffd2be817ac1e52a2dadb08c97a7cef12f937e707bc"), + []byte{72, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 149, 179, 31, 244, 146, 247, 129, 123, 132, 248, 215, 77, 44, 47, 91, 248, 229, 215, 89, 156, 210, 243, 3, 110, 204, 74, 101, 119, 53, 53, 145, 188, 193, 153, 130, 197, 83, 152, 36, 140, 150, 209, 191, 214, 193, 4, 144, 121, 32, 45, 205, 220, 59, 227, 28, 43, 161, 51, 108, 14, 106, 180, 135, 2}, + ) + g = mockbytes.New(0, mockbytes.MockTypeStandard).WithModulus(255) + bytedata, _ = g.SequentialBytes(swarm.ChunkSize * 2) + tag, _ = storerMock.NewSession() + sch = testingsoc.GenerateMockSOCWithKey(t, []byte("foo"), pk) + dirdata = []byte("Lorem ipsum dolor sit amet") + socResource = func(owner, id, sig string) string { return fmt.Sprintf("/soc/%s/%s?sig=%s", owner, id, sig) } + ) + + tc := []struct { + name string + downurl string + upurl string + exphash string + data io.Reader + expdata []byte + contenttype string + resp struct { + Reference swarm.Address `json:"reference"` + } + }{ + { + name: "bzz", + upurl: "/bzz?name=sample.html", + downurl: "/bzz", + exphash: "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade", + resp: api.BzzUploadResponse{Reference: swarm.MustParseHexAddress("a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade")}, + data: strings.NewReader(testfile), + expdata: []byte(testfile), + contenttype: "text/html; charset=utf-8", + }, + { + name: "bzz-dir", + upurl: "/bzz?name=ipsum/lorem.txt", + downurl: "/bzz", + exphash: "6561b2a744d2a8f276270585da22e092c07c56624af83ac9969d52b54e87cee6/ipsum/lorem.txt", + resp: api.BzzUploadResponse{Reference: swarm.MustParseHexAddress("6561b2a744d2a8f276270585da22e092c07c56624af83ac9969d52b54e87cee6")}, + data: tarFiles(t, []f{ + { + data: dirdata, + name: "lorem.txt", + dir: "ipsum", + header: http.Header{ + api.ContentTypeHeader: {"text/plain; charset=utf-8"}, + }, + }, + }), + expdata: dirdata, + contenttype: api.ContentTypeTar, + }, + { + name: "bytes", + upurl: "/bytes", + downurl: "/bytes", + exphash: "e30da540bb9e1901169977fcf617f28b7f8df4537de978784f6d47491619a630", + resp: api.BytesPostResponse{Reference: swarm.MustParseHexAddress("e30da540bb9e1901169977fcf617f28b7f8df4537de978784f6d47491619a630")}, + data: bytes.NewReader(bytedata), + expdata: bytedata, + contenttype: "application/octet-stream", + }, + { + name: "chunks", + upurl: "/chunks", + downurl: "/chunks", + exphash: "ca8d2d29466e017cba46d383e7e0794d99a141185ec525086037f25fc2093155", + resp: api.ChunkAddressResponse{Reference: swarm.MustParseHexAddress("ca8d2d29466e017cba46d383e7e0794d99a141185ec525086037f25fc2093155")}, + data: bytes.NewReader(chunk.Data()), + expdata: chunk.Data(), + contenttype: "binary/octet-stream", + }, + { + name: "soc", + upurl: socResource(hex.EncodeToString(sch.Owner), hex.EncodeToString(sch.ID), hex.EncodeToString(sch.Signature)), + downurl: "/chunks", + exphash: "b100d7ce487426b17b98ff779fad4f2dd471d04ab1c8949dd2a1a78fe4a1524e", + resp: api.ChunkAddressResponse{Reference: swarm.MustParseHexAddress("b100d7ce487426b17b98ff779fad4f2dd471d04ab1c8949dd2a1a78fe4a1524e")}, + data: bytes.NewReader(sch.WrappedChunk.Data()), + expdata: sch.Chunk().Data(), + contenttype: "binary/octet-stream", + }, + } + + for _, v := range tc { + upTestOpts := []jsonhttptest.Option{ + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmPinHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmTagHeader, fmt.Sprintf("%d", tag.TagID)), + jsonhttptest.WithRequestBody(v.data), + jsonhttptest.WithExpectedJSONResponse(v.resp), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, v.contenttype), + } + if v.name == "soc" { + upTestOpts = append(upTestOpts, jsonhttptest.WithRequestHeader(api.SwarmPinHeader, "true")) + } else { + upTestOpts = append(upTestOpts, jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader)) + } + expcontenttype := v.contenttype + if v.name == "bzz-dir" { + expcontenttype = "text/plain; charset=utf-8" + upTestOpts = append(upTestOpts, jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True")) + } + t.Run(v.name, func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(), + }) + header := jsonhttptest.Request(t, client, http.MethodPost, v.upurl, http.StatusCreated, + upTestOpts..., + ) + + historyRef := header.Get(api.SwarmActHistoryAddressHeader) + jsonhttptest.Request(t, client, http.MethodGet, v.downurl+"/"+v.exphash, http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, historyRef), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse(v.expdata), + jsonhttptest.WithExpectedContentLength(len(v.expdata)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, expcontenttype), + ) + + if v.name != "bzz-dir" && v.name != "soc" && v.name != "chunks" { + t.Run("head", func(t *testing.T) { + jsonhttptest.Request(t, client, http.MethodHead, v.downurl+"/"+v.exphash, http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, historyRef), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithRequestBody(v.data), + jsonhttptest.WithExpectedContentLength(len(v.expdata)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, expcontenttype), + ) + }) + } + }) + } +} + +// nolint:paralleltest,tparallel +// TestDacWithoutActHeader [negative tests]: +// 1. upload w/ "Swarm-Act" header then try to dowload w/o the header. +// 2. upload w/o "Swarm-Act" header then try to dowload w/ the header. +func TestDacWithoutAct(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + fileUploadResource = "/bzz" + fileDownloadResource = func(addr string) string { return "/bzz/" + addr } + storerMock = mockstorer.New() + h, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + fileName = "sample.html" + now = time.Now().Unix() + ) + + t.Run("upload-w/-act-then-download-w/o-act", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + var ( + testfile = "testfile1" + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusNotFound, + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: "address not found or incorrect", + Code: http.StatusNotFound, + }), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) + + t.Run("upload-w/o-act-then-download-w/-act", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(), + }) + var ( + rootHash = "0cb947ccbc410c43139ba4409d83bf89114cb0d79556a651c06c888cf73f4d7e" + sampleHtml = ` + + + +

My First Heading

+ +

My first paragraph.

+ + + ` + ) + + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(strings.NewReader(sampleHtml)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(rootHash), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", rootHash)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(rootHash), http.StatusInternalServerError, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: api.ErrActDownload.Error(), + Code: http.StatusInternalServerError, + }), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) +} + +// nolint:paralleltest,tparallel +// TestDacInvalidPath [negative test]: Expect Bad request when the path address is invalid. +func TestDacInvalidPath(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + fileDownloadResource = func(addr string) string { return "/bzz/" + addr } + storerMock = mockstorer.New() + _, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + now = time.Now().Unix() + ) + + t.Run("invalid-path-params", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(), + }) + var ( + encryptedRef = "asd" + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusBadRequest, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid path params", + Reasons: []jsonhttp.Reason{ + { + Field: "address", + Error: api.HexInvalidByteError('s').Error(), + }, + }}), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + ) + }) +} + +// nolint:paralleltest,tparallel +// TestDacHistory tests: +// [positive tests] 1., 2.: uploading a file w/ and w/o history address then downloading it and checking the data. +// [negative test] 3. uploading a file then downloading it with a wrong history address. +// [negative test] 4. uploading a file to a wrong history address. +// [negative test] 4. downloading a file to w/o history address. +func TestDacHistory(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + fileUploadResource = "/bzz" + fileDownloadResource = func(addr string) string { return "/bzz/" + addr } + storerMock = mockstorer.New() + h, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + fileName = "sample.html" + now = time.Now().Unix() + ) + + t.Run("empty-history-upload-then-download-and-check-data", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(), + }) + var ( + testfile = "testfile1" + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + header := jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + historyRef := header.Get(api.SwarmActHistoryAddressHeader) + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, historyRef), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse([]byte(testfile)), + jsonhttptest.WithExpectedContentLength(len(testfile)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), + ) + }) + + t.Run("with-history-upload-then-download-and-check-data", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + var ( + encryptedRef = "c611199e1b3674d6bf89a83e518bd16896bf5315109b4a23dcb4682a02d17b97" + testfile = ` + + + +

My First Heading

+ +

My first paragraph.

+ + + ` + ) + + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse([]byte(testfile)), + jsonhttptest.WithExpectedContentLength(len(testfile)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), + ) + }) + + t.Run("upload-then-download-wrong-history", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + var ( + testfile = "testfile1" + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusInternalServerError, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, "fc4e9fe978991257b897d987bc4ff13058b66ef45a53189a0b4fe84bb3346396"), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: api.ErrActDownload.Error(), + Code: http.StatusInternalServerError, + }), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) + + t.Run("upload-wrong-history", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(), + }) + var ( + testfile = "testfile1" + ) + + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusInternalServerError, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: api.ErrActUpload.Error(), + Code: http.StatusInternalServerError, + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + ) + }) + + t.Run("download-w/o-history", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + var ( + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusNotFound, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) +} + +// nolint:paralleltest,tparallel +// TestDacTimestamp doc. comment +// [positive test] 1.: uploading a file w/ ACT then download it w/ timestamp and check the data. +// [negative test] 2.: try to download a file w/o timestamp. +func TestDacTimestamp(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + fileUploadResource = "/bzz" + fileDownloadResource = func(addr string) string { return "/bzz/" + addr } + storerMock = mockstorer.New() + h, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + fileName = "sample.html" + ) + t.Run("upload-then-download-with-timestamp-and-check-data", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + var ( + thirdTime = time.Date(2015, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + encryptedRef = "c611199e1b3674d6bf89a83e518bd16896bf5315109b4a23dcb4682a02d17b97" + testfile = ` + + + +

My First Heading

+ +

My first paragraph.

+ + + ` + ) + + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(thirdTime, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse([]byte(testfile)), + jsonhttptest.WithExpectedContentLength(len(testfile)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), + ) + }) + + t.Run("download-w/o-timestamp", func(t *testing.T) { + var ( + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String())), + }) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusNotFound, + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) +} + +// nolint:paralleltest,tparallel +// TestDacPublisher doc. comment +// [positive test] 1.: uploading a file w/ ACT then download it w/ the publisher address and check the data. +// [negative test] 2.: expect Bad request when the public key is invalid. +// [negative test] 3.: try to download a file w/ an incorrect publisher address. +// [negative test] 3.: try to download a file w/o a publisher address. +func TestDacPublisher(t *testing.T) { + t.Parallel() + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + fileUploadResource = "/bzz" + fileDownloadResource = func(addr string) string { return "/bzz/" + addr } + storerMock = mockstorer.New() + h, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + fileName = "sample.html" + now = time.Now().Unix() + ) + + t.Run("upload-then-download-w/-publisher-and-check-data", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String()), mockdac.WithPublisher(publisher)), + }) + var ( + encryptedRef = "a5a26b4915d7ce1622f9ca52252092cf2445f98d359dabaf52588c05911aaf4f" + testfile = ` + + + +

My First Heading

+ +

My first paragraph.

+ + + ` + ) + + jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse([]byte(testfile)), + jsonhttptest.WithExpectedContentLength(len(testfile)), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), + ) + }) + + t.Run("upload-then-download-invalid-publickey", func(t *testing.T) { + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithPublisher(publisher)), + }) + var ( + publickey = "b786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfb" + encryptedRef = "a5a26b4915d7ce1622f9ca52252092cf2445f98d359dabaf52588c05911aaf4f" + testfile = ` + + + +

My First Heading

+ +

My first paragraph.

+ + + ` + ) + + header := jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmActHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(strings.NewReader(testfile)), + jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ + Reference: swarm.MustParseHexAddress(encryptedRef), + }), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), + jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", encryptedRef)), + ) + + historyRef := header.Get(api.SwarmActHistoryAddressHeader) + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusBadRequest, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, historyRef), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publickey), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid header params", + Reasons: []jsonhttp.Reason{ + { + Field: "Swarm-Act-Publisher", + Error: "malformed public key: invalid length: 32", + }, + }}), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + ) + }) + + t.Run("download-w/-wrong-publisher", func(t *testing.T) { + var ( + downloader = "03c712a7e29bc792ac8d8ae49793d28d5bda27ed70f0d90697b2fb456c0a168bd2" + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String()), mockdac.WithPublisher(publisher)), + }) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusInternalServerError, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, downloader), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: api.ErrActDownload.Error(), + Code: http.StatusInternalServerError, + }), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) + + t.Run("download-w/o-publisher", func(t *testing.T) { + var ( + encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" + ) + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + Dac: mockdac.New(mockdac.WithHistory(h, fixtureHref.String()), mockdac.WithPublisher(publisher)), + }) + + jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(encryptedRef), http.StatusNotFound, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(now, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, fixtureHref.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/json; charset=utf-8"), + ) + }) +} diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index 812ce522ba0..fd78ae6974f 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -38,6 +38,8 @@ var ( ErrInvalidNameOrAddress = errInvalidNameOrAddress ErrUnsupportedDevNodeOperation = errUnsupportedDevNodeOperation ErrOperationSupportedOnlyInFullMode = errOperationSupportedOnlyInFullMode + ErrActDownload = errActDownload + ErrActUpload = errActUpload ) var ( diff --git a/pkg/api/feed.go b/pkg/api/feed.go index 09fdf6515ec..eed03febe31 100644 --- a/pkg/api/feed.go +++ b/pkg/api/feed.go @@ -137,9 +137,11 @@ func (s *Service) feedPostHandler(w http.ResponseWriter, r *http.Request) { } headers := struct { - BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` - Pin bool `map:"Swarm-Pin"` - Deferred *bool `map:"Swarm-Deferred-Upload"` + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + Pin bool `map:"Swarm-Pin"` + Deferred *bool `map:"Swarm-Deferred-Upload"` + Act bool `map:"Swarm-Act"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) @@ -244,6 +246,15 @@ func (s *Service) feedPostHandler(w http.ResponseWriter, r *http.Request) { } return } + // TODO: do we want to allow feed act upload/ download? + encryptedReference := ref + if headers.Act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, ref, headers.HistoryAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } err = putter.Done(ref) if err != nil { @@ -253,7 +264,7 @@ func (s *Service) feedPostHandler(w http.ResponseWriter, r *http.Request) { return } - jsonhttp.Created(w, feedReferenceResponse{Reference: ref}) + jsonhttp.Created(w, feedReferenceResponse{Reference: encryptedReference}) } func parseFeedUpdate(ch swarm.Chunk) (swarm.Address, int64, error) { diff --git a/pkg/api/router.go b/pkg/api/router.go index 18c2bfb9a46..1b1e1c877cb 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -210,10 +210,12 @@ func (s *Service) mountAPI() { "GET": web.ChainHandlers( s.contentLengthMetricMiddleware(), s.newTracingHandler("bytes-download"), + s.actDecryptionHandler(), web.FinalHandlerFunc(s.bytesGetHandler), ), "HEAD": web.ChainHandlers( s.newTracingHandler("bytes-head"), + s.actDecryptionHandler(), web.FinalHandlerFunc(s.bytesHeadHandler), ), }) @@ -231,8 +233,14 @@ func (s *Service) mountAPI() { )) handle("/chunks/{address}", jsonhttp.MethodHandler{ - "GET": http.HandlerFunc(s.chunkGetHandler), - "HEAD": http.HandlerFunc(s.hasChunkHandler), + "GET": web.ChainHandlers( + s.actDecryptionHandler(), + web.FinalHandlerFunc(s.chunkGetHandler), + ), + "HEAD": web.ChainHandlers( + s.actDecryptionHandler(), + web.FinalHandlerFunc(s.hasChunkHandler), + ), }) handle("/soc/{owner}/{id}", jsonhttp.MethodHandler{ @@ -268,9 +276,11 @@ func (s *Service) mountAPI() { "GET": web.ChainHandlers( s.contentLengthMetricMiddleware(), s.newTracingHandler("bzz-download"), + s.actDecryptionHandler(), web.FinalHandlerFunc(s.bzzDownloadHandler), ), "HEAD": web.ChainHandlers( + s.actDecryptionHandler(), web.FinalHandlerFunc(s.bzzHeadHandler), ), }) @@ -409,7 +419,10 @@ func (s *Service) mountBusinessDebug(restricted bool) { }) handle("/chunks/{address}", jsonhttp.MethodHandler{ - "GET": http.HandlerFunc(s.hasChunkHandler), + "GET": web.ChainHandlers( + s.actDecryptionHandler(), + web.FinalHandlerFunc(s.hasChunkHandler), + ), }) handle("/topology", jsonhttp.MethodHandler{ diff --git a/pkg/api/soc.go b/pkg/api/soc.go index 0abf338deb9..b4ce98e91eb 100644 --- a/pkg/api/soc.go +++ b/pkg/api/soc.go @@ -43,8 +43,10 @@ func (s *Service) socUploadHandler(w http.ResponseWriter, r *http.Request) { } headers := struct { - BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` - Pin bool `map:"Swarm-Pin"` + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + Pin bool `map:"Swarm-Pin"` + Act bool `map:"Swarm-Act"` + HistoryAddress *swarm.Address `map:"Swarm-Act-History-Address"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) @@ -155,6 +157,15 @@ func (s *Service) socUploadHandler(w http.ResponseWriter, r *http.Request) { return } + encryptedReference := sch.Address() + if headers.Act { + encryptedReference, err = s.actEncryptionHandler(r.Context(), logger, w, putter, sch.Address(), headers.HistoryAddress) + if err != nil { + jsonhttp.InternalServerError(w, errActUpload) + return + } + } + err = putter.Put(r.Context(), sch) if err != nil { logger.Debug("write chunk failed", "chunk_address", sch.Address(), "error", err) @@ -171,5 +182,5 @@ func (s *Service) socUploadHandler(w http.ResponseWriter, r *http.Request) { return } - jsonhttp.Created(w, chunkAddressResponse{Reference: sch.Address()}) + jsonhttp.Created(w, socPostResponse{Reference: encryptedReference}) } diff --git a/pkg/dynamicaccess/accesslogic.go b/pkg/dynamicaccess/accesslogic.go index a2a6fbdce24..be9e8b194f1 100644 --- a/pkg/dynamicaccess/accesslogic.go +++ b/pkg/dynamicaccess/accesslogic.go @@ -1,6 +1,7 @@ package dynamicaccess import ( + "context" "crypto/ecdsa" encryption "github.com/ethersphere/bee/v2/pkg/encryption" @@ -14,7 +15,7 @@ var hashFunc = sha3.NewLegacyKeccak256 // Read-only interface for the ACT type Decryptor interface { // DecryptRef will return a decrypted reference, for given encrypted reference and grantee - DecryptRef(storage kvs.KeyValueStore, encryptedRef swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) + DecryptRef(ctx context.Context, storage kvs.KeyValueStore, encryptedRef swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) // Embedding the Session interface Session } @@ -24,9 +25,9 @@ type Control interface { // Embedding the Decryptor interface Decryptor // Adds a new grantee to the ACT - AddGrantee(storage kvs.KeyValueStore, publisherPubKey, granteePubKey *ecdsa.PublicKey, accessKey *encryption.Key) error + AddGrantee(ctx context.Context, storage kvs.KeyValueStore, publisherPubKey, granteePubKey *ecdsa.PublicKey, accessKey *encryption.Key) error // Encrypts a Swarm reference for a given grantee - EncryptRef(storage kvs.KeyValueStore, grantee *ecdsa.PublicKey, ref swarm.Address) (swarm.Address, error) + EncryptRef(ctx context.Context, storage kvs.KeyValueStore, grantee *ecdsa.PublicKey, ref swarm.Address) (swarm.Address, error) } type ActLogic struct { @@ -36,17 +37,17 @@ type ActLogic struct { var _ Control = (*ActLogic)(nil) // Adds a new publisher to an empty act -func (al ActLogic) AddPublisher(storage kvs.KeyValueStore, publisher *ecdsa.PublicKey) error { +func (al ActLogic) AddPublisher(ctx context.Context, storage kvs.KeyValueStore, publisher *ecdsa.PublicKey) error { accessKey := encryption.GenerateRandomKey(encryption.KeyLength) - return al.AddGrantee(storage, publisher, publisher, &accessKey) + return al.AddGrantee(ctx, storage, publisher, publisher, &accessKey) } // Encrypts a SWARM reference for a publisher -func (al ActLogic) EncryptRef(storage kvs.KeyValueStore, publisherPubKey *ecdsa.PublicKey, ref swarm.Address) (swarm.Address, error) { - accessKey, err := al.getAccessKey(storage, publisherPubKey) +func (al ActLogic) EncryptRef(ctx context.Context, storage kvs.KeyValueStore, publisherPubKey *ecdsa.PublicKey, ref swarm.Address) (swarm.Address, error) { + accessKey, err := al.getAccessKey(ctx, storage, publisherPubKey) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } refCipher := encryption.New(accessKey, 0, uint32(0), hashFunc) encryptedRef, _ := refCipher.Encrypt(ref.Bytes()) @@ -55,13 +56,13 @@ func (al ActLogic) EncryptRef(storage kvs.KeyValueStore, publisherPubKey *ecdsa. } // Adds a new grantee to the ACT -func (al ActLogic) AddGrantee(storage kvs.KeyValueStore, publisherPubKey, granteePubKey *ecdsa.PublicKey, accessKeyPointer *encryption.Key) error { +func (al ActLogic) AddGrantee(ctx context.Context, storage kvs.KeyValueStore, publisherPubKey, granteePubKey *ecdsa.PublicKey, accessKeyPointer *encryption.Key) error { var accessKey encryption.Key var err error // Declare the "err" variable if accessKeyPointer == nil { // Get previously generated access key - accessKey, err = al.getAccessKey(storage, publisherPubKey) + accessKey, err = al.getAccessKey(ctx, storage, publisherPubKey) if err != nil { return err } @@ -87,11 +88,11 @@ func (al ActLogic) AddGrantee(storage kvs.KeyValueStore, publisherPubKey, grante } // Add the new encrypted access key for the Act - return storage.Put(lookupKey, granteeEncryptedAccessKey) + return storage.Put(ctx, lookupKey, granteeEncryptedAccessKey) } // Will return the access key for a publisher (public key) -func (al *ActLogic) getAccessKey(storage kvs.KeyValueStore, publisherPubKey *ecdsa.PublicKey) ([]byte, error) { +func (al *ActLogic) getAccessKey(ctx context.Context, storage kvs.KeyValueStore, publisherPubKey *ecdsa.PublicKey) ([]byte, error) { keys, err := al.getKeys(publisherPubKey) if err != nil { return nil, err @@ -100,13 +101,12 @@ func (al *ActLogic) getAccessKey(storage kvs.KeyValueStore, publisherPubKey *ecd publisherAKDecryptionKey := keys[1] // no need to constructor call if value not found in act accessKeyDecryptionCipher := encryption.New(encryption.Key(publisherAKDecryptionKey), 0, uint32(0), hashFunc) - encryptedAK, err := storage.Get(publisherLookupKey) + encryptedAK, err := storage.Get(ctx, publisherLookupKey) if err != nil { return nil, err } return accessKeyDecryptionCipher.Decrypt(encryptedAK) - } var oneByteArray = []byte{1} @@ -118,32 +118,32 @@ func (al *ActLogic) getKeys(publicKey *ecdsa.PublicKey) ([][]byte, error) { } // DecryptRef will return a decrypted reference, for given encrypted reference and publisher -func (al ActLogic) DecryptRef(storage kvs.KeyValueStore, encryptedRef swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) { +func (al ActLogic) DecryptRef(ctx context.Context, storage kvs.KeyValueStore, encryptedRef swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) { keys, err := al.getKeys(publisher) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } lookupKey := keys[0] accessKeyDecryptionKey := keys[1] // Lookup encrypted access key from the ACT manifest - encryptedAccessKey, err := storage.Get(lookupKey) + encryptedAccessKey, err := storage.Get(ctx, lookupKey) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } // Decrypt access key accessKeyCipher := encryption.New(encryption.Key(accessKeyDecryptionKey), 0, uint32(0), hashFunc) accessKey, err := accessKeyCipher.Decrypt(encryptedAccessKey) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } // Decrypt reference refCipher := encryption.New(accessKey, 0, uint32(0), hashFunc) ref, err := refCipher.Decrypt(encryptedRef.Bytes()) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } return swarm.NewAddress(ref), nil diff --git a/pkg/dynamicaccess/accesslogic_test.go b/pkg/dynamicaccess/accesslogic_test.go index be115f424d1..66a3f8930e3 100644 --- a/pkg/dynamicaccess/accesslogic_test.go +++ b/pkg/dynamicaccess/accesslogic_test.go @@ -1,6 +1,7 @@ package dynamicaccess_test import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -14,7 +15,7 @@ import ( ) // Generates a new test environment with a fix private key -func setupAccessLogic2() dynamicaccess.ActLogic { +func setupAccessLogic() dynamicaccess.ActLogic { privateKey := getPrivKey(1) diffieHellman := dynamicaccess.NewDefaultSession(privateKey) al := dynamicaccess.NewLogic(diffieHellman) @@ -50,10 +51,11 @@ func getPrivKey(keyNumber int) *ecdsa.PrivateKey { } func TestDecryptRef_Success(t *testing.T) { + ctx := context.Background() id0 := getPrivKey(0) s := kvsmock.New() - al := setupAccessLogic2() - err := al.AddPublisher(s, &id0.PublicKey) + al := setupAccessLogic() + err := al.AddPublisher(ctx, s, &id0.PublicKey) if err != nil { t.Errorf("AddPublisher: expected no error, got %v", err) } @@ -63,14 +65,14 @@ func TestDecryptRef_Success(t *testing.T) { expectedRef := swarm.NewAddress(byteRef) t.Logf("encryptedRef: %s", expectedRef.String()) - encryptedRef, err := al.EncryptRef(s, &id0.PublicKey, expectedRef) + encryptedRef, err := al.EncryptRef(ctx, s, &id0.PublicKey, expectedRef) t.Logf("encryptedRef: %s", encryptedRef.String()) if err != nil { t.Errorf("There was an error while calling EncryptRef: ") t.Error(err) } - acutalRef, err := al.DecryptRef(s, encryptedRef, &id0.PublicKey) + acutalRef, err := al.DecryptRef(ctx, s, encryptedRef, &id0.PublicKey) if err != nil { t.Errorf("There was an error while calling Get: ") t.Error(err) @@ -83,18 +85,19 @@ func TestDecryptRef_Success(t *testing.T) { } func TestDecryptRefWithGrantee_Success(t *testing.T) { + ctx := context.Background() id0, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) diffieHellman := dynamicaccess.NewDefaultSession(id0) al := dynamicaccess.NewLogic(diffieHellman) s := kvsmock.New() - err := al.AddPublisher(s, &id0.PublicKey) + err := al.AddPublisher(ctx, s, &id0.PublicKey) if err != nil { t.Errorf("AddPublisher: expected no error, got %v", err) } id1, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - err = al.AddGrantee(s, &id0.PublicKey, &id1.PublicKey, nil) + err = al.AddGrantee(ctx, s, &id0.PublicKey, &id1.PublicKey, nil) if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } @@ -104,7 +107,7 @@ func TestDecryptRefWithGrantee_Success(t *testing.T) { expectedRef := swarm.NewAddress(byteRef) t.Logf("encryptedRef: %s", expectedRef.String()) - encryptedRef, err := al.EncryptRef(s, &id0.PublicKey, expectedRef) + encryptedRef, err := al.EncryptRef(ctx, s, &id0.PublicKey, expectedRef) t.Logf("encryptedRef: %s", encryptedRef.String()) if err != nil { t.Errorf("There was an error while calling EncryptRef: ") @@ -113,7 +116,7 @@ func TestDecryptRefWithGrantee_Success(t *testing.T) { diffieHellman2 := dynamicaccess.NewDefaultSession(id1) granteeAccessLogic := dynamicaccess.NewLogic(diffieHellman2) - acutalRef, err := granteeAccessLogic.DecryptRef(s, encryptedRef, &id0.PublicKey) + acutalRef, err := granteeAccessLogic.DecryptRef(ctx, s, encryptedRef, &id0.PublicKey) if err != nil { t.Errorf("There was an error while calling Get: ") t.Error(err) @@ -128,18 +131,19 @@ func TestDecryptRefWithGrantee_Success(t *testing.T) { func TestDecryptRef_Error(t *testing.T) { id0 := getPrivKey(0) + ctx := context.Background() s := kvsmock.New() - al := setupAccessLogic2() - err := al.AddPublisher(s, &id0.PublicKey) + al := setupAccessLogic() + err := al.AddPublisher(ctx, s, &id0.PublicKey) if err != nil { t.Errorf("AddPublisher: expected no error, got %v", err) } expectedRef := "39a5ea87b141fe44aa609c3327ecd896c0e2122897f5f4bbacf74db1033c5559" - encryptedRef, _ := al.EncryptRef(s, &id0.PublicKey, swarm.NewAddress([]byte(expectedRef))) + encryptedRef, _ := al.EncryptRef(ctx, s, &id0.PublicKey, swarm.NewAddress([]byte(expectedRef))) - r, err := al.DecryptRef(s, encryptedRef, nil) + r, err := al.DecryptRef(ctx, s, encryptedRef, nil) if err == nil { t.Logf("r: %s", r.String()) t.Errorf("Get should return encrypted access key not found error!") @@ -150,9 +154,10 @@ func TestAddPublisher(t *testing.T) { id0 := getPrivKey(0) savedLookupKey := "b6ee086390c280eeb9824c331a4427596f0c8510d5564bc1b6168d0059a46e2b" s := kvsmock.New() + ctx := context.Background() - al := setupAccessLogic2() - err := al.AddPublisher(s, &id0.PublicKey) + al := setupAccessLogic() + err := al.AddPublisher(ctx, s, &id0.PublicKey) if err != nil { t.Errorf("AddPublisher: expected no error, got %v", err) } @@ -162,7 +167,7 @@ func TestAddPublisher(t *testing.T) { t.Errorf("DecodeString: expected no error, got %v", err) } - encryptedAccessKey, err := s.Get(decodedSavedLookupKey) + encryptedAccessKey, err := s.Get(ctx, decodedSavedLookupKey) if err != nil { t.Errorf("Lookup: expected no error, got %v", err) } @@ -183,24 +188,25 @@ func TestAddNewGranteeToContent(t *testing.T) { id0 := getPrivKey(0) id1 := getPrivKey(1) id2 := getPrivKey(2) + ctx := context.Background() publisherLookupKey := "b6ee086390c280eeb9824c331a4427596f0c8510d5564bc1b6168d0059a46e2b" firstAddedGranteeLookupKey := "a13678e81f9d939b9401a3ad7e548d2ceb81c50f8c76424296e83a1ad79c0df0" secondAddedGranteeLookupKey := "d5e9a6499ca74f5b8b958a4b89b7338045b2baa9420e115443a8050e26986564" s := kvsmock.New() - al := setupAccessLogic2() - err := al.AddPublisher(s, &id0.PublicKey) + al := setupAccessLogic() + err := al.AddPublisher(ctx, s, &id0.PublicKey) if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } - err = al.AddGrantee(s, &id0.PublicKey, &id1.PublicKey, nil) + err = al.AddGrantee(ctx, s, &id0.PublicKey, &id1.PublicKey, nil) if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } - err = al.AddGrantee(s, &id0.PublicKey, &id2.PublicKey, nil) + err = al.AddGrantee(ctx, s, &id0.PublicKey, &id2.PublicKey, nil) if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } @@ -209,7 +215,7 @@ func TestAddNewGranteeToContent(t *testing.T) { if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } - result, _ := s.Get(lookupKeyAsByte) + result, _ := s.Get(ctx, lookupKeyAsByte) hexEncodedEncryptedAK := hex.EncodeToString(result) if len(hexEncodedEncryptedAK) != 64 { t.Errorf("AddNewGrantee: expected encrypted access key length 64, got %d", len(hexEncodedEncryptedAK)) @@ -219,7 +225,7 @@ func TestAddNewGranteeToContent(t *testing.T) { if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } - result, _ = s.Get(lookupKeyAsByte) + result, _ = s.Get(ctx, lookupKeyAsByte) hexEncodedEncryptedAK = hex.EncodeToString(result) if len(hexEncodedEncryptedAK) != 64 { t.Errorf("AddNewGrantee: expected encrypted access key length 64, got %d", len(hexEncodedEncryptedAK)) @@ -229,7 +235,7 @@ func TestAddNewGranteeToContent(t *testing.T) { if err != nil { t.Errorf("AddNewGrantee: expected no error, got %v", err) } - result, _ = s.Get(lookupKeyAsByte) + result, _ = s.Get(ctx, lookupKeyAsByte) hexEncodedEncryptedAK = hex.EncodeToString(result) if len(hexEncodedEncryptedAK) != 64 { t.Errorf("AddNewGrantee: expected encrypted access key length 64, got %d", len(hexEncodedEncryptedAK)) diff --git a/pkg/dynamicaccess/controller.go b/pkg/dynamicaccess/controller.go index ccdc8c2d7f4..87a24e707b2 100644 --- a/pkg/dynamicaccess/controller.go +++ b/pkg/dynamicaccess/controller.go @@ -1,54 +1,208 @@ package dynamicaccess import ( + "context" "crypto/ecdsa" + "time" + "github.com/ethersphere/bee/v2/pkg/file/loadsave" + "github.com/ethersphere/bee/v2/pkg/file/pipeline" + "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/kvs" kvsmock "github.com/ethersphere/bee/v2/pkg/kvs/mock" + "github.com/ethersphere/bee/v2/pkg/storage" "github.com/ethersphere/bee/v2/pkg/swarm" ) +type GranteeManager interface { + //PUT /grantees/{grantee} + //body: {publisher?, grantee root hash ,grantee} + Grant(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error + //DELETE /grantees/{grantee} + //body: {publisher?, grantee root hash , grantee} + Revoke(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error + //[ ] + //POST /grantees + //body: {publisher, historyRootHash} + Commit(ctx context.Context, granteesAddress swarm.Address, actRootHash swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, swarm.Address, error) + + //Post /grantees + //{publisher, addList, removeList} + HandleGrantees(ctx context.Context, rootHash swarm.Address, publisher *ecdsa.PublicKey, addList, removeList []*ecdsa.PublicKey) error + + //GET /grantees/{history root hash} + GetGrantees(ctx context.Context, rootHash swarm.Address) ([]*ecdsa.PublicKey, error) +} + +// TODO: ądd granteeList ref to history metadata to solve inconsistency type Controller interface { - DownloadHandler(timestamp int64, enryptedRef swarm.Address, publisher *ecdsa.PublicKey, tag string) (swarm.Address, error) - UploadHandler(ref swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) + GranteeManager + DownloadHandler(ctx context.Context, timestamp int64, encryptedRef swarm.Address, publisher *ecdsa.PublicKey, historyRootHash swarm.Address) (swarm.Address, error) + UploadHandler(ctx context.Context, reference swarm.Address, publisher *ecdsa.PublicKey, historyRootHash *swarm.Address) (swarm.Address, swarm.Address, swarm.Address, error) } -type defaultController struct { - history History - granteeManager GranteeManager - accessLogic ActLogic +type controller struct { + accessLogic ActLogic + granteeList GranteeList + //[ ]: do we need to protect this with a mutex? + revokeFlag []swarm.Address + getter storage.Getter + putter storage.Putter } -func (c *defaultController) DownloadHandler(timestamp int64, enryptedRef swarm.Address, publisher *ecdsa.PublicKey, tag string) (swarm.Address, error) { - kvs, err := c.history.Lookup(timestamp) +var _ Controller = (*controller)(nil) + +func (c *controller) DownloadHandler( + ctx context.Context, + timestamp int64, + encryptedRef swarm.Address, + publisher *ecdsa.PublicKey, + historyRootHash swarm.Address, +) (swarm.Address, error) { + ls := loadsave.New(c.getter, c.putter, requestPipelineFactory(ctx, c.putter, false, redundancy.NONE)) + history, err := NewHistory(ls, &historyRootHash) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, err } - addr, err := c.accessLogic.DecryptRef(kvs, enryptedRef, publisher) - return addr, err + + kvsRef, err := history.Lookup(ctx, timestamp) + if err != nil { + return swarm.ZeroAddress, err + } + kvs := kvs.New(ls, kvsRef) + return c.accessLogic.DecryptRef(ctx, kvs, encryptedRef, publisher) } -func (c *defaultController) UploadHandler(ref swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, error) { - kvs, err := c.history.Lookup(0) +// TODO: review return params: how to get back history ref ? +func (c *controller) UploadHandler( + ctx context.Context, + refrefence swarm.Address, + publisher *ecdsa.PublicKey, + historyRootHash *swarm.Address, +) (swarm.Address, swarm.Address, swarm.Address, error) { + ls := loadsave.New(c.getter, c.putter, requestPipelineFactory(ctx, c.putter, false, redundancy.NONE)) + history, err := NewHistory(ls, historyRootHash) + if err != nil { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err + } + now := time.Now().Unix() + kvsRef, err := history.Lookup(ctx, now) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err } - if kvs == nil { - // new feed - // TODO: putter session to create kvs - kvs = kvsmock.New() - _, err = c.granteeManager.Publish(kvs, publisher) + kvs := kvs.New(ls, kvsRef) + historyRef := swarm.ZeroAddress + if historyRootHash != nil { + historyRef = *historyRootHash + } + if kvsRef.Equal(swarm.ZeroAddress) { + err = c.accessLogic.AddPublisher(ctx, kvs, publisher) + if err != nil { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err + } + kvsRef, err = kvs.Save(ctx) if err != nil { - return swarm.EmptyAddress, err + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err + } + err = history.Add(ctx, kvsRef, &now) + if err != nil { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err + } + historyRef, err = history.Store(ctx) + if err != nil { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, err + } + } + encryptedRef, err := c.accessLogic.EncryptRef(ctx, kvs, publisher, refrefence) + return kvsRef, historyRef, encryptedRef, err +} + +func NewController(ctx context.Context, accessLogic ActLogic, getter storage.Getter, putter storage.Putter) Controller { + return &controller{ + granteeList: nil, + accessLogic: accessLogic, + getter: getter, + putter: putter, + } +} + +func (c *controller) Grant(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error { + return c.granteeList.Add([]*ecdsa.PublicKey{grantee}) +} + +func (c *controller) Revoke(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error { + if !c.isRevokeFlagged(granteesAddress) { + c.setRevokeFlag(granteesAddress, true) + } + return c.granteeList.Remove([]*ecdsa.PublicKey{grantee}) +} + +func (c *controller) Commit(ctx context.Context, granteesAddress swarm.Address, actRootHash swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, swarm.Address, error) { + var act kvs.KeyValueStore + if c.isRevokeFlagged(granteesAddress) { + act = kvsmock.New() + c.accessLogic.AddPublisher(ctx, act, publisher) + } else { + act = kvsmock.NewReference(actRootHash) + } + + grantees := c.granteeList.Get() + for _, grantee := range grantees { + c.accessLogic.AddGrantee(ctx, act, publisher, grantee, nil) + } + + granteeref, err := c.granteeList.Save(ctx) + if err != nil { + return swarm.EmptyAddress, swarm.EmptyAddress, err + } + + actref, err := act.Save(ctx) + if err != nil { + return swarm.EmptyAddress, swarm.EmptyAddress, err + } + + c.setRevokeFlag(granteesAddress, false) + return granteeref, actref, err +} + +func (c *controller) HandleGrantees(ctx context.Context, granteesAddress swarm.Address, publisher *ecdsa.PublicKey, addList, removeList []*ecdsa.PublicKey) error { + act := kvsmock.New() + + c.accessLogic.AddPublisher(ctx, act, publisher) + for _, grantee := range addList { + c.accessLogic.AddGrantee(ctx, act, publisher, grantee, nil) + } + return nil +} + +func (c *controller) GetGrantees(ctx context.Context, granteeRootHash swarm.Address) ([]*ecdsa.PublicKey, error) { + return c.granteeList.Get(), nil +} + +func (c *controller) isRevokeFlagged(granteeRootHash swarm.Address) bool { + for _, revoke := range c.revokeFlag { + if revoke.Equal(granteeRootHash) { + return true + } + } + return false +} + +func (c *controller) setRevokeFlag(granteeRootHash swarm.Address, set bool) { + if set { + c.revokeFlag = append(c.revokeFlag, granteeRootHash) + } else { + for i, revoke := range c.revokeFlag { + if revoke.Equal(granteeRootHash) { + c.revokeFlag = append(c.revokeFlag[:i], c.revokeFlag[i+1:]...) + } } } - //FIXME: check if kvs is consistent with the grantee list - return c.accessLogic.EncryptRef(kvs, publisher, ref) } -func NewController(history History, granteeManager GranteeManager, accessLogic ActLogic) Controller { - return &defaultController{ - history: history, - granteeManager: granteeManager, - accessLogic: accessLogic, +func requestPipelineFactory(ctx context.Context, s storage.Putter, encrypt bool, rLevel redundancy.Level) func() pipeline.Interface { + return func() pipeline.Interface { + return builder.NewPipelineBuilder(ctx, s, encrypt, rLevel) } } diff --git a/pkg/dynamicaccess/controller_test.go b/pkg/dynamicaccess/controller_test.go index efbfc8d4e42..0512e6481f9 100644 --- a/pkg/dynamicaccess/controller_test.go +++ b/pkg/dynamicaccess/controller_test.go @@ -1,83 +1,93 @@ package dynamicaccess_test import ( + "context" "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "encoding/hex" "testing" "time" - "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/ethersphere/bee/v2/pkg/dynamicaccess" - "github.com/ethersphere/bee/v2/pkg/dynamicaccess/mock" "github.com/ethersphere/bee/v2/pkg/encryption" - kvsmock "github.com/ethersphere/bee/v2/pkg/kvs/mock" + "github.com/ethersphere/bee/v2/pkg/file" + "github.com/ethersphere/bee/v2/pkg/kvs" "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/stretchr/testify/assert" "golang.org/x/crypto/sha3" ) var hashFunc = sha3.NewLegacyKeccak256 -func mockTestHistory(key, val []byte) dynamicaccess.History { - var ( - h = mock.NewHistory() - now = time.Now() - s = kvsmock.New() - ) - _ = s.Put(key, val) - h.Insert(now.AddDate(-3, 0, 0).Unix(), s) - return h +func getHistoryFixture(ctx context.Context, ls file.LoadSaver, al dynamicaccess.ActLogic, publisher *ecdsa.PublicKey) (swarm.Address, error) { + h, err := dynamicaccess.NewHistory(ls, nil) + if err != nil { + return swarm.ZeroAddress, nil + } + pk1 := getPrivKey(1) + pk2 := getPrivKey(2) + + kvs0 := kvs.New(ls, swarm.ZeroAddress) + al.AddPublisher(ctx, kvs0, publisher) + kvs0Ref, _ := kvs0.Save(ctx) + kvs1 := kvs.New(ls, swarm.ZeroAddress) + al.AddGrantee(ctx, kvs1, publisher, &pk1.PublicKey, nil) + al.AddPublisher(ctx, kvs1, publisher) + kvs1Ref, _ := kvs1.Save(ctx) + kvs2 := kvs.New(ls, swarm.ZeroAddress) + al.AddGrantee(ctx, kvs2, publisher, &pk2.PublicKey, nil) + al.AddPublisher(ctx, kvs2, publisher) + kvs2Ref, _ := kvs2.Save(ctx) + firstTime := time.Date(1994, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + secondTime := time.Date(2000, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + thirdTime := time.Date(2015, time.April, 1, 0, 0, 0, 0, time.UTC).Unix() + + h.Add(ctx, kvs0Ref, &thirdTime) + h.Add(ctx, kvs1Ref, &firstTime) + h.Add(ctx, kvs2Ref, &secondTime) + return h.Store(ctx) } -func TestDecrypt(t *testing.T) { - pk := getPrivateKey() - ak := encryption.Key([]byte("cica")) - - si := dynamicaccess.NewDefaultSession(pk) - aek, _ := si.Key(&pk.PublicKey, [][]byte{{0}, {1}}) - e2 := encryption.New(aek[1], 0, uint32(0), hashFunc) - peak, _ := e2.Encrypt(ak) - - h := mockTestHistory(aek[0], peak) - al := setupAccessLogic(pk) - gm := dynamicaccess.NewGranteeManager(al) - c := dynamicaccess.NewController(h, gm, al) - eref, ref := prepareEncryptedChunkReference(ak) - // ech := al.EncryptRef(ch, "tag") +// TODO: separate up down test with fixture, now these just check if the flow works at all +func TestController_NewUploadDownload(t *testing.T) { + ctx := context.Background() + publisher := getPrivKey(1) + diffieHellman := dynamicaccess.NewDefaultSession(publisher) + al := dynamicaccess.NewLogic(diffieHellman) + c := dynamicaccess.NewController(ctx, al, mockStorer.ChunkStore(), mockStorer.Cache()) + ref := swarm.RandAddress(t) + _, hRef, encryptedRef, err := c.UploadHandler(ctx, ref, &publisher.PublicKey, nil) + assert.NoError(t, err) + dref, err := c.DownloadHandler(ctx, time.Now().Unix(), encryptedRef, &publisher.PublicKey, hRef) + assert.NoError(t, err) + assert.Equal(t, ref, dref) +} - ts := int64(0) - addr, err := c.DownloadHandler(ts, eref, &pk.PublicKey, "tag") - if err != nil { - t.Fatalf("DownloadHandler() returned an error: %v", err) - } - if !addr.Equal(ref) { - t.Fatalf("Decrypted chunk address: %s is not the expected: %s", addr, ref) - } +func TestController_ExistingUploadDownload(t *testing.T) { + ls := createLs() + ctx := context.Background() + publisher := getPrivKey(0) + diffieHellman := dynamicaccess.NewDefaultSession(publisher) + al := dynamicaccess.NewLogic(diffieHellman) + c := dynamicaccess.NewController(ctx, al, mockStorer.ChunkStore(), mockStorer.Cache()) + ref := swarm.RandAddress(t) + hRef, err := getHistoryFixture(ctx, ls, al, &publisher.PublicKey) + assert.NoError(t, err) + _, hRef, encryptedRef, err := c.UploadHandler(ctx, ref, &publisher.PublicKey, &hRef) + assert.NoError(t, err) + dref, err := c.DownloadHandler(ctx, time.Now().Unix(), encryptedRef, &publisher.PublicKey, hRef) + assert.NoError(t, err) + assert.Equal(t, ref, dref) } -func TestEncrypt(t *testing.T) { - pk := getPrivateKey() - ak := encryption.Key([]byte("cica")) +func TestControllerGrant(t *testing.T) { +} - si := dynamicaccess.NewDefaultSession(pk) - aek, _ := si.Key(&pk.PublicKey, [][]byte{{0}, {1}}) - e2 := encryption.New(aek[1], 0, uint32(0), hashFunc) - peak, _ := e2.Encrypt(ak) +func TestControllerRevoke(t *testing.T) { - h := mockTestHistory(aek[0], peak) - al := setupAccessLogic(pk) - gm := dynamicaccess.NewGranteeManager(al) - c := dynamicaccess.NewController(h, gm, al) - eref, ref := prepareEncryptedChunkReference(ak) +} - key1, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - gm.Add([]*ecdsa.PublicKey{&key1.PublicKey}) +func TestControllerCommit(t *testing.T) { - addr, _ := c.UploadHandler(ref, &pk.PublicKey) - if !addr.Equal(eref) { - t.Fatalf("Decrypted chunk address: %s is not the expected: %s", addr, eref) - } } func prepareEncryptedChunkReference(ak []byte) (swarm.Address, swarm.Address) { @@ -85,14 +95,7 @@ func prepareEncryptedChunkReference(ak []byte) (swarm.Address, swarm.Address) { e1 := encryption.New(ak, 0, uint32(0), hashFunc) ech, err := e1.Encrypt(addr) if err != nil { - return swarm.EmptyAddress, swarm.NewAddress(addr) + return swarm.EmptyAddress, swarm.EmptyAddress } return swarm.NewAddress(ech), swarm.NewAddress(addr) } - -func getPrivateKey() *ecdsa.PrivateKey { - data, _ := hex.DecodeString("c786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") - - privKey, _ := crypto.DecodeSecp256k1PrivateKey(data) - return privKey -} diff --git a/pkg/dynamicaccess/grantee.go b/pkg/dynamicaccess/grantee.go index d850fc047cc..8499cfc4f04 100644 --- a/pkg/dynamicaccess/grantee.go +++ b/pkg/dynamicaccess/grantee.go @@ -19,7 +19,7 @@ type GranteeList interface { Add(addList []*ecdsa.PublicKey) error Remove(removeList []*ecdsa.PublicKey) error Get() []*ecdsa.PublicKey - Save() (swarm.Address, error) + Save(ctx context.Context) (swarm.Address, error) } type GranteeListStruct struct { @@ -78,8 +78,8 @@ func (g *GranteeListStruct) Add(addList []*ecdsa.PublicKey) error { return nil } -func (g *GranteeListStruct) Save() (swarm.Address, error) { - refBytes, err := g.loadSave.Save(context.Background(), g.grantees) +func (g *GranteeListStruct) Save(ctx context.Context) (swarm.Address, error) { + refBytes, err := g.loadSave.Save(ctx, g.grantees) if err != nil { return swarm.ZeroAddress, fmt.Errorf("grantee save error: %w", err) } @@ -132,3 +132,7 @@ func NewGranteeList(ls file.LoadSaver, putter storer.PutterSession, reference sw putter: putter, } } + +func (g *GranteeListStruct) Store() (swarm.Address, error) { + return swarm.EmptyAddress, nil +} diff --git a/pkg/dynamicaccess/grantee_manager.go b/pkg/dynamicaccess/grantee_manager.go deleted file mode 100644 index 1fac35a38bd..00000000000 --- a/pkg/dynamicaccess/grantee_manager.go +++ /dev/null @@ -1,47 +0,0 @@ -package dynamicaccess - -import ( - "crypto/ecdsa" - - "github.com/ethersphere/bee/v2/pkg/dynamicaccess/mock" - "github.com/ethersphere/bee/v2/pkg/kvs" - "github.com/ethersphere/bee/v2/pkg/swarm" -) - -type GranteeManager interface { - Get() []*ecdsa.PublicKey - Add(addList []*ecdsa.PublicKey) error - Publish(kvs kvs.KeyValueStore, publisher *ecdsa.PublicKey) (swarm.Address, error) - - // HandleGrantees(addList, removeList []*ecdsa.PublicKey) *Act - - // Load(grantee Grantee) - // Save() -} - -var _ GranteeManager = (*granteeManager)(nil) - -type granteeManager struct { - accessLogic ActLogic - granteeList *mock.GranteeListStructMock -} - -func NewGranteeManager(al ActLogic) *granteeManager { - return &granteeManager{accessLogic: al, granteeList: mock.NewGranteeList()} -} - -func (gm *granteeManager) Get() []*ecdsa.PublicKey { - return gm.granteeList.Get() -} - -func (gm *granteeManager) Add(addList []*ecdsa.PublicKey) error { - return gm.granteeList.Add(addList) -} - -func (gm *granteeManager) Publish(kvs kvs.KeyValueStore, publisher *ecdsa.PublicKey) (swarm.Address, error) { - err := gm.accessLogic.AddPublisher(kvs, publisher) - for _, grantee := range gm.granteeList.Get() { - err = gm.accessLogic.AddGrantee(kvs, publisher, grantee, nil) - } - return swarm.EmptyAddress, err -} diff --git a/pkg/dynamicaccess/grantee_manager_test.go b/pkg/dynamicaccess/grantee_manager_test.go deleted file mode 100644 index bb01c13cd85..00000000000 --- a/pkg/dynamicaccess/grantee_manager_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package dynamicaccess_test - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "fmt" - "testing" - - "github.com/ethersphere/bee/v2/pkg/dynamicaccess" - kvsmock "github.com/ethersphere/bee/v2/pkg/kvs/mock" -) - -func setupAccessLogic(privateKey *ecdsa.PrivateKey) dynamicaccess.ActLogic { - si := dynamicaccess.NewDefaultSession(privateKey) - al := dynamicaccess.NewLogic(si) - - return al -} - -func TestAdd(t *testing.T) { - m := dynamicaccess.NewGranteeManager(setupAccessLogic(getPrivateKey())) - pub, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - - id1, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - id2, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - err := m.Add([]*ecdsa.PublicKey{&id1.PublicKey}) - if err != nil { - t.Errorf("Add() returned an error") - } - err = m.Add([]*ecdsa.PublicKey{&id2.PublicKey}) - if err != nil { - t.Errorf("Add() returned an error") - } - s := kvsmock.New() - m.Publish(s, &pub.PublicKey) - fmt.Println("") -} diff --git a/pkg/dynamicaccess/grantee_test.go b/pkg/dynamicaccess/grantee_test.go index b644f5896c5..5730911ad74 100644 --- a/pkg/dynamicaccess/grantee_test.go +++ b/pkg/dynamicaccess/grantee_test.go @@ -148,19 +148,20 @@ func TestGranteeRemove(t *testing.T) { } func TestGranteeSave(t *testing.T) { + ctx := context.Background() keys, err := generateKeyListFixture() if err != nil { t.Errorf("key generation error: %v", err) } t.Run("Save empty grantee list return NO error", func(t *testing.T) { gl := dynamicaccess.NewGranteeList(createLs(), mockStorer.DirectUpload(), swarm.ZeroAddress) - _, err := gl.Save() + _, err := gl.Save(ctx) assert.NoError(t, err) }) t.Run("Save not empty grantee list return valid swarm address", func(t *testing.T) { gl := dynamicaccess.NewGranteeList(createLs(), mockStorer.DirectUpload(), swarm.ZeroAddress) err = gl.Add(keys) - ref, err := gl.Save() + ref, err := gl.Save(ctx) assert.NoError(t, err) assert.True(t, ref.IsValidNonEmpty()) }) @@ -172,7 +173,7 @@ func TestGranteeSave(t *testing.T) { err := gl1.Add(keys) assert.NoError(t, err) - ref, err := gl1.Save() + ref, err := gl1.Save(ctx) assert.NoError(t, err) gl2 := dynamicaccess.NewGranteeList(ls, putter, ref) @@ -188,7 +189,7 @@ func TestGranteeSave(t *testing.T) { err := gl1.Add(keys) assert.NoError(t, err) - ref, err := gl1.Save() + ref, err := gl1.Save(ctx) assert.NoError(t, err) // New KVS diff --git a/pkg/dynamicaccess/mock/service.go b/pkg/dynamicaccess/mock/service.go new file mode 100644 index 00000000000..2d19c3ed4e3 --- /dev/null +++ b/pkg/dynamicaccess/mock/service.go @@ -0,0 +1,162 @@ +// Copyright 2020 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mock + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" + "github.com/ethersphere/bee/v2/pkg/encryption" + "github.com/ethersphere/bee/v2/pkg/file" + "github.com/ethersphere/bee/v2/pkg/file/loadsave" + "github.com/ethersphere/bee/v2/pkg/file/pipeline" + "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/storage" + mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" + "github.com/ethersphere/bee/v2/pkg/swarm" + "golang.org/x/crypto/sha3" +) + +type mockDacService struct { + historyMap map[string]dynamicaccess.History + refMap map[string]swarm.Address + acceptAll bool + publisher string + encrypter encryption.Interface + ls file.LoadSaver +} + +type optionFunc func(*mockDacService) + +// Option is an option passed to a mock dynamicaccess Service. +type Option interface { + apply(*mockDacService) +} + +func (f optionFunc) apply(r *mockDacService) { f(r) } + +// New creates a new mock dynamicaccess service. +func New(o ...Option) dynamicaccess.Service { + storer := mockstorer.New() + m := &mockDacService{ + historyMap: make(map[string]dynamicaccess.History), + refMap: make(map[string]swarm.Address), + publisher: "", + encrypter: encryption.New(encryption.Key("b6ee086390c280eeb9824c331a4427596f0c8510d5564bc1b6168d0059a46e2b"), 0, uint32(0), sha3.NewLegacyKeccak256), + ls: loadsave.New(storer.ChunkStore(), storer.Cache(), requestPipelineFactory(context.Background(), storer.Cache(), false, redundancy.NONE)), + } + for _, v := range o { + v.apply(m) + } + + return m +} + +// WithAcceptAll sets the mock to return fixed references on every call to DownloadHandler. +func WithAcceptAll() Option { + return optionFunc(func(m *mockDacService) { m.acceptAll = true }) +} + +func WithHistory(h dynamicaccess.History, ref string) Option { + return optionFunc(func(m *mockDacService) { + m.historyMap = map[string]dynamicaccess.History{ref: h} + }) +} + +func WithPublisher(ref string) Option { + return optionFunc(func(m *mockDacService) { + m.publisher = ref + m.encrypter = encryption.New(encryption.Key(ref), 0, uint32(0), sha3.NewLegacyKeccak256) + }) +} + +func (m *mockDacService) DownloadHandler(ctx context.Context, timestamp int64, encryptedRef swarm.Address, publisher *ecdsa.PublicKey, historyRootHash swarm.Address) (swarm.Address, error) { + if m.acceptAll { + return swarm.ParseHexAddress("36e6c1bbdfee6ac21485d5f970479fd1df458d36df9ef4e8179708ed46da557f") + } + + publicKeyBytes := crypto.EncodeSecp256k1PublicKey(publisher) + p := hex.EncodeToString(publicKeyBytes) + if m.publisher != "" && m.publisher != p { + return swarm.ZeroAddress, fmt.Errorf("incorrect publisher") + } + + h, exists := m.historyMap[historyRootHash.String()] + if !exists { + return swarm.ZeroAddress, fmt.Errorf("history not found") + } + kvsRef, err := h.Lookup(ctx, timestamp) + if kvsRef.Equal(swarm.ZeroAddress) || err != nil { + return swarm.ZeroAddress, fmt.Errorf("kvs not found") + } + return m.refMap[encryptedRef.String()], nil +} + +func (m *mockDacService) UploadHandler(ctx context.Context, reference swarm.Address, publisher *ecdsa.PublicKey, historyRootHash *swarm.Address) (swarm.Address, swarm.Address, swarm.Address, error) { + historyRef, _ := swarm.ParseHexAddress("67bdf80a9bbea8eca9c8480e43fdceb485d2d74d5708e45144b8c4adacd13d9c") + kvsRef, _ := swarm.ParseHexAddress("3339613565613837623134316665343461613630396333333237656364383934") + if m.acceptAll { + encryptedRef, _ := swarm.ParseHexAddress("fc4e9fe978991257b897d987bc4ff13058b66ef45a53189a0b4fe84bb3346396") + return kvsRef, historyRef, encryptedRef, nil + } + var ( + h dynamicaccess.History + exists bool + ) + now := time.Now().Unix() + if historyRootHash != nil { + historyRef = *historyRootHash + h, exists = m.historyMap[historyRef.String()] + if !exists { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, fmt.Errorf("history not found") + } + kvsRef, _ = h.Lookup(ctx, now) + } else { + h, _ = dynamicaccess.NewHistory(m.ls, nil) + h.Add(ctx, kvsRef, &now) + historyRef, _ = h.Store(ctx) + m.historyMap[historyRef.String()] = h + } + if kvsRef.Equal(swarm.ZeroAddress) { + return swarm.ZeroAddress, swarm.ZeroAddress, swarm.ZeroAddress, fmt.Errorf("kvs not found") + } + encryptedRef, _ := m.encrypter.Encrypt(reference.Bytes()) + m.refMap[(hex.EncodeToString(encryptedRef))] = reference + return kvsRef, historyRef, swarm.NewAddress(encryptedRef), nil +} + +func (m *mockDacService) Close() error { + return nil +} + +func (m *mockDacService) Grant(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error { + return nil +} +func (m *mockDacService) Revoke(ctx context.Context, granteesAddress swarm.Address, grantee *ecdsa.PublicKey) error { + return nil +} +func (m *mockDacService) Commit(ctx context.Context, granteesAddress swarm.Address, actRootHash swarm.Address, publisher *ecdsa.PublicKey) (swarm.Address, swarm.Address, error) { + return swarm.ZeroAddress, swarm.ZeroAddress, nil +} +func (m *mockDacService) HandleGrantees(ctx context.Context, rootHash swarm.Address, publisher *ecdsa.PublicKey, addList, removeList []*ecdsa.PublicKey) error { + return nil +} +func (m *mockDacService) GetGrantees(ctx context.Context, rootHash swarm.Address) ([]*ecdsa.PublicKey, error) { + return nil, nil +} + +func requestPipelineFactory(ctx context.Context, s storage.Putter, encrypt bool, rLevel redundancy.Level) func() pipeline.Interface { + return func() pipeline.Interface { + return builder.NewPipelineBuilder(ctx, s, encrypt, rLevel) + } +} + +var _ dynamicaccess.Controller = (*mockDacService)(nil) diff --git a/pkg/dynamicaccess/service.go b/pkg/dynamicaccess/service.go new file mode 100644 index 00000000000..c87b74ee60c --- /dev/null +++ b/pkg/dynamicaccess/service.go @@ -0,0 +1,39 @@ +package dynamicaccess + +import ( + "context" + "crypto/ecdsa" + "io" + + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +type Service interface { + DownloadHandler(ctx context.Context, timestamp int64, encryptedRef swarm.Address, publisher *ecdsa.PublicKey, historyRootHash swarm.Address) (swarm.Address, error) + UploadHandler(ctx context.Context, reference swarm.Address, publisher *ecdsa.PublicKey, historyRootHash *swarm.Address) (swarm.Address, swarm.Address, swarm.Address, error) + io.Closer +} + +// TODO: is service needed at all? -> it is just a wrapper around controller +type service struct { + controller Controller +} + +func (s *service) DownloadHandler(ctx context.Context, timestamp int64, encryptedRef swarm.Address, publisher *ecdsa.PublicKey, historyRootHash swarm.Address) (swarm.Address, error) { + return s.controller.DownloadHandler(ctx, timestamp, encryptedRef, publisher, historyRootHash) +} + +func (s *service) UploadHandler(ctx context.Context, reference swarm.Address, publisher *ecdsa.PublicKey, historyRootHash *swarm.Address) (swarm.Address, swarm.Address, swarm.Address, error) { + return s.controller.UploadHandler(ctx, reference, publisher, historyRootHash) +} + +// TODO: what to do in close ? +func (s *service) Close() error { + return nil +} + +func NewService(controller Controller) (Service, error) { + return &service{ + controller: controller, + }, nil +} diff --git a/pkg/kvs/kvs.go b/pkg/kvs/kvs.go index fcb5fc668bb..42a07da6738 100644 --- a/pkg/kvs/kvs.go +++ b/pkg/kvs/kvs.go @@ -11,27 +11,24 @@ import ( "github.com/ethersphere/bee/v2/pkg/file" "github.com/ethersphere/bee/v2/pkg/manifest" - "github.com/ethersphere/bee/v2/pkg/storer" "github.com/ethersphere/bee/v2/pkg/swarm" ) type KeyValueStore interface { - Get(key []byte) ([]byte, error) - Put(key, value []byte) error - Save() (swarm.Address, error) + Get(ctx context.Context, key []byte) ([]byte, error) + Put(ctx context.Context, key, value []byte) error + Save(ctx context.Context) (swarm.Address, error) } type keyValueStore struct { manifest manifest.Interface - putter storer.PutterSession putCnt int } var _ KeyValueStore = (*keyValueStore)(nil) -// TODO: pass context as dep. -func (s *keyValueStore) Get(key []byte) ([]byte, error) { - entry, err := s.manifest.Lookup(context.Background(), hex.EncodeToString(key)) +func (s *keyValueStore) Get(ctx context.Context, key []byte) ([]byte, error) { + entry, err := s.manifest.Lookup(ctx, hex.EncodeToString(key)) if err != nil { return nil, err } @@ -39,8 +36,8 @@ func (s *keyValueStore) Get(key []byte) ([]byte, error) { return ref.Bytes(), nil } -func (s *keyValueStore) Put(key []byte, value []byte) error { - err := s.manifest.Add(context.Background(), hex.EncodeToString(key), manifest.NewEntry(swarm.NewAddress(value), map[string]string{})) +func (s *keyValueStore) Put(ctx context.Context, key []byte, value []byte) error { + err := s.manifest.Add(ctx, hex.EncodeToString(key), manifest.NewEntry(swarm.NewAddress(value), map[string]string{})) if err != nil { return err } @@ -48,15 +45,11 @@ func (s *keyValueStore) Put(key []byte, value []byte) error { return nil } -func (s *keyValueStore) Save() (swarm.Address, error) { +func (s *keyValueStore) Save(ctx context.Context) (swarm.Address, error) { if s.putCnt == 0 { return swarm.ZeroAddress, errors.New("nothing to save") } - ref, err := s.manifest.Store(context.Background()) - if err != nil { - return swarm.ZeroAddress, err - } - err = s.putter.Done(ref) + ref, err := s.manifest.Store(ctx) if err != nil { return swarm.ZeroAddress, err } @@ -64,7 +57,7 @@ func (s *keyValueStore) Save() (swarm.Address, error) { return ref, nil } -func New(ls file.LoadSaver, putter storer.PutterSession, rootHash swarm.Address) KeyValueStore { +func New(ls file.LoadSaver, rootHash swarm.Address) KeyValueStore { var ( manif manifest.Interface err error @@ -80,6 +73,5 @@ func New(ls file.LoadSaver, putter storer.PutterSession, rootHash swarm.Address) return &keyValueStore{ manifest: manif, - putter: putter, } } diff --git a/pkg/kvs/kvs_test.go b/pkg/kvs/kvs_test.go index 9edfe48062c..5c6f75e2379 100644 --- a/pkg/kvs/kvs_test.go +++ b/pkg/kvs/kvs_test.go @@ -38,24 +38,25 @@ func keyValuePair(t *testing.T) ([]byte, []byte) { func TestKvs(t *testing.T) { - s := kvs.New(createLs(), mockStorer.DirectUpload(), swarm.ZeroAddress) + s := kvs.New(createLs(), swarm.ZeroAddress) key, val := keyValuePair(t) + ctx := context.Background() t.Run("Get non-existent key should return error", func(t *testing.T) { - _, err := s.Get([]byte{1}) + _, err := s.Get(ctx, []byte{1}) assert.Error(t, err) }) t.Run("Multiple Get with same key, no error", func(t *testing.T) { - err := s.Put(key, val) + err := s.Put(ctx, key, val) assert.NoError(t, err) // get #1 - v, err := s.Get(key) + v, err := s.Get(ctx, key) assert.NoError(t, err) assert.Equal(t, val, v) // get #2 - v, err = s.Get(key) + v, err = s.Get(ctx, key) assert.NoError(t, err) assert.Equal(t, val, v) }) @@ -105,9 +106,9 @@ func TestKvs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := s.Put(tc.key, tc.val) + err := s.Put(ctx, tc.key, tc.val) assert.NoError(t, err) - retVal, err := s.Get(tc.key) + retVal, err := s.Get(ctx, tc.key) assert.NoError(t, err) assert.Equal(t, tc.val, retVal) }) @@ -116,53 +117,53 @@ func TestKvs(t *testing.T) { } func TestKvs_Save(t *testing.T) { + ctx := context.Background() + key1, val1 := keyValuePair(t) key2, val2 := keyValuePair(t) t.Run("Save empty KVS return error", func(t *testing.T) { - s := kvs.New(createLs(), mockStorer.DirectUpload(), swarm.ZeroAddress) - _, err := s.Save() + s := kvs.New(createLs(), swarm.ZeroAddress) + _, err := s.Save(ctx) assert.Error(t, err) }) t.Run("Save not empty KVS return valid swarm address", func(t *testing.T) { - s := kvs.New(createLs(), mockStorer.DirectUpload(), swarm.ZeroAddress) - s.Put(key1, val1) - ref, err := s.Save() + s := kvs.New(createLs(), swarm.ZeroAddress) + s.Put(ctx, key1, val1) + ref, err := s.Save(ctx) assert.NoError(t, err) assert.True(t, ref.IsValidNonEmpty()) }) t.Run("Save KVS with one item, no error, pre-save value exist", func(t *testing.T) { ls := createLs() - putter := mockStorer.DirectUpload() - s1 := kvs.New(ls, putter, swarm.ZeroAddress) + s1 := kvs.New(ls, swarm.ZeroAddress) - err := s1.Put(key1, val1) + err := s1.Put(ctx, key1, val1) assert.NoError(t, err) - ref, err := s1.Save() + ref, err := s1.Save(ctx) assert.NoError(t, err) - s2 := kvs.New(ls, putter, ref) - val, err := s2.Get(key1) + s2 := kvs.New(ls, ref) + val, err := s2.Get(ctx, key1) assert.NoError(t, err) assert.Equal(t, val1, val) }) t.Run("Save KVS and add one item, no error, after-save value exist", func(t *testing.T) { ls := createLs() - putter := mockStorer.DirectUpload() - kvs1 := kvs.New(ls, putter, swarm.ZeroAddress) + kvs1 := kvs.New(ls, swarm.ZeroAddress) - err := kvs1.Put(key1, val1) + err := kvs1.Put(ctx, key1, val1) assert.NoError(t, err) - ref, err := kvs1.Save() + ref, err := kvs1.Save(ctx) assert.NoError(t, err) // New KVS - kvs2 := kvs.New(ls, putter, ref) - err = kvs2.Put(key2, val2) + kvs2 := kvs.New(ls, ref) + err = kvs2.Put(ctx, key2, val2) assert.NoError(t, err) - val, err := kvs2.Get(key2) + val, err := kvs2.Get(ctx, key2) assert.NoError(t, err) assert.Equal(t, val2, val) }) diff --git a/pkg/kvs/mock/kvs.go b/pkg/kvs/mock/kvs.go index 78282934bf2..0203cc52ce6 100644 --- a/pkg/kvs/mock/kvs.go +++ b/pkg/kvs/mock/kvs.go @@ -1,6 +1,7 @@ package mock import ( + "context" "encoding/hex" "sync" @@ -44,13 +45,13 @@ type mockKeyValueStore struct { var _ kvs.KeyValueStore = (*mockKeyValueStore)(nil) -func (m *mockKeyValueStore) Get(key []byte) ([]byte, error) { +func (m *mockKeyValueStore) Get(_ context.Context, key []byte) ([]byte, error) { mem := getMemory() val := mem[m.address.String()][hex.EncodeToString(key)] return val, nil } -func (m *mockKeyValueStore) Put(key []byte, value []byte) error { +func (m *mockKeyValueStore) Put(_ context.Context, key []byte, value []byte) error { mem := getMemory() if _, ok := mem[m.address.String()]; !ok { mem[m.address.String()] = make(map[string][]byte) @@ -59,7 +60,7 @@ func (m *mockKeyValueStore) Put(key []byte, value []byte) error { return nil } -func (m *mockKeyValueStore) Save() (swarm.Address, error) { +func (m *mockKeyValueStore) Save(ctx context.Context) (swarm.Address, error) { return m.address, nil } diff --git a/pkg/node/devnode.go b/pkg/node/devnode.go index b45715bd152..e838f93a287 100644 --- a/pkg/node/devnode.go +++ b/pkg/node/devnode.go @@ -22,6 +22,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/auth" "github.com/ethersphere/bee/v2/pkg/bzz" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" "github.com/ethersphere/bee/v2/pkg/feeds/factory" "github.com/ethersphere/bee/v2/pkg/log" mockP2P "github.com/ethersphere/bee/v2/pkg/p2p/mock" @@ -66,6 +67,7 @@ type DevBee struct { localstoreCloser io.Closer apiCloser io.Closer pssCloser io.Closer + dacCloser io.Closer errorLogWriter io.Writer apiServer *http.Server debugAPIServer *http.Server @@ -234,6 +236,15 @@ func NewDevBee(logger log.Logger, o *DevOptions) (b *DevBee, err error) { } b.localstoreCloser = localStore + session := dynamicaccess.NewDefaultSession(mockKey) + actLogic := dynamicaccess.NewLogic(session) + ctrl := dynamicaccess.NewController(context.Background(), actLogic, localStore.ChunkStore(), localStore.Cache()) + dac, err := dynamicaccess.NewService(ctrl) + if err != nil { + return nil, fmt.Errorf("dac service: %w", err) + } + b.dacCloser = dac + pssService := pss.New(mockKey, logger) b.pssCloser = pssService @@ -383,6 +394,7 @@ func NewDevBee(logger log.Logger, o *DevOptions) (b *DevBee, err error) { Pss: pssService, FeedFactory: mockFeeds, Post: post, + Dac: dac, PostageContract: postageContract, Staking: mockStaking, Steward: mockSteward, @@ -492,6 +504,7 @@ func (b *DevBee) Shutdown() error { } tryClose(b.pssCloser, "pss") + tryClose(b.dacCloser, "dac") tryClose(b.tracerCloser, "tracer") tryClose(b.stateStoreCloser, "statestore") tryClose(b.localstoreCloser, "localstore") diff --git a/pkg/node/node.go b/pkg/node/node.go index 2996b8d77d5..786c9abe864 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -30,6 +30,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/auth" "github.com/ethersphere/bee/v2/pkg/config" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/dynamicaccess" "github.com/ethersphere/bee/v2/pkg/feeds/factory" "github.com/ethersphere/bee/v2/pkg/hive" "github.com/ethersphere/bee/v2/pkg/log" @@ -117,6 +118,7 @@ type Bee struct { shutdownInProgress bool shutdownMutex sync.Mutex syncingStopped *syncutil.Signaler + dacCloser io.Closer } type Options struct { @@ -200,6 +202,7 @@ func NewBee( logger log.Logger, libp2pPrivateKey, pssPrivateKey *ecdsa.PrivateKey, + session dynamicaccess.Session, o *Options, ) (b *Bee, err error) { tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{ @@ -772,6 +775,14 @@ func NewBee( b.localstoreCloser = localStore evictFn = func(id []byte) error { return localStore.EvictBatch(context.Background(), id) } + actLogic := dynamicaccess.NewLogic(session) + ctrl := dynamicaccess.NewController(ctx, actLogic, localStore.ChunkStore(), localStore.Cache()) + dac, err := dynamicaccess.NewService(ctrl) + if err != nil { + return nil, fmt.Errorf("dac service: %w", err) + } + b.dacCloser = dac + var ( syncErr atomic.Value syncStatus atomic.Value @@ -1087,6 +1098,7 @@ func NewBee( Pss: pssService, FeedFactory: feedFactory, Post: post, + Dac: dac, PostageContract: postageStampContractService, Staking: stakingContract, Steward: steward, @@ -1334,6 +1346,7 @@ func (b *Bee) Shutdown() error { c() } + tryClose(b.dacCloser, "dac") tryClose(b.tracerCloser, "tracer") tryClose(b.topologyCloser, "topology driver") tryClose(b.storageIncetivesCloser, "storage incentives agent") diff --git a/pkg/soc/testing/soc.go b/pkg/soc/testing/soc.go index e5325f464b1..9516a721d83 100644 --- a/pkg/soc/testing/soc.go +++ b/pkg/soc/testing/soc.go @@ -5,6 +5,7 @@ package testing import ( + "crypto/ecdsa" "testing" "github.com/ethersphere/bee/v2/pkg/cac" @@ -70,3 +71,38 @@ func GenerateMockSOC(t *testing.T, data []byte) *MockSOC { WrappedChunk: ch, } } + +// GenerateMockSOC generates a valid mocked SOC from given data and key. +func GenerateMockSOCWithKey(t *testing.T, data []byte, privKey *ecdsa.PrivateKey) *MockSOC { + t.Helper() + + signer := crypto.NewDefaultSigner(privKey) + owner, err := signer.EthereumAddress() + if err != nil { + t.Fatal(err) + } + + ch, err := cac.New(data) + if err != nil { + t.Fatal(err) + } + + id := make([]byte, swarm.HashSize) + hasher := swarm.NewHasher() + _, err = hasher.Write(append(id, ch.Address().Bytes()...)) + if err != nil { + t.Fatal(err) + } + + signature, err := signer.Sign(hasher.Sum(nil)) + if err != nil { + t.Fatal(err) + } + + return &MockSOC{ + ID: id, + Owner: owner.Bytes(), + Signature: signature, + WrappedChunk: ch, + } +}