From e1add9e9442d65c5d3f0ac255f1dc0b484de3835 Mon Sep 17 00:00:00 2001 From: Attila Gazso <230163+agazso@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:46:31 +0100 Subject: [PATCH 1/2] feat: encrypted ACT --- pkg/api/accesscontrol.go | 14 ++++++++++++-- pkg/api/bzz.go | 22 ++++++++++++++++++---- pkg/api/chunk.go | 8 +++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 1ae0fb2fe6e..ae13fb39704 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -18,6 +18,8 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/ethersphere/bee/v2/pkg/accesscontrol" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/encryption" + encstore "github.com/ethersphere/bee/v2/pkg/encryption/store" "github.com/ethersphere/bee/v2/pkg/file/loadsave" "github.com/ethersphere/bee/v2/pkg/file/redundancy" "github.com/ethersphere/bee/v2/pkg/jsonhttp" @@ -126,7 +128,11 @@ func (s *Service) actDecryptionHandler() func(h http.Handler) http.Handler { cache = *headers.Cache } ctx := r.Context() - ls := loadsave.NewReadonly(s.storer.Download(cache), s.storer.Cache(), redundancy.DefaultLevel) + getter := s.storer.Download(cache) + if headers.HistoryAddress != nil && len(headers.HistoryAddress.Bytes()) == encryption.ReferenceSize { + getter = encstore.New(getter) + } + ls := loadsave.NewReadonly(getter, s.storer.Cache(), redundancy.DefaultLevel) reference, err := s.accesscontrol.DownloadHandler(ctx, ls, paths.Address, headers.Publisher, *headers.HistoryAddress, timestamp) if err != nil { logger.Debug("access control download failed", "error", err) @@ -204,7 +210,11 @@ func (s *Service) actListGranteesHandler(w http.ResponseWriter, r *http.Request) cache = *headers.Cache } publisher := &s.publicKey - ls := loadsave.NewReadonly(s.storer.Download(cache), s.storer.Cache(), redundancy.DefaultLevel) + getter := s.storer.Download(cache) + if len(paths.GranteesAddress.Bytes()) == encryption.ReferenceSize { + getter = encstore.New(getter) + } + ls := loadsave.NewReadonly(getter, s.storer.Cache(), redundancy.DefaultLevel) grantees, err := s.accesscontrol.Get(r.Context(), ls, publisher, paths.GranteesAddress) if err != nil { logger.Debug("could not get grantees", "error", err) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index d99594bc682..d15b93af6b1 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -22,6 +22,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/accesscontrol" + "github.com/ethersphere/bee/v2/pkg/encryption" + encstore "github.com/ethersphere/bee/v2/pkg/encryption/store" "github.com/ethersphere/bee/v2/pkg/feeds" "github.com/ethersphere/bee/v2/pkg/file" "github.com/ethersphere/bee/v2/pkg/file/joiner" @@ -499,7 +501,11 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV } ctx := r.Context() - ls := loadsave.NewReadonly(s.storer.Download(cache), s.storer.Cache(), redundancy.DefaultLevel) + downloadGetter := s.storer.Download(cache) + if len(address.Bytes()) == encryption.ReferenceSize { + downloadGetter = encstore.New(downloadGetter) + } + ls := loadsave.NewReadonly(downloadGetter, s.storer.Cache(), redundancy.DefaultLevel) feedDereferenced := false ctx, err := getter.SetConfigInContext(ctx, headers.Strategy, headers.FallbackMode, headers.ChunkRetrievalTimeout, logger) @@ -564,7 +570,11 @@ FETCH: address = wc.Address() // modify ls and init with non-existing wrapped chunk - ls = loadsave.NewReadonlyWithRootCh(s.storer.Download(cache), s.storer.Cache(), wc, rLevel) + downloadGetter = s.storer.Download(cache) + if len(address.Bytes()) == encryption.ReferenceSize { + downloadGetter = encstore.New(downloadGetter) + } + ls = loadsave.NewReadonlyWithRootCh(downloadGetter, s.storer.Cache(), wc, rLevel) feedDereferenced = true curBytes, err := cur.MarshalBinary() if err != nil { @@ -725,10 +735,14 @@ func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *h reader file.Joiner l int64 ) + getter := s.storer.Download(cache) + if len(reference.Bytes()) == encryption.ReferenceSize { + getter = encstore.New(getter) + } if rootCh != nil { - reader, l, err = joiner.NewJoiner(ctx, s.storer.Download(cache), s.storer.Cache(), reference, rootCh) + reader, l, err = joiner.NewJoiner(ctx, getter, s.storer.Cache(), reference, rootCh) } else { - reader, l, err = joiner.New(ctx, s.storer.Download(cache), s.storer.Cache(), reference, rLevel) + reader, l, err = joiner.New(ctx, getter, s.storer.Cache(), reference, rLevel) } if err != nil { if errors.Is(err, storage.ErrNotFound) || errors.Is(err, topology.ErrNotFound) { diff --git a/pkg/api/chunk.go b/pkg/api/chunk.go index a9ffcad5ae0..1d6805adede 100644 --- a/pkg/api/chunk.go +++ b/pkg/api/chunk.go @@ -14,6 +14,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/accesscontrol" "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/encryption" + encstore "github.com/ethersphere/bee/v2/pkg/encryption/store" "github.com/ethersphere/bee/v2/pkg/soc" "github.com/ethersphere/bee/v2/pkg/storer" @@ -254,7 +256,11 @@ func (s *Service) chunkGetHandler(w http.ResponseWriter, r *http.Request) { address = v } - chunk, err := s.storer.Download(cache).Get(r.Context(), address) + getter := s.storer.Download(cache) + if len(address.Bytes()) == encryption.ReferenceSize { + getter = encstore.New(getter) + } + chunk, err := getter.Get(r.Context(), address) if err != nil { if errors.Is(err, storage.ErrNotFound) { loggerV1.Debug("chunk not found", "address", address) From 8556fc21a8e5c7a2593ae4c85764ce29634e7cd7 Mon Sep 17 00:00:00 2001 From: Attila Gazso <230163+agazso@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:02:59 +0100 Subject: [PATCH 2/2] test: add test for encrypted act --- pkg/api/accesscontrol_test.go | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/pkg/api/accesscontrol_test.go b/pkg/api/accesscontrol_test.go index 91dcfcc6efb..4b7fbd41a21 100644 --- a/pkg/api/accesscontrol_test.go +++ b/pkg/api/accesscontrol_test.go @@ -18,13 +18,16 @@ import ( "github.com/ethersphere/bee/v2/pkg/accesscontrol" mockac "github.com/ethersphere/bee/v2/pkg/accesscontrol/mock" + "github.com/ethersphere/bee/v2/pkg/accesscontrol/kvs" "github.com/ethersphere/bee/v2/pkg/api" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/encryption" "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" + "github.com/ethersphere/bee/v2/pkg/manifest" 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" @@ -561,6 +564,109 @@ func TestAccessLogicHistory(t *testing.T) { }) } +// nolint:paralleltest,tparallel +// TestAccessLogicHistoryEncrypted ensures /bzz ACT download works with encrypted +// history + ACT manifests (64-byte references). +func TestAccessLogicHistoryEncrypted(t *testing.T) { + t.Parallel() + + var ( + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) + publisher = hex.EncodeToString(publicKeyBytes) + logger = log.Noop + storerMock = mockstorer.New() + ctx = context.Background() + ) + + lsEncrypted := loadsave.New( + storerMock.ChunkStore(), + storerMock.Cache(), + pipelineFactory(storerMock.Cache(), true, redundancy.NONE), + redundancy.DefaultLevel, + ) + + // 1) Store encrypted content + payload := []byte("encrypted-act-history-test") + contentRefBytes, err := lsEncrypted.Save(ctx, payload) + if err != nil { + t.Fatalf("save encrypted content: %v", err) + } + contentRef := swarm.NewAddress(contentRefBytes) + + // 2) Create encrypted mantaray manifest pointing to encrypted content + m, err := manifest.NewMantarayManifest(lsEncrypted, true) + if err != nil { + t.Fatalf("new mantaray manifest: %v", err) + } + zeroRef := swarm.NewAddress(make([]byte, encryption.ReferenceSize)) + if err := m.Add(ctx, manifest.RootPath, manifest.NewEntry(zeroRef, map[string]string{ + manifest.WebsiteIndexDocumentSuffixKey: "index.html", + })); err != nil { + t.Fatalf("add root metadata: %v", err) + } + if err := m.Add(ctx, "index.html", manifest.NewEntry(contentRef, map[string]string{ + manifest.EntryMetadataContentTypeKey: "text/plain; charset=utf-8", + manifest.EntryMetadataFilenameKey: "index.html", + })); err != nil { + t.Fatalf("add index entry: %v", err) + } + manifestRef, err := m.Store(ctx) + if err != nil { + t.Fatalf("store manifest: %v", err) + } + + // 3) Build encrypted ACT + history + session := accesscontrol.NewDefaultSession(pk) + al := accesscontrol.NewLogic(session) + actStore, err := kvs.New(lsEncrypted) + if err != nil { + t.Fatalf("new act kvs: %v", err) + } + if err := al.AddGrantee(ctx, actStore, &pk.PublicKey, &pk.PublicKey); err != nil { + t.Fatalf("add publisher grantee: %v", err) + } + actRef, err := actStore.Save(ctx) + if err != nil { + t.Fatalf("save act kvs: %v", err) + } + + history, err := accesscontrol.NewHistory(lsEncrypted) + if err != nil { + t.Fatalf("new history: %v", err) + } + ts := time.Now().Unix() + if err := history.Add(ctx, actRef, &ts, nil); err != nil { + t.Fatalf("history add: %v", err) + } + historyRef, err := history.Store(ctx) + if err != nil { + t.Fatalf("history store: %v", err) + } + + encryptedRef, err := al.EncryptRef(ctx, actStore, &pk.PublicKey, manifestRef) + if err != nil { + t.Fatalf("encrypt ref: %v", err) + } + + client, _, _, _ := newTestServer(t, testServerOptions{ + Storer: storerMock, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + PublicKey: pk.PublicKey, + AccessControl: accesscontrol.NewController(al), + }) + + jsonhttptest.Request(t, client, http.MethodGet, "/bzz/"+encryptedRef.String()+"/", http.StatusOK, + jsonhttptest.WithRequestHeader(api.SwarmActTimestampHeader, strconv.FormatInt(ts, 10)), + jsonhttptest.WithRequestHeader(api.SwarmActHistoryAddressHeader, historyRef.String()), + jsonhttptest.WithRequestHeader(api.SwarmActPublisherHeader, publisher), + jsonhttptest.WithExpectedResponse(payload), + jsonhttptest.WithExpectedContentLength(len(payload)), + ) +} + // nolint:paralleltest,tparallel // TestAccessLogicTimestamp // [positive test] 1.: uploading a file w/ ACT then download it w/ timestamp and check the data.