Repository: kopia/kopia
Pull request #2639 (2022-12-07 23:04:39)

build(deps): bump go.opentelemetry.io/otel from 1.11.1 to 1.11.2
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.11.1 to 1.11.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel's changelog</a>.</em></p>
<blockquote>
<h2>[1.11.2/0.34.0] 2022-12-05</h2>
<h3>Added</h3>
<ul>
<li>The <code>WithView</code> <code>Option</code> is added to the <code>go.opentelemetry.io/otel/sdk/metric</code> package.
This option is used to configure the view(s) a <code>MeterProvider</code> will use for all <code>Reader</code>s that are registered with it. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3387">#3387</a>)</li>
<li>Add Instrumentation Scope and Version as info metric and label in Prometheus exporter.
This can be disabled using the <code>WithoutScopeInfo()</code> option added to that package.(<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3273">#3273</a>, <a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3357">#3357</a>)</li>
<li>OTLP exporters now recognize: (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3363">#3363</a>)
<ul>
<li><code>OTEL_EXPORTER_OTLP_INSECURE</code></li>
<li><code>OTEL_EXPORTER_OTLP_TRACES_INSECURE</code></li>
<li><code>OTEL_EXPORTER_OTLP_METRICS_INSECURE</code></li>
<li><code>OTEL_EXPORTER_OTLP_CLIENT_KEY</code></li>
<li><code>OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY</code></li>
<li><code>OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY</code></li>
<li><code>OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE</code></li>
<li><code>OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE</code></li>
<li><code>OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE</code></li>
</ul>
</li>
<li>The <code>View</code> type and related <code>NewView</code> function to create a view according to the OpenTelemetry specification are added to <code>go.opentelemetry.io/otel/sdk/metric</code>.
These additions are replacements for the <code>View</code> type and <code>New</code> function from <code>go.opentelemetry.io/otel/sdk/metric/view</code>. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3459">#3459</a>)</li>
<li>The <code>Instrument</code> and <code>InstrumentKind</code> type are added to <code>go.opentelemetry.io/otel/sdk/metric</code>.
These additions are replacements for the <code>Instrument</code> and <code>InstrumentKind</code> types from <code>go.opentelemetry.io/otel/sdk/metric/view</code>. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3459">#3459</a>)</li>
<li>The <code>Stream</code> type is added to <code>go.opentelemetry.io/otel/sdk/metric</code> to define a metric data stream a view will produce. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3459">#3459</a>)</li>
<li>The <code>AssertHasAttributes</code> allows instrument authors to test that datapoints returned have appropriate attributes. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3487">#3487</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>The <code>&quot;go.opentelemetry.io/otel/sdk/metric&quot;.WithReader</code> option no longer accepts views to associate with the <code>Reader</code>.
Instead, views are now registered directly with the <code>MeterProvider</code> via the new <code>WithView</code> option.
The views registered with the <code>MeterProvider</code> apply to all <code>Reader</code>s. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3387">#3387</a>)</li>
<li>The <code>Temporality(view.InstrumentKind) metricdata.Temporality</code> and <code>Aggregation(view.InstrumentKind) aggregation.Aggregation</code> methods are added to the <code>&quot;go.opentelemetry.io/otel/sdk/metric&quot;.Exporter</code> interface. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3260">#3260</a>)</li>
<li>The <code>Temporality(view.InstrumentKind) metricdata.Temporality</code> and <code>Aggregation(view.InstrumentKind) aggregation.Aggregation</code> methods are added to the <code>&quot;go.opentelemetry.io/otel/exporters/otlp/otlpmetric&quot;.Client</code> interface. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3260">#3260</a>)</li>
<li>The <code>WithTemporalitySelector</code> and <code>WithAggregationSelector</code> <code>ReaderOption</code>s have been changed to <code>ManualReaderOption</code>s in the <code>go.opentelemetry.io/otel/sdk/metric</code> package. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3260">#3260</a>)</li>
<li>The periodic reader in the <code>go.opentelemetry.io/otel/sdk/metric</code> package now uses the temporality and aggregation selectors from its configured exporter instead of accepting them as options. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3260">#3260</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>The <code>go.opentelemetry.io/otel/exporters/prometheus</code> exporter fixes duplicated <code>_total</code> suffixes. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3369">#3369</a>)</li>
<li>Remove comparable requirement for <code>Reader</code>s. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3387">#3387</a>)</li>
<li>Cumulative metrics from the OpenCensus bridge (<code>go.opentelemetry.io/otel/bridge/opencensus</code>) are defined as monotonic sums, instead of non-monotonic. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3389">#3389</a>)</li>
<li>Asynchronous counters (<code>Counter</code> and <code>UpDownCounter</code>) from the metric SDK now produce delta sums when configured with delta temporality. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3398">#3398</a>)</li>
<li>Exported <code>Status</code> codes in the <code>go.opentelemetry.io/otel/exporters/zipkin</code> exporter are now exported as all upper case values. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3340">#3340</a>)</li>
<li><code>Aggregation</code>s from <code>go.opentelemetry.io/otel/sdk/metric</code> with no data are not exported. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3394">#3394</a>, <a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3436">#3436</a>)</li>
<li>Reenabled Attribute Filters in the Metric SDK. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3396">#3396</a>)</li>
<li>Asynchronous callbacks are only called if they are registered with at least one instrument that does not use drop aggragation. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3408">#3408</a>)</li>
<li>Do not report empty partial-success responses in the <code>go.opentelemetry.io/otel/exporters/otlp</code> exporters. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3438">#3438</a>, <a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3432">#3432</a>)</li>
<li>Handle partial success responses in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric</code> exporters. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3162">#3162</a>, <a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3440">#3440</a>)</li>
<li>Prevent duplicate Prometheus description, unit, and type. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3469">#3469</a>)</li>
<li>Prevents panic when using incorrect <code>attribute.Value.As[Type]Slice()</code>. (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3489">#3489</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/bc5cf7eb26a455be6d5b359dea0b6592c4176412"><code>bc5cf7e</code></a> Release v1.11.2/v0.34.0 (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3512">#3512</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/22434311ae4783010a04081634ec503d9cbb5e33"><code>2243431</code></a> Signed-off-by: Ziqi Zhao &lt;<a href="mailto:zhaoziqi9146@gmail.com">zhaoziqi9146@gmail.com</a>&gt; (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3469">#3469</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/e97704c1ec872144da633a29b27feaef06fb2867"><code>e97704c</code></a> Fix a typo in the manual reader (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3509">#3509</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/a436ae76f70af9a79884a8f95779517af8f0b89b"><code>a436ae7</code></a> Bump github.com/Masterminds/semver/v3 from 3.1.1 to 3.2.0 in /schema (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3511">#3511</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/69ad652afb0f9ec181338258123925b17a15ac17"><code>69ad652</code></a> change jaeger tag version in otel-collector (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3495">#3495</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/291aaa022158abff8f6381d4a148190f3547c9b2"><code>291aaa0</code></a> Redefine gauge in Prometheus example to be a Gauge (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3498">#3498</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/289a612e6a40a7575cd13dbc02ae07f6269e5491"><code>289a612</code></a> Adds an Attribute assertion to metric data test (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3487">#3487</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/aa868d506b7e4de2f534f47b168b27b7f8824351"><code>aa868d5</code></a> Clarify HTTP/gRPC exporter examples (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3492">#3492</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d7b31155b5cc03cdadb39e894f11f9b4f2616fed"><code>d7b3115</code></a> Make the AsType functions not panic (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3489">#3489</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1f5e6adbf2ca8fdb394ea07b3370506889a10bfa"><code>1f5e6ad</code></a> Update the usage of set-output command in GH actions (<a href="https://github-redirect.dependabot.com/open-telemetry/opentelemetry-go/issues/3485">#3485</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.11.1...v1.11.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.opentelemetry.io/otel&package-manager=go_modules&previous-version=1.11.1&new-version=1.11.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once CI passes on it, as requested by @kopia-builder.

[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/README.md b/README.md
index 5c54142c..99231dbd 100644
--- a/README.md
+++ b/README.md
@@ -32,9 +32,9 @@ Kopia supports saving your [encrypted](https://kopia.io/docs/features/#end-to-en
 * **Google Cloud Storage**
 * Any remote server or cloud storage that supports **WebDAV**
 * Any remote server or cloud storage that supports **SFTP**
-* Some of the cloud storages supported by **Rclone**
+* Some of the cloud storage options supported by **Rclone**
   * Requires you to download and setup Rclone in addition to Kopia, but after that Kopia manages/runs Rclone for you
-  * Rclone support is experimental: not all the cloud storages supported by Rclone have been tested to work with Kopia, and some may not work with Kopia; Kopia has been tested to work with **Dropbox**, **OneDrive**, and **Google Drive** through Rclone
+  * Rclone support is experimental: not all the cloud storage products supported by Rclone have been tested to work with Kopia, and some may not work with Kopia; Kopia has been tested to work with **Dropbox**, **OneDrive**, and **Google Drive** through Rclone
 * Your local machine and any network-attached storage or server
 * Your own server by setting up a [Kopia Repository Server](https://kopia.io/docs/repository-server/)
 
diff --git a/cli/command_blob_list.go b/cli/command_blob_list.go
index 2b2fc6d1..64f54009 100644
--- a/cli/command_blob_list.go
+++ b/cli/command_blob_list.go
@@ -5,9 +5,10 @@
 	"strings"
 
 	"github.com/kopia/kopia/internal/epoch"
+	"github.com/kopia/kopia/internal/repolog"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 type commandBlobList struct {
@@ -57,7 +58,7 @@ func (c *commandBlobList) run(ctx context.Context, rep repo.DirectRepository) er
 
 func (c *commandBlobList) shouldInclude(b blob.Metadata) bool {
 	if c.dataOnly {
-		if strings.HasPrefix(string(b.BlobID), content.LegacyIndexBlobPrefix) {
+		if strings.HasPrefix(string(b.BlobID), indexblob.V0IndexBlobPrefix) {
 			return false
 		}
 
@@ -65,7 +66,7 @@ func (c *commandBlobList) shouldInclude(b blob.Metadata) bool {
 			return false
 		}
 
-		if strings.HasPrefix(string(b.BlobID), content.TextLogBlobPrefix) {
+		if strings.HasPrefix(string(b.BlobID), repolog.BlobPrefix) {
 			return false
 		}
 
diff --git a/cli/command_blob_show.go b/cli/command_blob_show.go
index e26c9a2f..7a664348 100644
--- a/cli/command_blob_show.go
+++ b/cli/command_blob_show.go
@@ -8,11 +8,11 @@
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/blobcrypto"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/iocopy"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/content"
 )
 
 type commandBlobShow struct {
@@ -57,7 +57,7 @@ func (c *commandBlobShow) maybeDecryptBlob(ctx context.Context, w io.Writer, rep
 		var tmp gather.WriteBuffer
 		defer tmp.Close()
 
-		if err := content.DecryptBLOB(rep.ContentReader().ContentFormat(), b, blobID, &tmp); err != nil {
+		if err := blobcrypto.Decrypt(rep.ContentReader().ContentFormat(), b, blobID, &tmp); err != nil {
 			return errors.Wrap(err, "error decrypting blob")
 		}
 
diff --git a/cli/command_index_inspect.go b/cli/command_index_inspect.go
index d878b827..f2ba3178 100644
--- a/cli/command_index_inspect.go
+++ b/cli/command_index_inspect.go
@@ -11,6 +11,7 @@
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 type commandIndexInspect struct {
@@ -81,7 +82,7 @@ func (c *commandIndexInspect) inspectAllBlobs(ctx context.Context, rep repo.Dire
 		return errors.Wrap(err, "error listing index blobs")
 	}
 
-	indexesCh := make(chan content.IndexBlobInfo, len(indexes))
+	indexesCh := make(chan indexblob.Metadata, len(indexes))
 	for _, bm := range indexes {
 		indexesCh <- bm
 	}
@@ -162,7 +163,7 @@ func (c *commandIndexInspect) inspectSingleIndexBlob(ctx context.Context, rep re
 		return errors.Wrapf(err, "unable to get data for %v", blobID)
 	}
 
-	entries, err := content.ParseIndexBlob(ctx, blobID, data.Bytes(), rep.ContentReader().ContentFormat())
+	entries, err := content.ParseIndexBlob(blobID, data.Bytes(), rep.ContentReader().ContentFormat())
 	if err != nil {
 		return errors.Wrapf(err, "unable to recover index from %v", blobID)
 	}
diff --git a/cli/command_index_optimize.go b/cli/command_index_optimize.go
index b65b761d..04e22259 100644
--- a/cli/command_index_optimize.go
+++ b/cli/command_index_optimize.go
@@ -5,7 +5,7 @@
 	"time"
 
 	"github.com/kopia/kopia/repo"
-	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 type commandIndexOptimize struct {
@@ -36,7 +36,7 @@ func (c *commandIndexOptimize) runOptimizeCommand(ctx context.Context, rep repo.
 		return err
 	}
 
-	opt := content.CompactOptions{
+	opt := indexblob.CompactOptions{
 		MaxSmallBlobs: c.optimizeMaxSmallBlobs,
 		AllIndexes:    c.optimizeAllIndexes,
 		DropContents:  contentIDs,
diff --git a/cli/command_index_recover.go b/cli/command_index_recover.go
index 43fabc65..a145f92f 100644
--- a/cli/command_index_recover.go
+++ b/cli/command_index_recover.go
@@ -12,6 +12,7 @@
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 type commandIndexRecover struct {
@@ -60,7 +61,7 @@ func (c *commandIndexRecover) run(ctx context.Context, rep repo.DirectRepository
 	}()
 
 	if c.deleteIndexes {
-		if err := rep.BlobReader().ListBlobs(ctx, content.LegacyIndexBlobPrefix, func(bm blob.Metadata) error {
+		if err := rep.BlobReader().ListBlobs(ctx, indexblob.V0IndexBlobPrefix, func(bm blob.Metadata) error {
 			if c.commit {
 				log(ctx).Infof("deleting old index blob: %v", bm.BlobID)
 				return errors.Wrap(rep.BlobStorage().DeleteBlob(ctx, bm.BlobID), "error deleting index blob")
diff --git a/cli/command_logs_session.go b/cli/command_logs_session.go
index 66c00ddd..a656e05b 100644
--- a/cli/command_logs_session.go
+++ b/cli/command_logs_session.go
@@ -11,8 +11,8 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/internal/repolog"
 	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/content"
 )
 
 type logSessionInfo struct {
@@ -70,7 +70,7 @@ func getLogSessions(ctx context.Context, st blob.Reader) ([]*logSessionInfo, err
 
 	var allSessions []*logSessionInfo
 
-	if err := st.ListBlobs(ctx, content.TextLogBlobPrefix, func(bm blob.Metadata) error {
+	if err := st.ListBlobs(ctx, repolog.BlobPrefix, func(bm blob.Metadata) error {
 		parts := strings.Split(string(bm.BlobID), "_")
 
 		//nolint:gomnd
diff --git a/cli/command_logs_show.go b/cli/command_logs_show.go
index 57195efa..278e55da 100644
--- a/cli/command_logs_show.go
+++ b/cli/command_logs_show.go
@@ -5,9 +5,9 @@
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/blobcrypto"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/repo"
-	"github.com/kopia/kopia/repo/content"
 )
 
 type commandLogsShow struct {
@@ -70,7 +70,7 @@ func (c *commandLogsShow) run(ctx context.Context, rep repo.DirectRepository) er
 				return errors.Wrap(err, "error getting log")
 			}
 
-			if err := content.DecryptBLOB(rep.ContentReader().ContentFormat(), data.Bytes(), bm.BlobID, &decrypted); err != nil {
+			if err := blobcrypto.Decrypt(rep.ContentReader().ContentFormat(), data.Bytes(), bm.BlobID, &decrypted); err != nil {
 				return errors.Wrap(err, "error decrypting log")
 			}
 
diff --git a/cli/command_repository_set_parameters.go b/cli/command_repository_set_parameters.go
index 30cc0df0..bfa161c6 100644
--- a/cli/command_repository_set_parameters.go
+++ b/cli/command_repository_set_parameters.go
@@ -11,7 +11,6 @@
 	"github.com/kopia/kopia/internal/units"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/format"
 )
 
@@ -143,7 +142,7 @@ func updateRepositoryParameters(
 	}
 
 	if upgradeToEpochManager {
-		if err := content.WriteLegacyIndexPoisonBlob(ctx, rep.BlobStorage()); err != nil {
+		if err := format.WriteLegacyIndexPoisonBlob(ctx, rep.BlobStorage()); err != nil {
 			log(ctx).Errorf("unable to write legacy index poison blob: %v", err)
 		}
 	}
diff --git a/cli/command_repository_upgrade.go b/cli/command_repository_upgrade.go
index 1e9235f8..1240bf9c 100644
--- a/cli/command_repository_upgrade.go
+++ b/cli/command_repository_upgrade.go
@@ -10,16 +10,20 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/epoch"
+	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 )
 
 type commandRepositoryUpgrade struct {
-	forceRollback bool
-	skip          bool
-	force         bool
-	lockOnly      bool
+	forceRollback             bool
+	skip                      bool
+	allowUnsafeUpgradeTimings bool
+	commitMode                string
+	lockOnly                  bool
 
 	// lock settings
 	ioDrainTimeout         time.Duration
@@ -38,6 +42,11 @@ type commandRepositoryUpgrade struct {
 	maxPermittedClockDriftDefault = 5 * time.Minute
 )
 
+const (
+	commitModeAlwaysCommit = "always"
+	commitModeNeverCommit  = "never"
+)
+
 func (c *commandRepositoryUpgrade) setup(svc advancedAppServices, parent commandParent) {
 	// override the parent, the upgrade sub-command becomes the new parent here-onwards
 	parent = parent.Command("upgrade", fmt.Sprintf("Upgrade repository format.\n\n%s", warningColor.Sprint(experimentalWarning))).Hidden().
@@ -50,10 +59,11 @@ func (c *commandRepositoryUpgrade) setup(svc advancedAppServices, parent command
 
 	beginCmd := parent.Command("begin", "Begin upgrade.").Default()
 	beginCmd.Flag("io-drain-timeout", "Max time it should take all other Kopia clients to drop repository connections").Default(format.DefaultRepositoryBlobCacheDuration.String()).DurationVar(&c.ioDrainTimeout)
-	beginCmd.Flag("allow-unsafe-upgrade", "Force using an unsafe io-drain-timeout for the upgrade lock").Default("false").Hidden().BoolVar(&c.force)
+	beginCmd.Flag("allow-unsafe-upgrade", "Force using an unsafe io-drain-timeout for the upgrade lock").Default("false").Hidden().BoolVar(&c.allowUnsafeUpgradeTimings)
 	beginCmd.Flag("status-poll-interval", "An advisory polling interval to check for the status of upgrade").Default("60s").DurationVar(&c.statusPollInterval)
 	beginCmd.Flag("max-permitted-clock-drift", "The maximum drift between repository and client clocks").Default(maxPermittedClockDriftDefault.String()).DurationVar(&c.maxPermittedClockDrift)
 	beginCmd.Flag("lock-only", "Advertise the upgrade lock and exit without actually performing the drain or upgrade").Default("false").Hidden().BoolVar(&c.lockOnly) // this is used by tests
+	beginCmd.Flag("commit-mode", "Change behavior of commit. When not set, commit on validation success. 'always': always commit. 'never': always exit before commit.").Hidden().EnumVar(&c.commitMode, commitModeAlwaysCommit, commitModeNeverCommit)
 
 	// upgrade phases
 
@@ -63,6 +73,8 @@ func (c *commandRepositoryUpgrade) setup(svc advancedAppServices, parent command
 	beginCmd.Action(svc.directRepositoryWriteAction(c.runPhase(c.drainOrCommit)))
 	// If the lock is fully established then perform the upgrade.
 	beginCmd.Action(svc.directRepositoryWriteAction(c.runPhase(c.upgrade)))
+	// Validate index upgrade success
+	beginCmd.Action(svc.directRepositoryWriteAction(c.runPhase(c.ignoreErrorOnAlwaysCommit(c.validateAction))))
 	// Commit the upgrade and revoke the lock, this will also cleanup any
 	// backups used for rollback.
 	beginCmd.Action(svc.directRepositoryWriteAction(c.runPhase(c.commitUpgrade)))
@@ -72,9 +84,151 @@ func (c *commandRepositoryUpgrade) setup(svc advancedAppServices, parent command
 
 	rollbackCmd.Action(svc.directRepositoryWriteAction(c.forceRollbackAction))
 
+	validateCmd := parent.Command("validate", "Validate the upgraded indexes.")
+
+	validateCmd.Action(svc.directRepositoryWriteAction(c.validateAction))
+
 	c.svc = svc
 }
 
+// assign store the info struct in a map that can be used to compare indexes.
+func assign(iif content.Info, i int, m map[content.ID][2]index.Info) {
+	v := m[iif.GetContentID()]
+	v[i] = iif
+	m[iif.GetContentID()] = v
+}
+
+// loadIndexBlobs load index blobs into indexEntries map.  indexEntries map will allow comparison betweel two indexes (index at which == 0 and index at which == 1).
+func loadIndexBlobs(ctx context.Context, indexEntries map[content.ID][2]index.Info, sm *content.SharedManager, which int, indexBlobInfos []indexblob.Metadata) error {
+	d := gather.WriteBuffer{}
+
+	for _, indexBlobInfo := range indexBlobInfos {
+		blobID := indexBlobInfo.BlobID
+
+		indexInfos, err := sm.LoadIndexBlob(ctx, blobID, &d)
+		if err != nil {
+			return errors.Wrapf(err, "failed to load index blob with BlobID %s", blobID)
+		}
+
+		for _, indexInfo := range indexInfos {
+			assign(indexInfo, which, indexEntries)
+		}
+	}
+
+	return nil
+}
+
+// validateAction returns an error if the new V1 index blob content does not match the source V0 index blob content.
+// This is used to check that the upgraded index (V1 index) reflects the content of the old V0 index.
+func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo.DirectRepositoryWriter) error {
+	indexEntries := map[content.ID][2]index.Info{}
+
+	sm := rep.ContentManager().SharedManager
+
+	indexBlobInfos0, _, err := sm.IndexReaderV0().ListIndexBlobInfos(ctx)
+	if err != nil {
+		return errors.Wrapf(err, "failed to list index blobs for old index")
+	}
+
+	indexBlobInfos1, _, err := sm.IndexReaderV1().ListIndexBlobInfos(ctx)
+	if err != nil {
+		log(ctx).Errorf("failed to list index blobs for new index. upgrade may have failed.: %v", err)
+		return nil
+	}
+
+	if len(indexBlobInfos0) == 0 && len(indexBlobInfos1) > 0 {
+		log(ctx).Infof("old index is empty (possibly due to upgrade), nothing to compare against")
+		return nil
+	}
+
+	// load index blobs into their appropriate positions inside the indexEntries map
+
+	err = loadIndexBlobs(ctx, indexEntries, sm, 0, indexBlobInfos0)
+	if err != nil {
+		return errors.Wrapf(err, "failed to load index entries for v0 index entry")
+	}
+
+	err = loadIndexBlobs(ctx, indexEntries, sm, 1, indexBlobInfos1)
+	if err != nil {
+		return errors.Wrapf(err, "failed to load index entries for new index")
+	}
+
+	var msgs []string // a place to keep messages from the index comparison process
+
+	// both indexes will have matching contentiDs with matching indexInfo sructures.
+	for contentID, indexEntryPairs := range indexEntries {
+		iep0 := indexEntryPairs[0] // first entry of index entry pair
+		iep1 := indexEntryPairs[1] // second entry of index entry pair
+
+		// check that both the new and old indexes have entries for the same content
+		if iep0 != nil && iep1 != nil {
+			// this is the happy-path, check the entries.  any problems found will be added to msgs
+			msgs = append(msgs, CheckIndexInfo(iep0, iep1)...)
+			continue
+		}
+
+		// one of iep0 or iep1 are nil .. find out which one and add an appropriate message.
+		if iep0 != nil {
+			msgs = append(msgs, fmt.Sprintf("lop-sided index entries for contentID %q at blob %q", contentID, iep0.GetPackBlobID()))
+			continue
+		}
+
+		msgs = append(msgs, fmt.Sprintf("lop-sided index entries for contentID %q at blob %q", contentID, iep1.GetPackBlobID()))
+	}
+
+	// no msgs means the check passed without finding anything wrong
+	if len(msgs) == 0 {
+		log(ctx).Infof("index validation succeeded")
+		return nil
+	}
+
+	// otherwise there's a problem somewhere ... log the problems
+	log(ctx).Error("inconsistencies found in migrated index:")
+
+	for _, m := range msgs {
+		log(ctx).Error(m)
+	}
+
+	// and return an error that states something's wrong.
+	return errors.Wrap(err, "repository will remain locked until index differences are resolved")
+}
+
+// CheckIndexInfo compare two index infos.  If a mismatch exists, return an error with diagnostic information.
+func CheckIndexInfo(i0, i1 index.Info) []string {
+	var q []string
+
+	switch {
+	case i0.GetFormatVersion() != i1.GetFormatVersion():
+		q = append(q, fmt.Sprintf("mismatched FormatVersions: %v %v", i0.GetFormatVersion(), i1.GetFormatVersion()))
+	case i0.GetOriginalLength() != i1.GetOriginalLength():
+		q = append(q, fmt.Sprintf("mismatched OriginalLengths: %v %v", i0.GetOriginalLength(), i1.GetOriginalLength()))
+	case i0.GetPackBlobID() != i1.GetPackBlobID():
+		q = append(q, fmt.Sprintf("mismatched PackBlobIDs: %v %v", i0.GetPackBlobID(), i1.GetPackBlobID()))
+	case i0.GetPackedLength() != i1.GetPackedLength():
+		q = append(q, fmt.Sprintf("mismatched PackedLengths: %v %v", i0.GetPackedLength(), i1.GetPackedLength()))
+	case i0.GetPackOffset() != i1.GetPackOffset():
+		q = append(q, fmt.Sprintf("mismatched PackOffsets: %v %v", i0.GetPackOffset(), i1.GetPackOffset()))
+	case i0.GetEncryptionKeyID() != i1.GetEncryptionKeyID():
+		q = append(q, fmt.Sprintf("mismatched EncryptionKeyIDs: %v %v", i0.GetEncryptionKeyID(), i1.GetEncryptionKeyID()))
+	case i0.GetCompressionHeaderID() != i1.GetCompressionHeaderID():
+		q = append(q, fmt.Sprintf("mismatched GetCompressionHeaderID: %v %v", i0.GetCompressionHeaderID(), i1.GetCompressionHeaderID()))
+	case i0.GetDeleted() != i1.GetDeleted():
+		q = append(q, fmt.Sprintf("mismatched Deleted flags: %v %v", i0.GetDeleted(), i1.GetDeleted()))
+	case i0.GetTimestampSeconds() != i1.GetTimestampSeconds():
+		q = append(q, fmt.Sprintf("mismatched TimestampSeconds: %v %v", i0.GetTimestampSeconds(), i1.GetTimestampSeconds()))
+	}
+
+	if len(q) == 0 {
+		return nil
+	}
+
+	for i := range q {
+		q[i] = fmt.Sprintf("index blobs do not match: %q, %q: %s", i0.GetPackBlobID(), i1.GetPackBlobID(), q[i])
+	}
+
+	return q
+}
+
 func (c *commandRepositoryUpgrade) forceRollbackAction(ctx context.Context, rep repo.DirectRepositoryWriter) error {
 	if !c.forceRollback {
 		return errors.New("repository upgrade lock can only be revoked unsafely; please use the --force flag")
@@ -109,10 +263,33 @@ func (c *commandRepositoryUpgrade) runPhase(act func(context.Context, repo.Direc
 	}
 }
 
+func (c *commandRepositoryUpgrade) ignoreErrorOnAlwaysCommit(act func(context.Context, repo.DirectRepositoryWriter) error) func(context.Context, repo.DirectRepositoryWriter) error {
+	return func(ctx context.Context, rep repo.DirectRepositoryWriter) error {
+		err := act(ctx, rep)
+		if err == nil {
+			return nil
+		}
+
+		if c.commitMode == commitModeAlwaysCommit {
+			log(ctx).Errorf("%v", err)
+			return nil
+		}
+
+		// Explicitly skip all stages on error because tests do not
+		// skip/exit on error. Tests override os.Exit() that prevents
+		// running rest of the phases until we set the skip flag here.
+		// This flag is designed for testability and also to support
+		// rollback.
+		c.skip = true
+
+		return err
+	}
+}
+
 // setLockIntent is an upgrade phase which sets the upgrade lock intent with
 // desired parameters.
 func (c *commandRepositoryUpgrade) setLockIntent(ctx context.Context, rep repo.DirectRepositoryWriter) error {
-	if c.ioDrainTimeout < format.DefaultRepositoryBlobCacheDuration && !c.force {
+	if c.ioDrainTimeout < format.DefaultRepositoryBlobCacheDuration && !c.allowUnsafeUpgradeTimings {
 		return errors.Errorf("minimum required io-drain-timeout is %s", format.DefaultRepositoryBlobCacheDuration)
 	}
 
@@ -283,11 +460,6 @@ func (c *commandRepositoryUpgrade) upgrade(ctx context.Context, rep repo.DirectR
 		return errors.Wrap(err, "error setting parameters")
 	}
 
-	// poison V0 index so that old readers won't be able to open it.
-	if err := content.WriteLegacyIndexPoisonBlob(ctx, rep.BlobStorage()); err != nil {
-		log(ctx).Errorf("unable to write legacy index poison blob: %v", err)
-	}
-
 	// we need to reopen the repository after this point
 
 	log(ctx).Infof("Repository indices have been upgraded.")
@@ -301,6 +473,11 @@ func (c *commandRepositoryUpgrade) upgrade(ctx context.Context, rep repo.DirectR
 // cleanup and backups used for the rollback mechanism, so we cannot rollback
 // after this phase.
 func (c *commandRepositoryUpgrade) commitUpgrade(ctx context.Context, rep repo.DirectRepositoryWriter) error {
+	if c.commitMode == commitModeNeverCommit {
+		log(ctx).Infof("Commit mode is set to 'never'.  Skipping commit.")
+		return nil
+	}
+
 	if err := rep.FormatManager().CommitUpgrade(ctx); err != nil {
 		return errors.Wrap(err, "error finalizing upgrade")
 	}
diff --git a/go.mod b/go.mod
index 8f3413f0..9fac2843 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
 	github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 // this is pulling master, which is newer than v2
 	github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
-	github.com/aws/aws-sdk-go v1.44.143
+	github.com/aws/aws-sdk-go v1.44.157
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
 	github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
 	github.com/fatih/color v1.13.0
@@ -56,8 +56,8 @@ require (
 	golang.org/x/sync v0.1.0
 	golang.org/x/sys v0.3.0
 	golang.org/x/term v0.2.0
-	golang.org/x/text v0.4.0
-	google.golang.org/api v0.103.0
+	golang.org/x/text v0.5.0
+	google.golang.org/api v0.104.0
 	google.golang.org/grpc v1.51.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/ini.v1 v1.66.6 // indirect
@@ -98,7 +98,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect
+	google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect
 )
 
 require (
@@ -108,17 +108,17 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/klauspost/reedsolomon v1.11.3
 	github.com/prometheus/client_model v0.3.0
-	go.opentelemetry.io/otel v1.11.1
+	go.opentelemetry.io/otel v1.11.2
 	go.opentelemetry.io/otel/exporters/jaeger v1.11.1
 	go.opentelemetry.io/otel/sdk v1.11.1
-	go.opentelemetry.io/otel/trace v1.11.1
+	go.opentelemetry.io/otel/trace v1.11.2
 	golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
 )
 
 require (
-	cloud.google.com/go/compute v1.12.1 // indirect
-	cloud.google.com/go/compute/metadata v0.2.1 // indirect
-	cloud.google.com/go/iam v0.7.0 // indirect
+	cloud.google.com/go/compute v1.13.0 // indirect
+	cloud.google.com/go/compute/metadata v0.2.2 // indirect
+	cloud.google.com/go/iam v0.8.0 // indirect
 	github.com/alessio/shellescape v1.4.1 // indirect
 	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/felixge/fgprof v0.9.3 // indirect
diff --git a/go.sum b/go.sum
index 811d8173..0bd6fb93 100644
--- a/go.sum
+++ b/go.sum
@@ -21,14 +21,14 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
-cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
-cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
-cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
+cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
+cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
+cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
+cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
-cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk=
+cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
 cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
@@ -81,8 +81,8 @@ github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5K
 github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
 github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
-github.com/aws/aws-sdk-go v1.44.143 h1:5xN01uj7zV0VozeE/cxsHsNjSIooqmzVRX0omLxoO0w=
-github.com/aws/aws-sdk-go v1.44.143/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.157 h1:JVBPpEWC8+yA7CbfAuTl/ZFFlHS3yoqWFqxFyTCISwg=
+github.com/aws/aws-sdk-go v1.44.157/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -427,14 +427,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
-go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
+go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0=
+go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI=
 go.opentelemetry.io/otel/exporters/jaeger v1.11.1 h1:F9Io8lqWdGyIbY3/SOGki34LX/l+7OL0gXNxjqwcbuQ=
 go.opentelemetry.io/otel/exporters/jaeger v1.11.1/go.mod h1:lRa2w3bQ4R4QN6zYsDgy7tEezgoKEu7Ow2g35Y75+KI=
 go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
 go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
-go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
-go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
+go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0=
+go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -620,8 +620,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -688,8 +689,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
-google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
+google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg=
+google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -727,8 +728,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg=
-google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/repo/content/blob_crypto.go b/internal/blobcrypto/blob_crypto.go
similarity index 73%
rename from repo/content/blob_crypto.go
rename to internal/blobcrypto/blob_crypto.go
index df0dbf6d..da4d126c 100644
--- a/repo/content/blob_crypto.go
+++ b/internal/blobcrypto/blob_crypto.go
@@ -1,4 +1,5 @@
-package content
+// Package blobcrypto performs whole-blob crypto operations.
+package blobcrypto
 
 import (
 	"crypto/aes"
@@ -13,8 +14,8 @@
 	"github.com/kopia/kopia/repo/hashing"
 )
 
-// crypter ecapsulates hashing and encryption.
-type crypter interface {
+// Crypter ecapsulates hashing and encryption.
+type Crypter interface {
 	HashFunc() hashing.HashFunc
 	Encryptor() encryption.Encryptor
 }
@@ -38,16 +39,16 @@ func getIndexBlobIV(s blob.ID) ([]byte, error) {
 	return v, nil
 }
 
-// EncryptBLOB encrypts the given data using crypter-defined key and returns a name that should
+// Encrypt encrypts the given data using crypter-defined key and returns a name that should
 // be used to save the blob in thre repository.
-func EncryptBLOB(c crypter, payload gather.Bytes, prefix blob.ID, sessionID SessionID, output *gather.WriteBuffer) (blob.ID, error) {
+func Encrypt(c Crypter, payload gather.Bytes, prefix, suffix blob.ID, output *gather.WriteBuffer) (blob.ID, error) {
 	var hashOutput [hashing.MaxHashSize]byte
 
 	hash := c.HashFunc()(hashOutput[:0], payload)
 	blobID := prefix + blob.ID(hex.EncodeToString(hash))
 
-	if sessionID != "" {
-		blobID += blob.ID("-" + sessionID)
+	if suffix != "" {
+		blobID += "-" + suffix
 	}
 
 	iv, err := getIndexBlobIV(blobID)
@@ -64,8 +65,8 @@ func EncryptBLOB(c crypter, payload gather.Bytes, prefix blob.ID, sessionID Sess
 	return blobID, nil
 }
 
-// DecryptBLOB decrypts the provided data using provided blobID to derive initialization vector.
-func DecryptBLOB(c crypter, payload gather.Bytes, blobID blob.ID, output *gather.WriteBuffer) error {
+// Decrypt decrypts the provided data using provided blobID to derive initialization vector.
+func Decrypt(c Crypter, payload gather.Bytes, blobID blob.ID, output *gather.WriteBuffer) error {
 	iv, err := getIndexBlobIV(blobID)
 	if err != nil {
 		return errors.Wrap(err, "unable to get index blob IV")
diff --git a/internal/repolog/internal_logger.go b/internal/repolog/internal_logger.go
new file mode 100644
index 00000000..f537385c
--- /dev/null
+++ b/internal/repolog/internal_logger.go
@@ -0,0 +1,119 @@
+package repolog
+
+import (
+	"compress/gzip"
+	"fmt"
+	"io"
+	"sync"
+	"sync/atomic"
+
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/repo/blob"
+)
+
+// internalLogger represents a single log session that saves log files as blobs in the repository.
+// The logger starts disabled and to actually persist logs enable() must be called.
+type internalLogger struct {
+	nextChunkNumber atomic.Int32
+
+	m  *LogManager
+	mu sync.Mutex
+
+	// +checklocks:mu
+	buf *gather.WriteBuffer
+	// +checklocks:mu
+	gzw *gzip.Writer
+
+	// +checklocks:mu
+	startTime int64 // unix timestamp of the first log
+
+	prefix blob.ID // +checklocksignore
+}
+
+func (l *internalLogger) Write(b []byte) (int, error) {
+	l.maybeEncryptAndWriteChunkUnlocked(l.addAndMaybeFlush(b))
+	return len(b), nil
+}
+
+func (l *internalLogger) maybeEncryptAndWriteChunkUnlocked(data gather.Bytes, closeFunc func()) {
+	if data.Length() == 0 {
+		closeFunc()
+		return
+	}
+
+	if !l.m.enabled.Load() {
+		closeFunc()
+		return
+	}
+
+	endTime := l.m.timeFunc().Unix()
+
+	l.mu.Lock()
+	st := l.startTime
+	l.mu.Unlock()
+
+	prefix := blob.ID(fmt.Sprintf("%v_%v_%v_%v_", l.prefix, st, endTime, l.nextChunkNumber.Add(1)))
+
+	l.m.encryptAndWriteLogBlob(prefix, data, closeFunc)
+}
+
+func (l *internalLogger) addAndMaybeFlush(b []byte) (payload gather.Bytes, closeFunc func()) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+
+	w := l.ensureWriterInitializedLocked()
+
+	_, err := w.Write(b)
+	l.logUnexpectedError(err)
+
+	if l.buf.Length() < l.m.flushThreshold {
+		return gather.Bytes{}, func() {}
+	}
+
+	return l.flushAndResetLocked()
+}
+
+// +checklocks:l.mu
+func (l *internalLogger) ensureWriterInitializedLocked() io.Writer {
+	if l.gzw == nil {
+		l.buf = gather.NewWriteBuffer()
+		l.gzw = gzip.NewWriter(l.buf)
+		l.startTime = l.m.timeFunc().Unix()
+	}
+
+	return l.gzw
+}
+
+// +checklocks:l.mu
+func (l *internalLogger) flushAndResetLocked() (payload gather.Bytes, closeFunc func()) {
+	if l.gzw == nil {
+		return gather.Bytes{}, func() {}
+	}
+
+	l.logUnexpectedError(l.gzw.Flush())
+	l.logUnexpectedError(l.gzw.Close())
+
+	closeBuf := l.buf.Close
+	res := l.buf.Bytes()
+
+	l.buf = nil
+	l.gzw = nil
+
+	return res, closeBuf
+}
+
+func (l *internalLogger) logUnexpectedError(err error) {
+	if err == nil {
+		return
+	}
+}
+
+func (l *internalLogger) Sync() error {
+	l.mu.Lock()
+	data, closeFunc := l.flushAndResetLocked()
+	l.mu.Unlock()
+
+	l.maybeEncryptAndWriteChunkUnlocked(data, closeFunc)
+
+	return nil
+}
diff --git a/internal/repolog/log_manager.go b/internal/repolog/log_manager.go
new file mode 100644
index 00000000..8e2379f3
--- /dev/null
+++ b/internal/repolog/log_manager.go
@@ -0,0 +1,112 @@
+// Package repolog manages logs in the repository.
+package repolog
+
+import (
+	"context"
+	"crypto/rand"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+
+	"github.com/kopia/kopia/internal/blobcrypto"
+	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/zaplogutil"
+	"github.com/kopia/kopia/repo/blob"
+)
+
+const blobLoggerFlushThreshold = 4 << 20
+
+// BlobPrefix is a prefix given to text logs stored in repository.
+const BlobPrefix = "_log_"
+
+// LogManager manages writing encrypted log blobs to the repository.
+type LogManager struct {
+	enabled atomic.Bool // set by enable(), logger is ineffective until called
+
+	// InternalLogManager implements io.Writer and we must be able to write to the
+	// repository asynchronously when the context is not provided.
+	ctx context.Context //nolint:containedctx
+
+	st             blob.Storage
+	bc             blobcrypto.Crypter
+	wg             sync.WaitGroup
+	timeFunc       func() time.Time
+	flushThreshold int
+}
+
+// Close closes the log manager.
+func (m *LogManager) Close(ctx context.Context) {
+	m.wg.Wait()
+}
+
+func (m *LogManager) encryptAndWriteLogBlob(prefix blob.ID, data gather.Bytes, closeFunc func()) {
+	encrypted := gather.NewWriteBuffer()
+	// Close happens in a goroutine
+
+	blobID, err := blobcrypto.Encrypt(m.bc, data, prefix, "", encrypted)
+	if err != nil {
+		encrypted.Close()
+
+		// this should not happen, also nothing can be done about this, we're not in a place where we can return error, log it.
+		return
+	}
+
+	b := encrypted.Bytes()
+
+	m.wg.Add(1)
+
+	go func() {
+		defer m.wg.Done()
+		defer encrypted.Close()
+		defer closeFunc()
+
+		if err := m.st.PutBlob(m.ctx, blobID, b, blob.PutOptions{}); err != nil {
+			// nothing can be done about this, we're not in a place where we can return error, log it.
+			return
+		}
+	}()
+}
+
+// NewLogger creates new logger.
+func (m *LogManager) NewLogger() *zap.SugaredLogger {
+	var rnd [2]byte
+
+	rand.Read(rnd[:]) //nolint:errcheck
+
+	w := &internalLogger{
+		m:      m,
+		prefix: blob.ID(fmt.Sprintf("%v%v_%x", BlobPrefix, clock.Now().Local().Format("20060102150405"), rnd)),
+	}
+
+	return zap.New(zapcore.NewCore(
+		zaplogutil.NewStdConsoleEncoder(zaplogutil.StdConsoleEncoderConfig{
+			TimeLayout: zaplogutil.PreciseLayout,
+			LocalTime:  false,
+		}),
+		w, zap.DebugLevel), zap.WithClock(zaplogutil.Clock())).Sugar()
+}
+
+// Enable enables writing any buffered logs to repository.
+func (m *LogManager) Enable() {
+	if m == nil {
+		return
+	}
+
+	m.enabled.Store(true)
+}
+
+// NewLogManager creates a new LogManager that will emit logs as repository blobs.
+func NewLogManager(ctx context.Context, st blob.Storage, bc blobcrypto.Crypter) *LogManager {
+	return &LogManager{
+		ctx:            ctx,
+		st:             st,
+		bc:             bc,
+		flushThreshold: blobLoggerFlushThreshold,
+		timeFunc:       clock.Now,
+	}
+}
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index 1aa99fa7..bddac324 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -18,11 +18,13 @@
 	"github.com/kopia/kopia/internal/listcache"
 	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/ownwrites"
+	"github.com/kopia/kopia/internal/repolog"
 	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/filesystem"
 	"github.com/kopia/kopia/repo/blob/sharded"
 	"github.com/kopia/kopia/repo/compression"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/hashing"
 	"github.com/kopia/kopia/repo/logging"
@@ -45,9 +47,9 @@
 
 //nolint:gochecknoglobals
 var cachedIndexBlobPrefixes = []blob.ID{
-	LegacyIndexBlobPrefix,
-	compactionLogBlobPrefix,
-	cleanupBlobPrefix,
+	indexblob.V0IndexBlobPrefix,
+	indexblob.V0CompactionLogBlobPrefix,
+	indexblob.V0CleanupBlobPrefix,
 
 	epoch.UncompactedIndexBlobPrefix,
 	epoch.EpochMarkerIndexBlobPrefix,
@@ -57,19 +59,15 @@
 
 //nolint:gochecknoglobals
 var allIndexBlobPrefixes = []blob.ID{
-	LegacyIndexBlobPrefix,
+	indexblob.V0IndexBlobPrefix,
 	epoch.UncompactedIndexBlobPrefix,
 	epoch.SingleEpochCompactionBlobPrefix,
 	epoch.RangeCheckpointIndexBlobPrefix,
 }
 
-// indexBlobManager is the API of index blob manager as used by content manager.
-type indexBlobManager interface {
-	writeIndexBlobs(ctx context.Context, data []gather.Bytes, sessionID SessionID) ([]blob.Metadata, error)
-	listActiveIndexBlobs(ctx context.Context) ([]IndexBlobInfo, time.Time, error)
-	compact(ctx context.Context, opts CompactOptions) error
-	flushCache(ctx context.Context)
-	invalidate(ctx context.Context)
+// IndexBlobReader provides an API for reading index blobs.
+type IndexBlobReader interface {
+	ListIndexBlobInfos(context.Context) ([]indexblob.Metadata, time.Time, error)
 }
 
 // SharedManager is responsible for read-only access to committed data.
@@ -77,14 +75,13 @@ type SharedManager struct {
 	Stats *Stats
 	st    blob.Storage
 
-	indexBlobManagerV0 *indexBlobManagerV0
-	indexBlobManagerV1 *indexBlobManagerV1
+	indexBlobManagerV0 *indexblob.ManagerV0
+	indexBlobManagerV1 *indexblob.ManagerV1
 
 	contentCache      cache.ContentCache
 	metadataCache     cache.ContentCache
 	indexBlobCache    *cache.PersistentCache
 	committedContents *committedContentIndex
-	enc               *encryptedBlobMgr
 	timeNow           func() time.Time
 
 	// lock to protect the set of commtited indexes
@@ -107,13 +104,33 @@ type SharedManager struct {
 	log logging.Logger
 
 	// logger associated with the context that opened the repository.
-	contextLogger      logging.Logger
-	internalLogManager *internalLogManager
-	internalLogger     *zap.SugaredLogger // backing logger for 'sharedBaseLogger'
+	contextLogger  logging.Logger
+	repoLogManager *repolog.LogManager
+	internalLogger *zap.SugaredLogger // backing logger for 'sharedBaseLogger'
 
 	metricsStruct
 }
 
+// LoadIndexBlob return index information loaded from the specified blob.
+func (sm *SharedManager) LoadIndexBlob(ctx context.Context, ibid blob.ID, d *gather.WriteBuffer) ([]Info, error) {
+	err := sm.st.GetBlob(ctx, ibid, 0, -1, d)
+	if err != nil {
+		return nil, errors.Wrapf(err, "could not find index blob %q", ibid)
+	}
+
+	return ParseIndexBlob(ibid, d.Bytes(), sm.format)
+}
+
+// IndexReaderV0 return an index reader for reading V0 indexes.
+func (sm *SharedManager) IndexReaderV0() IndexBlobReader {
+	return sm.indexBlobManagerV0
+}
+
+// IndexReaderV1 return an index reader for reading V1 indexes.
+func (sm *SharedManager) IndexReaderV1() IndexBlobReader {
+	return sm.indexBlobManagerV1
+}
+
 func (sm *SharedManager) readPackFileLocalIndex(ctx context.Context, packFile blob.ID, packFileLength int64, output *gather.WriteBuffer) error {
 	var err error
 
@@ -191,13 +208,17 @@ func (sm *SharedManager) loadPackIndexesLocked(ctx context.Context) error {
 		}
 
 		if i > 0 {
-			ibm.flushCache(ctx)
+			// invalidate any list caches.
+			if err := sm.st.FlushCaches(ctx); err != nil {
+				sm.log.Errorw("unable to flush caches", "err", err)
+			}
+
 			sm.log.Debugf("encountered NOT_FOUND when loading, sleeping %v before retrying #%v", nextSleepTime, i)
 			time.Sleep(nextSleepTime)
 			nextSleepTime *= 2
 		}
 
-		indexBlobs, ignoreDeletedBefore, err := ibm.listActiveIndexBlobs(ctx)
+		indexBlobs, ignoreDeletedBefore, err := ibm.ListActiveIndexBlobs(ctx)
 		if err != nil {
 			return errors.Wrap(err, "error listing index blobs")
 		}
@@ -239,7 +260,7 @@ func (sm *SharedManager) getCacheForContentID(id ID) cache.ContentCache {
 	return sm.contentCache
 }
 
-func (sm *SharedManager) indexBlobManager() (indexBlobManager, error) {
+func (sm *SharedManager) indexBlobManager() (indexblob.Manager, error) {
 	mp, mperr := sm.format.GetMutableParameters()
 	if mperr != nil {
 		return nil, errors.Wrap(mperr, "mutable parameters")
@@ -311,9 +332,9 @@ func (sm *SharedManager) decryptAndVerify(encrypted gather.Bytes, iv []byte, out
 }
 
 // IndexBlobs returns the list of active index blobs.
-func (sm *SharedManager) IndexBlobs(ctx context.Context, includeInactive bool) ([]IndexBlobInfo, error) {
+func (sm *SharedManager) IndexBlobs(ctx context.Context, includeInactive bool) ([]indexblob.Metadata, error) {
 	if includeInactive {
-		var result []IndexBlobInfo
+		var result []indexblob.Metadata
 
 		for _, prefix := range allIndexBlobPrefixes {
 			blobs, err := blob.ListAllBlobs(ctx, sm.st, prefix)
@@ -322,7 +343,7 @@ func (sm *SharedManager) IndexBlobs(ctx context.Context, includeInactive bool) (
 			}
 
 			for _, bm := range blobs {
-				result = append(result, IndexBlobInfo{Metadata: bm})
+				result = append(result, indexblob.Metadata{Metadata: bm})
 			}
 		}
 
@@ -334,7 +355,7 @@ func (sm *SharedManager) IndexBlobs(ctx context.Context, includeInactive bool) (
 		return nil, err0
 	}
 
-	blobs, _, err := ibm.listActiveIndexBlobs(ctx)
+	blobs, _, err := ibm.ListActiveIndexBlobs(ctx)
 
 	//nolint:wrapcheck
 	return blobs, err
@@ -446,32 +467,36 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 		return errors.Wrap(err, "unable to initialize list cache")
 	}
 
-	sm.enc = &encryptedBlobMgr{
-		st:             cachedSt,
-		crypter:        sm.format,
-		indexBlobCache: indexBlobCache,
-		log:            sm.namedLogger("encrypted-blob-manager"),
-	}
+	enc := indexblob.NewEncryptionManager(
+		cachedSt,
+		sm.format,
+		indexBlobCache,
+		sm.namedLogger("encrypted-blob-manager"))
 
 	// set up legacy index blob manager
-	sm.indexBlobManagerV0 = &indexBlobManagerV0{
-		st:                cachedSt,
-		enc:               sm.enc,
-		timeNow:           sm.timeNow,
-		formattingOptions: sm.format,
-		log:               sm.namedLogger("index-blob-manager"),
-	}
+	sm.indexBlobManagerV0 = indexblob.NewManagerV0(
+		cachedSt,
+		enc,
+		sm.timeNow,
+		sm.format,
+		sm.namedLogger("index-blob-manager"),
+	)
 
 	// set up new index blob manager
-	sm.indexBlobManagerV1 = &indexBlobManagerV1{
-		st:                cachedSt,
-		enc:               sm.enc,
-		timeNow:           sm.timeNow,
-		formattingOptions: sm.format,
-		log:               sm.namedLogger("index-blob-manager"),
-	}
-
-	sm.indexBlobManagerV1.epochMgr = epoch.NewManager(cachedSt, epochParameters{sm.format}, sm.indexBlobManagerV1.compactEpoch, sm.namedLogger("epoch-manager"), sm.timeNow)
+	sm.indexBlobManagerV1 = indexblob.NewManagerV1(
+		cachedSt,
+		enc,
+		epoch.NewManager(cachedSt,
+			epochParameters{sm.format},
+			func(ctx context.Context, blobIDs []blob.ID, outputPrefix blob.ID) error {
+				return errors.Wrap(sm.indexBlobManagerV1.CompactEpoch(ctx, blobIDs, outputPrefix), "CompactEpoch")
+			},
+			sm.namedLogger("epoch-manager"),
+			sm.timeNow),
+		sm.timeNow,
+		sm.format,
+		sm.namedLogger("index-blob-manager"),
+	)
 
 	// once everything is ready, set it up
 	sm.contentCache = dataCache
@@ -480,7 +505,7 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 	sm.committedContents = newCommittedContentIndex(caching,
 		sm.format.Encryptor().Overhead,
 		sm.format,
-		sm.enc.getEncryptedBlob,
+		enc.GetEncryptedBlob,
 		sm.namedLogger("committed-content-index"),
 		caching.MinIndexSweepAge.DurationOrDefault(DefaultIndexCacheSweepAge))
 
@@ -507,12 +532,12 @@ func (sm *SharedManager) EpochManager() (*epoch.Manager, bool, error) {
 		return nil, false, err
 	}
 
-	ibm1, ok := ibm.(*indexBlobManagerV1)
+	ibm1, ok := ibm.(*indexblob.ManagerV1)
 	if !ok {
 		return nil, false, nil
 	}
 
-	return ibm1.epochMgr, true, nil
+	return ibm1.EpochManager(), true, nil
 }
 
 // CloseShared releases all resources in a shared manager.
@@ -529,9 +554,9 @@ func (sm *SharedManager) CloseShared(ctx context.Context) error {
 		sm.internalLogger.Sync() //nolint:errcheck
 	}
 
-	sm.internalLogManager.Close(ctx)
+	sm.repoLogManager.Close(ctx)
 
-	sm.indexBlobManagerV1.epochMgr.Flush()
+	sm.indexBlobManagerV1.EpochManager().Flush()
 
 	if err := sm.st.Close(ctx); err != nil {
 		return errors.Wrap(err, "error closing storage")
@@ -543,7 +568,7 @@ func (sm *SharedManager) CloseShared(ctx context.Context) error {
 // AlsoLogToContentLog wraps the provided content so that all logs are also sent to
 // internal content log.
 func (sm *SharedManager) AlsoLogToContentLog(ctx context.Context) context.Context {
-	sm.internalLogManager.enable()
+	sm.repoLogManager.Enable()
 
 	return logging.AlsoLogTo(ctx, sm.log)
 }
@@ -555,6 +580,12 @@ func (sm *SharedManager) shouldRefreshIndexes() bool {
 	return sm.timeNow().After(sm.refreshIndexesAfter)
 }
 
+// PrepareUpgradeToIndexBlobManagerV1 prepares the repository for migrating to IndexBlobManagerV1.
+func (sm *SharedManager) PrepareUpgradeToIndexBlobManagerV1(ctx context.Context, params epoch.Parameters) error {
+	//nolint:wrapcheck
+	return sm.indexBlobManagerV1.PrepareUpgradeToIndexBlobManagerV1(ctx, params, sm.indexBlobManagerV0)
+}
+
 // NewSharedManager returns SharedManager that is used by SessionWriteManagers on top of a repository.
 func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider, caching *CachingOptions, opts *ManagerOptions, mr *metrics.Registry) (*SharedManager, error) {
 	opts = opts.CloneOrDefault()
@@ -562,18 +593,6 @@ func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider
 		opts.TimeNow = clock.Now
 	}
 
-	// create internal logger that will be writing logs as encrypted repository blobs.
-	ilm := newInternalLogManager(ctx, st, prov)
-
-	// sharedBaseLogger writes to the both context and internal log
-	// and is used as a base for all content manager components.
-	var internalLog *zap.SugaredLogger
-
-	// capture logger (usually console or log file) associated with current context.
-	if !opts.DisableInternalLog {
-		internalLog = ilm.NewLogger()
-	}
-
 	sm := &SharedManager{
 		st:                      st,
 		Stats:                   new(Stats),
@@ -583,14 +602,16 @@ func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider
 		maxPreambleLength:       defaultMaxPreambleLength,
 		paddingUnit:             defaultPaddingUnit,
 		checkInvariantsOnUnlock: os.Getenv("KOPIA_VERIFY_INVARIANTS") != "",
-		internalLogManager:      ilm,
-		internalLogger:          internalLog,
+		repoLogManager:          repolog.NewLogManager(ctx, st, prov),
 		contextLogger:           logging.Module(FormatLogModule)(ctx),
 
 		metricsStruct: initMetricsStruct(mr),
 	}
 
-	// remember logger defined for the context.
+	if !opts.DisableInternalLog {
+		sm.internalLogger = sm.repoLogManager.NewLogger()
+	}
+
 	sm.log = sm.namedLogger("shared-manager")
 
 	caching = caching.CloneOrDefault()
diff --git a/repo/content/content_manager.go b/repo/content/content_manager.go
index cd832d65..92eee98c 100644
--- a/repo/content/content_manager.go
+++ b/repo/content/content_manager.go
@@ -20,6 +20,7 @@
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/hashing"
 	"github.com/kopia/kopia/repo/logging"
@@ -62,12 +63,6 @@
 // ErrContentNotFound is returned when content is not found.
 var ErrContentNotFound = errors.New("content not found")
 
-// IndexBlobInfo is an information about a single index blob managed by Manager.
-type IndexBlobInfo struct {
-	blob.Metadata
-	Superseded []blob.Metadata
-}
-
 // WriteManager builds content-addressable storage with encryption, deduplication and packaging on top of BLOB store.
 type WriteManager struct {
 	revision            atomic.Int64 // changes on each local write
@@ -429,7 +424,7 @@ func (bm *WriteManager) writeIndexBlobs(ctx context.Context, dataShards []gather
 	}
 
 	//nolint:wrapcheck
-	return ibm.writeIndexBlobs(ctx, dataShards, sessionID)
+	return ibm.WriteIndexBlobs(ctx, dataShards, blob.ID(sessionID))
 }
 
 // +checklocksread:bm.indexesLock
@@ -452,7 +447,7 @@ func (bm *WriteManager) flushPackIndexesLocked(ctx context.Context, mp format.Mu
 
 	if len(bm.packIndexBuilder) > 0 {
 		_, span2 := tracer.Start(ctx, "BuildShards")
-		dataShards, closeShards, err := bm.packIndexBuilder.BuildShards(mp.IndexVersion, true, defaultIndexShardSize)
+		dataShards, closeShards, err := bm.packIndexBuilder.BuildShards(mp.IndexVersion, true, indexblob.DefaultIndexShardSize)
 
 		span2.End()
 
@@ -732,7 +727,7 @@ func (bm *WriteManager) getOrCreatePendingPackInfoLocked(ctx context.Context, pr
 		return pp, nil
 	}
 
-	bm.internalLogManager.enable()
+	bm.repoLogManager.Enable()
 
 	b := gather.NewWriteBuffer()
 
diff --git a/repo/content/content_manager_indexes.go b/repo/content/content_manager_indexes.go
index 76eded95..8cc4d38b 100644
--- a/repo/content/content_manager_indexes.go
+++ b/repo/content/content_manager_indexes.go
@@ -2,35 +2,17 @@
 
 import (
 	"context"
-	"time"
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/blobcrypto"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
-const verySmallContentFraction = 20 // blobs less than 1/verySmallContentFraction of maxPackSize are considered 'very small'
-
-// CompactOptions provides options for compaction.
-type CompactOptions struct {
-	MaxSmallBlobs                    int
-	AllIndexes                       bool
-	DropDeletedBefore                time.Time
-	DropContents                     []ID
-	DisableEventualConsistencySafety bool
-}
-
-func (co *CompactOptions) maxEventualConsistencySettleTime() time.Duration {
-	if co.DisableEventualConsistencySafety {
-		return 0
-	}
-
-	return defaultEventualConsistencySettleTime
-}
-
 // Refresh reloads the committed content indexes.
 func (sm *SharedManager) Refresh(ctx context.Context) error {
 	sm.indexesLock.Lock()
@@ -43,7 +25,7 @@ func (sm *SharedManager) Refresh(ctx context.Context) error {
 		return err
 	}
 
-	ibm.invalidate(ctx)
+	ibm.Invalidate(ctx)
 
 	timer := timetrack.StartTimer()
 
@@ -54,7 +36,7 @@ func (sm *SharedManager) Refresh(ctx context.Context) error {
 }
 
 // CompactIndexes performs compaction of index blobs ensuring that # of small index blobs is below opt.maxSmallBlobs.
-func (sm *SharedManager) CompactIndexes(ctx context.Context, opt CompactOptions) error {
+func (sm *SharedManager) CompactIndexes(ctx context.Context, opt indexblob.CompactOptions) error {
 	// we must hold the lock here to avoid the race with Refresh() which can reload the
 	// current set of indexes while we process them.
 	sm.indexesLock.Lock()
@@ -67,7 +49,7 @@ func (sm *SharedManager) CompactIndexes(ctx context.Context, opt CompactOptions)
 		return err
 	}
 
-	if err := ibm.compact(ctx, opt); err != nil {
+	if err := ibm.Compact(ctx, opt); err != nil {
 		return errors.Wrap(err, "error performing compaction")
 	}
 
@@ -80,11 +62,11 @@ func (sm *SharedManager) CompactIndexes(ctx context.Context, opt CompactOptions)
 }
 
 // ParseIndexBlob loads entries in a given index blob and returns them.
-func ParseIndexBlob(ctx context.Context, blobID blob.ID, encrypted gather.Bytes, crypter crypter) ([]Info, error) {
+func ParseIndexBlob(blobID blob.ID, encrypted gather.Bytes, crypter blobcrypto.Crypter) ([]Info, error) {
 	var data gather.WriteBuffer
 	defer data.Close()
 
-	if err := DecryptBLOB(crypter, encrypted, blobID, &data); err != nil {
+	if err := blobcrypto.Decrypt(crypter, encrypted, blobID, &data); err != nil {
 		return nil, errors.Wrap(err, "unable to decrypt index blob")
 	}
 
@@ -102,17 +84,3 @@ func ParseIndexBlob(ctx context.Context, blobID blob.ID, encrypted gather.Bytes,
 
 	return results, errors.Wrap(err, "error iterating index entries")
 }
-
-func addBlobsToIndex(ndx map[blob.ID]*IndexBlobInfo, blobs []blob.Metadata) {
-	for _, it := range blobs {
-		if ndx[it.BlobID] == nil {
-			ndx[it.BlobID] = &IndexBlobInfo{
-				Metadata: blob.Metadata{
-					BlobID:    it.BlobID,
-					Length:    it.Length,
-					Timestamp: it.Timestamp,
-				},
-			}
-		}
-	}
-}
diff --git a/repo/content/encrypted_blob_mgr.go b/repo/content/encrypted_blob_mgr.go
deleted file mode 100644
index 3960af6f..00000000
--- a/repo/content/encrypted_blob_mgr.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package content
-
-import (
-	"context"
-
-	"github.com/pkg/errors"
-
-	"github.com/kopia/kopia/internal/cache"
-	"github.com/kopia/kopia/internal/gather"
-	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/logging"
-)
-
-type encryptedBlobMgr struct {
-	st             blob.Storage
-	crypter        crypter
-	indexBlobCache *cache.PersistentCache
-	log            logging.Logger
-}
-
-func (m *encryptedBlobMgr) getEncryptedBlob(ctx context.Context, blobID blob.ID, output *gather.WriteBuffer) error {
-	var payload gather.WriteBuffer
-	defer payload.Close()
-
-	if err := m.indexBlobCache.GetOrLoad(ctx, string(blobID), func(output *gather.WriteBuffer) error {
-		//nolint:wrapcheck
-		return m.st.GetBlob(ctx, blobID, 0, -1, output)
-	}, &payload); err != nil {
-		return errors.Wrap(err, "getContent")
-	}
-
-	return DecryptBLOB(m.crypter, payload.Bytes(), blobID, output)
-}
-
-func (m *encryptedBlobMgr) encryptAndWriteBlob(ctx context.Context, data gather.Bytes, prefix blob.ID, sessionID SessionID) (blob.Metadata, error) {
-	var data2 gather.WriteBuffer
-	defer data2.Close()
-
-	blobID, err := EncryptBLOB(m.crypter, data, prefix, sessionID, &data2)
-	if err != nil {
-		return blob.Metadata{}, errors.Wrap(err, "error encrypting")
-	}
-
-	bm, err := blob.PutBlobAndGetMetadata(ctx, m.st, blobID, data2.Bytes(), blob.PutOptions{})
-	if err != nil {
-		m.log.Debugf("write-index-blob %v failed %v", blobID, err)
-		return blob.Metadata{}, errors.Wrapf(err, "error writing blob %v", blobID)
-	}
-
-	m.log.Debugf("write-index-blob %v %v %v", blobID, bm.Length, bm.Timestamp)
-
-	return bm, nil
-}
diff --git a/repo/content/index_blob_manager_v1.go b/repo/content/index_blob_manager_v1.go
deleted file mode 100644
index a143aa36..00000000
--- a/repo/content/index_blob_manager_v1.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package content
-
-import (
-	"context"
-	"crypto/rand"
-	"fmt"
-	"time"
-
-	"github.com/pkg/errors"
-
-	"github.com/kopia/kopia/internal/epoch"
-	"github.com/kopia/kopia/internal/gather"
-	"github.com/kopia/kopia/repo/blob"
-	"github.com/kopia/kopia/repo/content/index"
-	"github.com/kopia/kopia/repo/logging"
-)
-
-type indexBlobManagerV1 struct {
-	st                blob.Storage
-	enc               *encryptedBlobMgr
-	epochMgr          *epoch.Manager
-	timeNow           func() time.Time
-	log               logging.Logger
-	formattingOptions IndexFormattingOptions
-}
-
-func (m *indexBlobManagerV1) listActiveIndexBlobs(ctx context.Context) ([]IndexBlobInfo, time.Time, error) {
-	active, deletionWatermark, err := m.epochMgr.GetCompleteIndexSet(ctx, epoch.LatestEpoch)
-	if err != nil {
-		return nil, time.Time{}, errors.Wrap(err, "error getting index set")
-	}
-
-	var result []IndexBlobInfo
-
-	for _, bm := range active {
-		result = append(result, IndexBlobInfo{Metadata: bm})
-	}
-
-	m.log.Errorf("active indexes %v deletion watermark %v", blob.IDsFromMetadata(active), deletionWatermark)
-
-	return result, deletionWatermark, nil
-}
-
-func (m *indexBlobManagerV1) invalidate(ctx context.Context) {
-	m.epochMgr.Invalidate()
-}
-
-func (m *indexBlobManagerV1) flushCache(ctx context.Context) {
-	if err := m.st.FlushCaches(ctx); err != nil {
-		m.log.Debugf("error flushing caches: %v", err)
-	}
-}
-
-func (m *indexBlobManagerV1) compact(ctx context.Context, opt CompactOptions) error {
-	if opt.DropDeletedBefore.IsZero() {
-		return nil
-	}
-
-	return errors.Wrap(m.epochMgr.AdvanceDeletionWatermark(ctx, opt.DropDeletedBefore), "error advancing deletion watermark")
-}
-
-func (m *indexBlobManagerV1) compactEpoch(ctx context.Context, blobIDs []blob.ID, outputPrefix blob.ID) error {
-	tmpbld := make(index.Builder)
-
-	for _, indexBlob := range blobIDs {
-		if err := addIndexBlobsToBuilder(ctx, m.enc, tmpbld, indexBlob); err != nil {
-			return errors.Wrap(err, "error adding index to builder")
-		}
-	}
-
-	mp, mperr := m.formattingOptions.GetMutableParameters()
-	if mperr != nil {
-		return errors.Wrap(mperr, "mutable parameters")
-	}
-
-	dataShards, cleanupShards, err := tmpbld.BuildShards(mp.IndexVersion, true, defaultIndexShardSize)
-	if err != nil {
-		return errors.Wrap(err, "unable to build index dataShards")
-	}
-
-	defer cleanupShards()
-
-	var rnd [8]byte
-
-	if _, err := rand.Read(rnd[:]); err != nil {
-		return errors.Wrap(err, "error getting random session ID")
-	}
-
-	sessionID := fmt.Sprintf("s%x-c%v", rnd[:], len(dataShards))
-
-	var data2 gather.WriteBuffer
-	defer data2.Close()
-
-	for _, data := range dataShards {
-		data2.Reset()
-
-		blobID, err := EncryptBLOB(m.enc.crypter, data, outputPrefix, SessionID(sessionID), &data2)
-		if err != nil {
-			return errors.Wrap(err, "error encrypting")
-		}
-
-		if err := m.st.PutBlob(ctx, blobID, data2.Bytes(), blob.PutOptions{}); err != nil {
-			return errors.Wrap(err, "error writing index blob")
-		}
-	}
-
-	return nil
-}
-
-func (m *indexBlobManagerV1) writeIndexBlobs(ctx context.Context, dataShards []gather.Bytes, sessionID SessionID) ([]blob.Metadata, error) {
-	shards := map[blob.ID]blob.Bytes{}
-
-	sessionID = SessionID(fmt.Sprintf("%v-c%v", sessionID, len(dataShards)))
-
-	for _, data := range dataShards {
-		// important - we're intentionally using data2 in the inner loop scheduling multiple Close()
-		// we want all Close() to be called at the end of the function after WriteIndex()
-		data2 := gather.NewWriteBuffer()
-		defer data2.Close() //nolint:gocritic
-
-		unprefixedBlobID, err := EncryptBLOB(m.enc.crypter, data, "", sessionID, data2)
-		if err != nil {
-			return nil, errors.Wrap(err, "error encrypting")
-		}
-
-		shards[unprefixedBlobID] = data2.Bytes()
-	}
-
-	//nolint:wrapcheck
-	return m.epochMgr.WriteIndex(ctx, shards)
-}
-
-var _ indexBlobManager = (*indexBlobManagerV1)(nil)
-
-// PrepareUpgradeToIndexBlobManagerV1 prepares the repository for migrating to IndexBlobManagerV1.
-func (sm *SharedManager) PrepareUpgradeToIndexBlobManagerV1(ctx context.Context, params epoch.Parameters) error {
-	ibl, _, err := sm.indexBlobManagerV0.listActiveIndexBlobs(ctx)
-	if err != nil {
-		return errors.Wrap(err, "error listing active index blobs")
-	}
-
-	var blobIDs []blob.ID
-
-	for _, ib := range ibl {
-		blobIDs = append(blobIDs, ib.BlobID)
-	}
-
-	if err := sm.indexBlobManagerV1.compactEpoch(ctx, blobIDs, epoch.UncompactedEpochBlobPrefix(epoch.FirstEpoch)); err != nil {
-		return errors.Wrap(err, "unable to generate initial epoch")
-	}
-
-	return nil
-}
diff --git a/repo/content/indexblob/index_blob.go b/repo/content/indexblob/index_blob.go
new file mode 100644
index 00000000..adb14922
--- /dev/null
+++ b/repo/content/indexblob/index_blob.go
@@ -0,0 +1,56 @@
+// Package indexblob manages sets of active index blobs.
+package indexblob
+
+import (
+	"context"
+	"time"
+
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/content/index"
+)
+
+// Manager is the API of index blob manager as used by content manager.
+type Manager interface {
+	WriteIndexBlobs(ctx context.Context, data []gather.Bytes, suffix blob.ID) ([]blob.Metadata, error)
+	ListActiveIndexBlobs(ctx context.Context) ([]Metadata, time.Time, error)
+	Compact(ctx context.Context, opts CompactOptions) error
+	Invalidate(ctx context.Context)
+}
+
+// CompactOptions provides options for compaction.
+type CompactOptions struct {
+	MaxSmallBlobs                    int
+	AllIndexes                       bool
+	DropDeletedBefore                time.Time
+	DropContents                     []index.ID
+	DisableEventualConsistencySafety bool
+}
+
+func (co *CompactOptions) maxEventualConsistencySettleTime() time.Duration {
+	if co.DisableEventualConsistencySafety {
+		return 0
+	}
+
+	return defaultEventualConsistencySettleTime
+}
+
+// DefaultIndexShardSize is the maximum number of items in an index shard.
+// It is less than 2^24, which lets V1 index use 24-bit/3-byte indexes.
+const DefaultIndexShardSize = 16e6
+
+const verySmallContentFraction = 20 // blobs less than 1/verySmallContentFraction of maxPackSize are considered 'very small'
+
+func addBlobsToIndex(ndx map[blob.ID]*Metadata, blobs []blob.Metadata) {
+	for _, it := range blobs {
+		if ndx[it.BlobID] == nil {
+			ndx[it.BlobID] = &Metadata{
+				Metadata: blob.Metadata{
+					BlobID:    it.BlobID,
+					Length:    it.Length,
+					Timestamp: it.Timestamp,
+				},
+			}
+		}
+	}
+}
diff --git a/repo/content/indexblob/index_blob_encryption.go b/repo/content/indexblob/index_blob_encryption.go
new file mode 100644
index 00000000..1112f559
--- /dev/null
+++ b/repo/content/indexblob/index_blob_encryption.go
@@ -0,0 +1,75 @@
+package indexblob
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+
+	"github.com/kopia/kopia/internal/blobcrypto"
+	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/logging"
+)
+
+// Metadata is an information about a single index blob managed by Manager.
+type Metadata struct {
+	blob.Metadata
+	Superseded []blob.Metadata
+}
+
+// EncryptionManager manages encryption and caching of index blobs.
+type EncryptionManager struct {
+	st             blob.Storage
+	crypter        blobcrypto.Crypter
+	indexBlobCache *cache.PersistentCache
+	log            logging.Logger
+}
+
+// GetEncryptedBlob fetches and decrypts the contents of a given encrypted blob
+// using cache first and falling back to the underlying storage.
+func (m *EncryptionManager) GetEncryptedBlob(ctx context.Context, blobID blob.ID, output *gather.WriteBuffer) error {
+	var payload gather.WriteBuffer
+	defer payload.Close()
+
+	if err := m.indexBlobCache.GetOrLoad(ctx, string(blobID), func(output *gather.WriteBuffer) error {
+		//nolint:wrapcheck
+		return m.st.GetBlob(ctx, blobID, 0, -1, output)
+	}, &payload); err != nil {
+		return errors.Wrap(err, "getContent")
+	}
+
+	return errors.Wrap(blobcrypto.Decrypt(m.crypter, payload.Bytes(), blobID, output), "decrypt blob")
+}
+
+// EncryptAndWriteBlob encrypts and writes the provided data into a blob,
+// with name {prefix}{hash}[-{suffix}].
+func (m *EncryptionManager) EncryptAndWriteBlob(ctx context.Context, data gather.Bytes, prefix, suffix blob.ID) (blob.Metadata, error) {
+	var data2 gather.WriteBuffer
+	defer data2.Close()
+
+	blobID, err := blobcrypto.Encrypt(m.crypter, data, prefix, suffix, &data2)
+	if err != nil {
+		return blob.Metadata{}, errors.Wrap(err, "error encrypting")
+	}
+
+	bm, err := blob.PutBlobAndGetMetadata(ctx, m.st, blobID, data2.Bytes(), blob.PutOptions{})
+	if err != nil {
+		m.log.Debugf("write-index-blob %v failed %v", blobID, err)
+		return blob.Metadata{}, errors.Wrapf(err, "error writing blob %v", blobID)
+	}
+
+	m.log.Debugf("write-index-blob %v %v %v", blobID, bm.Length, bm.Timestamp)
+
+	return bm, nil
+}
+
+// NewEncryptionManager creates new encryption manager.
+func NewEncryptionManager(
+	st blob.Storage,
+	crypter blobcrypto.Crypter,
+	indexBlobCache *cache.PersistentCache,
+	log logging.Logger,
+) *EncryptionManager {
+	return &EncryptionManager{st, crypter, indexBlobCache, log}
+}
diff --git a/repo/content/index_blob_manager_v0.go b/repo/content/indexblob/index_blob_manager_v0.go
similarity index 72%
rename from repo/content/index_blob_manager_v0.go
rename to repo/content/indexblob/index_blob_manager_v0.go
index 8e674f0e..b6e7e428 100644
--- a/repo/content/index_blob_manager_v0.go
+++ b/repo/content/indexblob/index_blob_manager_v0.go
@@ -1,4 +1,4 @@
-package content
+package indexblob
 
 import (
 	"context"
@@ -15,18 +15,16 @@
 	"github.com/kopia/kopia/repo/logging"
 )
 
-// LegacyIndexBlobPrefix is the prefix for all legacy (v0) index blobs.
-const LegacyIndexBlobPrefix = "n"
+// V0IndexBlobPrefix is the prefix for all legacy (v0) index blobs.
+const V0IndexBlobPrefix = "n"
 
-const (
-	legacyIndexPoisonBlobID = "n00000000000000000000000000000000-repository_unreadable_by_this_kopia_version_upgrade_required"
+// V0CompactionLogBlobPrefix is the prefix for all legacy (v0) index compactions blobs.
+const V0CompactionLogBlobPrefix = "m"
 
-	defaultIndexShardSize = 16e6 // slightly less than 2^24, which lets index use 24-bit/3-byte indexes
+// V0CleanupBlobPrefix is the prefix for all legacy (v0) index cleanup blobs.
+const V0CleanupBlobPrefix = "l"
 
-	defaultEventualConsistencySettleTime = 1 * time.Hour
-	compactionLogBlobPrefix              = "m"
-	cleanupBlobPrefix                    = "l"
-)
+const defaultEventualConsistencySettleTime = 1 * time.Hour
 
 // compactionLogEntry represents contents of compaction log entry stored in `m` blob.
 type compactionLogEntry struct {
@@ -58,30 +56,53 @@ type IndexFormattingOptions interface {
 	GetMutableParameters() (format.MutableParameters, error)
 }
 
-type indexBlobManagerV0 struct {
-	st      blob.Storage
-	enc     *encryptedBlobMgr
-	timeNow func() time.Time
-	log     logging.Logger
-
+// ManagerV0 is a V0 (legacy) implementation of index blob manager.
+type ManagerV0 struct {
+	st                blob.Storage
+	enc               *EncryptionManager
+	timeNow           func() time.Time
 	formattingOptions IndexFormattingOptions
+	log               logging.Logger
+}
+
+// ListIndexBlobInfos list active blob info structs.  Also returns time of latest content deletion commit.
+func (m *ManagerV0) ListIndexBlobInfos(ctx context.Context) ([]Metadata, time.Time, error) {
+	activeIndexBlobs, t0, err := m.ListActiveIndexBlobs(ctx)
+	if err != nil {
+		return nil, time.Time{}, err
+	}
+
+	q := make([]Metadata, 0, len(activeIndexBlobs))
+
+	for _, activeIndexBlob := range activeIndexBlobs {
+		// skip the V0 blob poison that is used to prevent client reads.
+		if activeIndexBlob.BlobID == format.LegacyIndexPoisonBlobID {
+			continue
+		}
+
+		q = append(q, activeIndexBlob)
+	}
+
+	return q, t0, nil
 }
 
-func (m *indexBlobManagerV0) listActiveIndexBlobs(ctx context.Context) ([]IndexBlobInfo, time.Time, error) {
+// ListActiveIndexBlobs lists the metadata for active index blobs and returns the cut-off time
+// before which all deleted index entries should be treated as non-existent.
+func (m *ManagerV0) ListActiveIndexBlobs(ctx context.Context) ([]Metadata, time.Time, error) {
 	var compactionLogMetadata, storageIndexBlobs []blob.Metadata
 
 	var eg errgroup.Group
 
 	// list index and cleanup blobs in parallel.
 	eg.Go(func() error {
-		v, err := blob.ListAllBlobs(ctx, m.st, compactionLogBlobPrefix)
+		v, err := blob.ListAllBlobs(ctx, m.st, V0CompactionLogBlobPrefix)
 		compactionLogMetadata = v
 
 		return errors.Wrap(err, "error listing compaction blobs")
 	})
 
 	eg.Go(func() error {
-		v, err := blob.ListAllBlobs(ctx, m.st, LegacyIndexBlobPrefix)
+		v, err := blob.ListAllBlobs(ctx, m.st, V0IndexBlobPrefix)
 		storageIndexBlobs = v
 
 		return errors.Wrap(err, "error listing index blobs")
@@ -99,7 +120,7 @@ func (m *indexBlobManagerV0) listActiveIndexBlobs(ctx context.Context) ([]IndexB
 		m.log.Debugf("found-compaction-blobs[%v] %v", i, clm)
 	}
 
-	indexMap := map[blob.ID]*IndexBlobInfo{}
+	indexMap := map[blob.ID]*Metadata{}
 	addBlobsToIndex(indexMap, storageIndexBlobs)
 
 	compactionLogs, err := m.getCompactionLogEntries(ctx, compactionLogMetadata)
@@ -110,7 +131,7 @@ func (m *indexBlobManagerV0) listActiveIndexBlobs(ctx context.Context) ([]IndexB
 	// remove entries from indexMap that have been compacted and replaced by other indexes.
 	m.removeCompactedIndexes(indexMap, compactionLogs)
 
-	var results []IndexBlobInfo
+	var results []Metadata
 	for _, v := range indexMap {
 		results = append(results, *v)
 	}
@@ -122,17 +143,14 @@ func (m *indexBlobManagerV0) listActiveIndexBlobs(ctx context.Context) ([]IndexB
 	return results, time.Time{}, nil
 }
 
-func (m *indexBlobManagerV0) invalidate(ctx context.Context) {
+// Invalidate invalidates any caches.
+func (m *ManagerV0) Invalidate(ctx context.Context) {
 }
 
-func (m *indexBlobManagerV0) flushCache(ctx context.Context) {
-	if err := m.st.FlushCaches(ctx); err != nil {
-		m.log.Debugf("error flushing caches: %v", err)
-	}
-}
-
-func (m *indexBlobManagerV0) compact(ctx context.Context, opt CompactOptions) error {
-	indexBlobs, _, err := m.listActiveIndexBlobs(ctx)
+// Compact performs compaction of index blobs by merging smaller ones into larger
+// and registering compaction and cleanup blobs in the repository.
+func (m *ManagerV0) Compact(ctx context.Context, opt CompactOptions) error {
+	indexBlobs, _, err := m.ListActiveIndexBlobs(ctx)
 	if err != nil {
 		return errors.Wrap(err, "error listing active index blobs")
 	}
@@ -155,7 +173,7 @@ func (m *indexBlobManagerV0) compact(ctx context.Context, opt CompactOptions) er
 	return nil
 }
 
-func (m *indexBlobManagerV0) registerCompaction(ctx context.Context, inputs, outputs []blob.Metadata, maxEventualConsistencySettleTime time.Duration) error {
+func (m *ManagerV0) registerCompaction(ctx context.Context, inputs, outputs []blob.Metadata, maxEventualConsistencySettleTime time.Duration) error {
 	logEntryBytes, err := json.Marshal(&compactionLogEntry{
 		InputMetadata:  inputs,
 		OutputMetadata: outputs,
@@ -164,7 +182,7 @@ func (m *indexBlobManagerV0) registerCompaction(ctx context.Context, inputs, out
 		return errors.Wrap(err, "unable to marshal log entry bytes")
 	}
 
-	compactionLogBlobMetadata, err := m.enc.encryptAndWriteBlob(ctx, gather.FromSlice(logEntryBytes), compactionLogBlobPrefix, "")
+	compactionLogBlobMetadata, err := m.enc.EncryptAndWriteBlob(ctx, gather.FromSlice(logEntryBytes), V0CompactionLogBlobPrefix, "")
 	if err != nil {
 		return errors.Wrap(err, "unable to write compaction log")
 	}
@@ -186,15 +204,12 @@ func (m *indexBlobManagerV0) registerCompaction(ctx context.Context, inputs, out
 	return nil
 }
 
-func (m *indexBlobManagerV0) getIndexBlob(ctx context.Context, blobID blob.ID, output *gather.WriteBuffer) error {
-	return m.enc.getEncryptedBlob(ctx, blobID, output)
-}
-
-func (m *indexBlobManagerV0) writeIndexBlobs(ctx context.Context, dataShards []gather.Bytes, sessionID SessionID) ([]blob.Metadata, error) {
+// WriteIndexBlobs writes the provided data shards into new index blobs oprionally appending the provided suffix.
+func (m *ManagerV0) WriteIndexBlobs(ctx context.Context, dataShards []gather.Bytes, suffix blob.ID) ([]blob.Metadata, error) {
 	var result []blob.Metadata
 
 	for _, data := range dataShards {
-		bm, err := m.enc.encryptAndWriteBlob(ctx, data, LegacyIndexBlobPrefix, sessionID)
+		bm, err := m.enc.EncryptAndWriteBlob(ctx, data, V0IndexBlobPrefix, suffix)
 		if err != nil {
 			return nil, errors.Wrap(err, "error writing index blbo")
 		}
@@ -205,14 +220,14 @@ func (m *indexBlobManagerV0) writeIndexBlobs(ctx context.Context, dataShards []g
 	return result, nil
 }
 
-func (m *indexBlobManagerV0) getCompactionLogEntries(ctx context.Context, blobs []blob.Metadata) (map[blob.ID]*compactionLogEntry, error) {
+func (m *ManagerV0) getCompactionLogEntries(ctx context.Context, blobs []blob.Metadata) (map[blob.ID]*compactionLogEntry, error) {
 	results := map[blob.ID]*compactionLogEntry{}
 
 	var data gather.WriteBuffer
 	defer data.Close()
 
 	for _, cb := range blobs {
-		err := m.enc.getEncryptedBlob(ctx, cb.BlobID, &data)
+		err := m.enc.GetEncryptedBlob(ctx, cb.BlobID, &data)
 
 		if errors.Is(err, blob.ErrBlobNotFound) {
 			continue
@@ -236,7 +251,7 @@ func (m *indexBlobManagerV0) getCompactionLogEntries(ctx context.Context, blobs
 	return results, nil
 }
 
-func (m *indexBlobManagerV0) getCleanupEntries(ctx context.Context, latestServerBlobTime time.Time, blobs []blob.Metadata) (map[blob.ID]*cleanupEntry, error) {
+func (m *ManagerV0) getCleanupEntries(ctx context.Context, latestServerBlobTime time.Time, blobs []blob.Metadata) (map[blob.ID]*cleanupEntry, error) {
 	results := map[blob.ID]*cleanupEntry{}
 
 	var data gather.WriteBuffer
@@ -245,7 +260,7 @@ func (m *indexBlobManagerV0) getCleanupEntries(ctx context.Context, latestServer
 	for _, cb := range blobs {
 		data.Reset()
 
-		err := m.enc.getEncryptedBlob(ctx, cb.BlobID, &data)
+		err := m.enc.GetEncryptedBlob(ctx, cb.BlobID, &data)
 
 		if errors.Is(err, blob.ErrBlobNotFound) {
 			continue
@@ -269,8 +284,8 @@ func (m *indexBlobManagerV0) getCleanupEntries(ctx context.Context, latestServer
 	return results, nil
 }
 
-func (m *indexBlobManagerV0) deleteOldBlobs(ctx context.Context, latestBlob blob.Metadata, maxEventualConsistencySettleTime time.Duration) error {
-	allCompactionLogBlobs, err := blob.ListAllBlobs(ctx, m.st, compactionLogBlobPrefix)
+func (m *ManagerV0) deleteOldBlobs(ctx context.Context, latestBlob blob.Metadata, maxEventualConsistencySettleTime time.Duration) error {
+	allCompactionLogBlobs, err := blob.ListAllBlobs(ctx, m.st, V0CompactionLogBlobPrefix)
 	if err != nil {
 		return errors.Wrap(err, "error listing compaction log blobs")
 	}
@@ -304,7 +319,7 @@ func (m *indexBlobManagerV0) deleteOldBlobs(ctx context.Context, latestBlob blob
 	return nil
 }
 
-func (m *indexBlobManagerV0) findIndexBlobsToDelete(latestServerBlobTime time.Time, entries map[blob.ID]*compactionLogEntry, maxEventualConsistencySettleTime time.Duration) []blob.ID {
+func (m *ManagerV0) findIndexBlobsToDelete(latestServerBlobTime time.Time, entries map[blob.ID]*compactionLogEntry, maxEventualConsistencySettleTime time.Duration) []blob.ID {
 	tmp := map[blob.ID]bool{}
 
 	for _, cl := range entries {
@@ -330,7 +345,7 @@ func (m *indexBlobManagerV0) findIndexBlobsToDelete(latestServerBlobTime time.Ti
 	return result
 }
 
-func (m *indexBlobManagerV0) findCompactionLogBlobsToDelayCleanup(compactionBlobs []blob.Metadata) []blob.ID {
+func (m *ManagerV0) findCompactionLogBlobsToDelayCleanup(compactionBlobs []blob.Metadata) []blob.ID {
 	var result []blob.ID
 
 	for _, cb := range compactionBlobs {
@@ -341,7 +356,7 @@ func (m *indexBlobManagerV0) findCompactionLogBlobsToDelayCleanup(compactionBlob
 	return result
 }
 
-func (m *indexBlobManagerV0) findBlobsToDelete(entries map[blob.ID]*cleanupEntry, maxEventualConsistencySettleTime time.Duration) (compactionLogs, cleanupBlobs []blob.ID) {
+func (m *ManagerV0) findBlobsToDelete(entries map[blob.ID]*cleanupEntry, maxEventualConsistencySettleTime time.Duration) (compactionLogs, cleanupBlobs []blob.ID) {
 	for k, e := range entries {
 		if e.age >= maxEventualConsistencySettleTime {
 			compactionLogs = append(compactionLogs, e.BlobIDs...)
@@ -352,7 +367,7 @@ func (m *indexBlobManagerV0) findBlobsToDelete(entries map[blob.ID]*cleanupEntry
 	return
 }
 
-func (m *indexBlobManagerV0) delayCleanupBlobs(ctx context.Context, blobIDs []blob.ID, cleanupScheduleTime time.Time) error {
+func (m *ManagerV0) delayCleanupBlobs(ctx context.Context, blobIDs []blob.ID, cleanupScheduleTime time.Time) error {
 	if len(blobIDs) == 0 {
 		return nil
 	}
@@ -365,14 +380,14 @@ func (m *indexBlobManagerV0) delayCleanupBlobs(ctx context.Context, blobIDs []bl
 		return errors.Wrap(err, "unable to marshal cleanup log bytes")
 	}
 
-	if _, err := m.enc.encryptAndWriteBlob(ctx, gather.FromSlice(payload), cleanupBlobPrefix, ""); err != nil {
+	if _, err := m.enc.EncryptAndWriteBlob(ctx, gather.FromSlice(payload), V0CleanupBlobPrefix, ""); err != nil {
 		return errors.Wrap(err, "unable to cleanup log")
 	}
 
 	return nil
 }
 
-func (m *indexBlobManagerV0) deleteBlobsFromStorageAndCache(ctx context.Context, blobIDs []blob.ID) error {
+func (m *ManagerV0) deleteBlobsFromStorageAndCache(ctx context.Context, blobIDs []blob.ID) error {
 	for _, blobID := range blobIDs {
 		if err := m.st.DeleteBlob(ctx, blobID); err != nil && !errors.Is(err, blob.ErrBlobNotFound) {
 			m.log.Debugf("delete-blob failed %v %v", blobID, err)
@@ -385,8 +400,8 @@ func (m *indexBlobManagerV0) deleteBlobsFromStorageAndCache(ctx context.Context,
 	return nil
 }
 
-func (m *indexBlobManagerV0) cleanup(ctx context.Context, maxEventualConsistencySettleTime time.Duration) error {
-	allCleanupBlobs, err := blob.ListAllBlobs(ctx, m.st, cleanupBlobPrefix)
+func (m *ManagerV0) cleanup(ctx context.Context, maxEventualConsistencySettleTime time.Duration) error {
+	allCleanupBlobs, err := blob.ListAllBlobs(ctx, m.st, V0CleanupBlobPrefix)
 	if err != nil {
 		return errors.Wrap(err, "error listing cleanup blobs")
 	}
@@ -417,14 +432,16 @@ func (m *indexBlobManagerV0) cleanup(ctx context.Context, maxEventualConsistency
 		return errors.Wrap(err, "unable to delete cleanup blobs")
 	}
 
-	m.flushCache(ctx)
+	if err := m.st.FlushCaches(ctx); err != nil {
+		m.log.Debugw("error flushing caches", "err", err)
+	}
 
 	return nil
 }
 
-func (m *indexBlobManagerV0) getBlobsToCompact(indexBlobs []IndexBlobInfo, opt CompactOptions, mp format.MutableParameters) []IndexBlobInfo {
+func (m *ManagerV0) getBlobsToCompact(indexBlobs []Metadata, opt CompactOptions, mp format.MutableParameters) []Metadata {
 	var (
-		nonCompactedBlobs, verySmallBlobs                                              []IndexBlobInfo
+		nonCompactedBlobs, verySmallBlobs                                              []Metadata
 		totalSizeNonCompactedBlobs, totalSizeVerySmallBlobs, totalSizeMediumSizedBlobs int64
 		mediumSizedBlobCount                                                           int
 	)
@@ -448,7 +465,7 @@ func (m *indexBlobManagerV0) getBlobsToCompact(indexBlobs []IndexBlobInfo, opt C
 
 	if len(nonCompactedBlobs) < opt.MaxSmallBlobs {
 		// current count is below min allowed - nothing to do
-		m.log.Debugf("no small contents to compact")
+		m.log.Debugf("no small contents to Compact")
 		return nil
 	}
 
@@ -462,7 +479,7 @@ func (m *indexBlobManagerV0) getBlobsToCompact(indexBlobs []IndexBlobInfo, opt C
 	return nonCompactedBlobs
 }
 
-func (m *indexBlobManagerV0) compactIndexBlobs(ctx context.Context, indexBlobs []IndexBlobInfo, opt CompactOptions) error {
+func (m *ManagerV0) compactIndexBlobs(ctx context.Context, indexBlobs []Metadata, opt CompactOptions) error {
 	if len(indexBlobs) <= 1 && opt.DropDeletedBefore.IsZero() && len(opt.DropContents) == 0 {
 		return nil
 	}
@@ -490,14 +507,14 @@ func (m *indexBlobManagerV0) compactIndexBlobs(ctx context.Context, indexBlobs [
 	// we must do it after all input blobs have been merged, otherwise we may resurrect contents.
 	m.dropContentsFromBuilder(bld, opt)
 
-	dataShards, cleanupShards, err := bld.BuildShards(mp.IndexVersion, false, defaultIndexShardSize)
+	dataShards, cleanupShards, err := bld.BuildShards(mp.IndexVersion, false, DefaultIndexShardSize)
 	if err != nil {
 		return errors.Wrap(err, "unable to build an index")
 	}
 
 	defer cleanupShards()
 
-	compactedIndexBlobs, err := m.writeIndexBlobs(ctx, dataShards, "")
+	compactedIndexBlobs, err := m.WriteIndexBlobs(ctx, dataShards, "")
 	if err != nil {
 		return errors.Wrap(err, "unable to write compacted indexes")
 	}
@@ -511,7 +528,7 @@ func (m *indexBlobManagerV0) compactIndexBlobs(ctx context.Context, indexBlobs [
 	return nil
 }
 
-func (m *indexBlobManagerV0) dropContentsFromBuilder(bld index.Builder, opt CompactOptions) {
+func (m *ManagerV0) dropContentsFromBuilder(bld index.Builder, opt CompactOptions) {
 	for _, dc := range opt.DropContents {
 		if _, ok := bld[dc]; ok {
 			m.log.Debugf("manual-drop-from-index %v", dc)
@@ -533,23 +550,11 @@ func (m *indexBlobManagerV0) dropContentsFromBuilder(bld index.Builder, opt Comp
 	}
 }
 
-// WriteLegacyIndexPoisonBlob writes a "poison blob" that will prevent old kopia clients
-// that have not been upgraded from being able to open the repository after its format
-// has been upgraded.
-func WriteLegacyIndexPoisonBlob(ctx context.Context, st blob.Storage) error {
-	//nolint:wrapcheck
-	return st.PutBlob(
-		ctx,
-		legacyIndexPoisonBlobID,
-		gather.FromSlice([]byte("The format of this repository has been upgraded and cannot be read by old clients")),
-		blob.PutOptions{})
-}
-
-func addIndexBlobsToBuilder(ctx context.Context, enc *encryptedBlobMgr, bld index.Builder, indexBlobID blob.ID) error {
+func addIndexBlobsToBuilder(ctx context.Context, enc *EncryptionManager, bld index.Builder, indexBlobID blob.ID) error {
 	var data gather.WriteBuffer
 	defer data.Close()
 
-	err := enc.getEncryptedBlob(ctx, indexBlobID, &data)
+	err := enc.GetEncryptedBlob(ctx, indexBlobID, &data)
 	if err != nil {
 		return errors.Wrapf(err, "error getting index %q", indexBlobID)
 	}
@@ -559,7 +564,7 @@ func addIndexBlobsToBuilder(ctx context.Context, enc *encryptedBlobMgr, bld inde
 		return errors.Wrapf(err, "unable to open index blob %q", indexBlobID)
 	}
 
-	_ = ndx.Iterate(index.AllIDs, func(i Info) error {
+	_ = ndx.Iterate(index.AllIDs, func(i index.Info) error {
 		bld.Add(i)
 		return nil
 	})
@@ -579,7 +584,7 @@ func blobsOlderThan(m []blob.Metadata, cutoffTime time.Time) []blob.Metadata {
 	return res
 }
 
-func (m *indexBlobManagerV0) removeCompactedIndexes(bimap map[blob.ID]*IndexBlobInfo, compactionLogs map[blob.ID]*compactionLogEntry) {
+func (m *ManagerV0) removeCompactedIndexes(bimap map[blob.ID]*Metadata, compactionLogs map[blob.ID]*compactionLogEntry) {
 	var validCompactionLogs []*compactionLogEntry
 
 	for _, cl := range compactionLogs {
@@ -612,3 +617,16 @@ func (m *indexBlobManagerV0) removeCompactedIndexes(bimap map[blob.ID]*IndexBlob
 		}
 	}
 }
+
+// NewManagerV0 creates new instance of ManagerV0 with all required parameters set.
+func NewManagerV0(
+	st blob.Storage,
+	enc *EncryptionManager,
+	timeNow func() time.Time,
+	formattingOptions IndexFormattingOptions,
+	log logging.Logger,
+) *ManagerV0 {
+	return &ManagerV0{st, enc, timeNow, formattingOptions, log}
+}
+
+var _ Manager = (*ManagerV0)(nil)
diff --git a/repo/content/indexblob/index_blob_manager_v1.go b/repo/content/indexblob/index_blob_manager_v1.go
new file mode 100644
index 00000000..31d0518e
--- /dev/null
+++ b/repo/content/indexblob/index_blob_manager_v1.go
@@ -0,0 +1,189 @@
+package indexblob
+
+import (
+	"context"
+	"crypto/rand"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"github.com/kopia/kopia/internal/blobcrypto"
+	"github.com/kopia/kopia/internal/epoch"
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/logging"
+)
+
+// ManagerV1 is the append-only implementation of indexblob.Manager
+// based on epoch manager.
+type ManagerV1 struct {
+	st                blob.Storage
+	enc               *EncryptionManager
+	timeNow           func() time.Time
+	formattingOptions IndexFormattingOptions
+	log               logging.Logger
+
+	EpochMgr *epoch.Manager
+}
+
+// ListIndexBlobInfos list active blob info structs.  Also returns time of latest content deletion commit.
+func (m *ManagerV1) ListIndexBlobInfos(ctx context.Context) ([]Metadata, time.Time, error) {
+	return m.ListActiveIndexBlobs(ctx)
+}
+
+// ListActiveIndexBlobs lists the metadata for active index blobs and returns the cut-off time
+// before which all deleted index entries should be treated as non-existent.
+func (m *ManagerV1) ListActiveIndexBlobs(ctx context.Context) ([]Metadata, time.Time, error) {
+	active, deletionWatermark, err := m.EpochMgr.GetCompleteIndexSet(ctx, epoch.LatestEpoch)
+	if err != nil {
+		return nil, time.Time{}, errors.Wrap(err, "error getting index set")
+	}
+
+	var result []Metadata
+
+	for _, bm := range active {
+		result = append(result, Metadata{Metadata: bm})
+	}
+
+	m.log.Errorf("active indexes %v deletion watermark %v", blob.IDsFromMetadata(active), deletionWatermark)
+
+	return result, deletionWatermark, nil
+}
+
+// Invalidate clears any read caches.
+func (m *ManagerV1) Invalidate(ctx context.Context) {
+	m.EpochMgr.Invalidate()
+}
+
+// Compact advances the deletion watermark.
+func (m *ManagerV1) Compact(ctx context.Context, opt CompactOptions) error {
+	if opt.DropDeletedBefore.IsZero() {
+		return nil
+	}
+
+	return errors.Wrap(m.EpochMgr.AdvanceDeletionWatermark(ctx, opt.DropDeletedBefore), "error advancing deletion watermark")
+}
+
+// CompactEpoch compacts the provided index blobs and writes a new set of blobs.
+func (m *ManagerV1) CompactEpoch(ctx context.Context, blobIDs []blob.ID, outputPrefix blob.ID) error {
+	tmpbld := make(index.Builder)
+
+	for _, indexBlob := range blobIDs {
+		if err := addIndexBlobsToBuilder(ctx, m.enc, tmpbld, indexBlob); err != nil {
+			return errors.Wrap(err, "error adding index to builder")
+		}
+	}
+
+	mp, mperr := m.formattingOptions.GetMutableParameters()
+	if mperr != nil {
+		return errors.Wrap(mperr, "mutable parameters")
+	}
+
+	dataShards, cleanupShards, err := tmpbld.BuildShards(mp.IndexVersion, true, DefaultIndexShardSize)
+	if err != nil {
+		return errors.Wrap(err, "unable to build index dataShards")
+	}
+
+	defer cleanupShards()
+
+	var rnd [8]byte
+
+	if _, err := rand.Read(rnd[:]); err != nil {
+		return errors.Wrap(err, "error getting random session ID")
+	}
+
+	sessionID := fmt.Sprintf("s%x-c%v", rnd[:], len(dataShards))
+
+	var data2 gather.WriteBuffer
+	defer data2.Close()
+
+	for _, data := range dataShards {
+		data2.Reset()
+
+		blobID, err := blobcrypto.Encrypt(m.enc.crypter, data, outputPrefix, blob.ID(sessionID), &data2)
+		if err != nil {
+			return errors.Wrap(err, "error encrypting")
+		}
+
+		if err := m.st.PutBlob(ctx, blobID, data2.Bytes(), blob.PutOptions{}); err != nil {
+			return errors.Wrap(err, "error writing index blob")
+		}
+	}
+
+	return nil
+}
+
+// WriteIndexBlobs writes the provided data shards into new index blobs oprionally appending the provided suffix.
+// The writes are atomic in the sense that if any of them fails, the reader will
+// ignore all of the indexes that share the same suffix.
+func (m *ManagerV1) WriteIndexBlobs(ctx context.Context, dataShards []gather.Bytes, suffix blob.ID) ([]blob.Metadata, error) {
+	shards := map[blob.ID]blob.Bytes{}
+
+	suffix = blob.ID(fmt.Sprintf("%v-c%v", suffix, len(dataShards)))
+
+	for _, data := range dataShards {
+		// important - we're intentionally using data2 in the inner loop scheduling multiple Close()
+		// we want all Close() to be called at the end of the function after WriteIndex()
+		data2 := gather.NewWriteBuffer()
+		defer data2.Close() //nolint:gocritic
+
+		unprefixedBlobID, err := blobcrypto.Encrypt(m.enc.crypter, data, "", suffix, data2)
+		if err != nil {
+			return nil, errors.Wrap(err, "error encrypting")
+		}
+
+		shards[unprefixedBlobID] = data2.Bytes()
+	}
+
+	//nolint:wrapcheck
+	return m.EpochMgr.WriteIndex(ctx, shards)
+}
+
+// EpochManager returns the epoch manager.
+func (m *ManagerV1) EpochManager() *epoch.Manager {
+	return m.EpochMgr
+}
+
+// PrepareUpgradeToIndexBlobManagerV1 prepares the repository for migrating to IndexBlobManagerV1.
+func (m *ManagerV1) PrepareUpgradeToIndexBlobManagerV1(ctx context.Context, params epoch.Parameters, v0 *ManagerV0) error {
+	ibl, _, err := v0.ListActiveIndexBlobs(ctx)
+	if err != nil {
+		return errors.Wrap(err, "error listing active index blobs")
+	}
+
+	var blobIDs []blob.ID
+
+	for _, ib := range ibl {
+		blobIDs = append(blobIDs, ib.BlobID)
+	}
+
+	if err := m.CompactEpoch(ctx, blobIDs, epoch.UncompactedEpochBlobPrefix(epoch.FirstEpoch)); err != nil {
+		return errors.Wrap(err, "unable to generate initial epoch")
+	}
+
+	return nil
+}
+
+// NewManagerV1 creates new instance of ManagerV1 with all required parameters set.
+func NewManagerV1(
+	st blob.Storage,
+	enc *EncryptionManager,
+	epochMgr *epoch.Manager,
+	timeNow func() time.Time,
+	formattingOptions IndexFormattingOptions,
+	log logging.Logger,
+) *ManagerV1 {
+	return &ManagerV1{
+		st:                st,
+		enc:               enc,
+		timeNow:           timeNow,
+		log:               log,
+		formattingOptions: formattingOptions,
+
+		EpochMgr: epochMgr,
+	}
+}
+
+var _ Manager = (*ManagerV1)(nil)
diff --git a/repo/content/internal_logger.go b/repo/content/internal_logger.go
deleted file mode 100644
index 15c171fd..00000000
--- a/repo/content/internal_logger.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package content
-
-import (
-	"compress/gzip"
-	"context"
-	"crypto/rand"
-	"fmt"
-	"io"
-	"sync"
-	"sync/atomic"
-	"time"
-
-	"go.uber.org/zap"
-	"go.uber.org/zap/zapcore"
-
-	"github.com/kopia/kopia/internal/clock"
-	"github.com/kopia/kopia/internal/gather"
-	"github.com/kopia/kopia/internal/zaplogutil"
-	"github.com/kopia/kopia/repo/blob"
-)
-
-const blobLoggerFlushThreshold = 4 << 20
-
-// TextLogBlobPrefix is a prefix given to text logs stored in repositor.
-const TextLogBlobPrefix = "_log_"
-
-type internalLogManager struct {
-	enabled atomic.Bool // set by enable(), logger is ineffective until called
-
-	// internalLogManager implements io.Writer and we must be able to write to the
-	// repository asynchronously when the context is not provided.
-	ctx context.Context //nolint:containedctx
-
-	st             blob.Storage
-	bc             crypter
-	wg             sync.WaitGroup
-	timeFunc       func() time.Time
-	flushThreshold int
-}
-
-// Close closes the log manager.
-func (m *internalLogManager) Close(ctx context.Context) {
-	m.wg.Wait()
-}
-
-func (m *internalLogManager) encryptAndWriteLogBlob(prefix blob.ID, data gather.Bytes, closeFunc func()) {
-	encrypted := gather.NewWriteBuffer()
-	// Close happens in a goroutine
-
-	blobID, err := EncryptBLOB(m.bc, data, prefix, "", encrypted)
-	if err != nil {
-		encrypted.Close()
-
-		// this should not happen, also nothing can be done about this, we're not in a place where we can return error, log it.
-		return
-	}
-
-	b := encrypted.Bytes()
-
-	m.wg.Add(1)
-
-	go func() {
-		defer m.wg.Done()
-		defer encrypted.Close()
-		defer closeFunc()
-
-		if err := m.st.PutBlob(m.ctx, blobID, b, blob.PutOptions{}); err != nil {
-			// nothing can be done about this, we're not in a place where we can return error, log it.
-			return
-		}
-	}()
-}
-
-// NewLogger creates new logger.
-func (m *internalLogManager) NewLogger() *zap.SugaredLogger {
-	var rnd [2]byte
-
-	rand.Read(rnd[:]) //nolint:errcheck
-
-	w := &internalLogger{
-		m:      m,
-		prefix: blob.ID(fmt.Sprintf("%v%v_%x", TextLogBlobPrefix, clock.Now().Local().Format("20060102150405"), rnd)),
-	}
-
-	return zap.New(zapcore.NewCore(
-		zaplogutil.NewStdConsoleEncoder(zaplogutil.StdConsoleEncoderConfig{
-			TimeLayout: zaplogutil.PreciseLayout,
-			LocalTime:  false,
-		}),
-		w, zap.DebugLevel), zap.WithClock(zaplogutil.Clock())).Sugar()
-}
-
-// internalLogger represents a single log session that saves log files as blobs in the repository.
-// The logger starts disabled and to actually persist logs enable() must be called.
-type internalLogger struct {
-	nextChunkNumber atomic.Int32
-
-	m  *internalLogManager
-	mu sync.Mutex
-
-	// +checklocks:mu
-	buf *gather.WriteBuffer
-	// +checklocks:mu
-	gzw *gzip.Writer
-
-	// +checklocks:mu
-	startTime int64 // unix timestamp of the first log
-
-	prefix blob.ID // +checklocksignore
-}
-
-func (m *internalLogManager) enable() {
-	if m == nil {
-		return
-	}
-
-	m.enabled.Store(true)
-}
-
-func (l *internalLogger) Write(b []byte) (int, error) {
-	l.maybeEncryptAndWriteChunkUnlocked(l.addAndMaybeFlush(b))
-	return len(b), nil
-}
-
-func (l *internalLogger) maybeEncryptAndWriteChunkUnlocked(data gather.Bytes, closeFunc func()) {
-	if data.Length() == 0 {
-		closeFunc()
-		return
-	}
-
-	if !l.m.enabled.Load() {
-		closeFunc()
-		return
-	}
-
-	endTime := l.m.timeFunc().Unix()
-
-	l.mu.Lock()
-	st := l.startTime
-	l.mu.Unlock()
-
-	prefix := blob.ID(fmt.Sprintf("%v_%v_%v_%v_", l.prefix, st, endTime, l.nextChunkNumber.Add(1)))
-
-	l.m.encryptAndWriteLogBlob(prefix, data, closeFunc)
-}
-
-func (l *internalLogger) addAndMaybeFlush(b []byte) (payload gather.Bytes, closeFunc func()) {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-
-	w := l.ensureWriterInitializedLocked()
-
-	_, err := w.Write(b)
-	l.logUnexpectedError(err)
-
-	if l.buf.Length() < l.m.flushThreshold {
-		return gather.Bytes{}, func() {}
-	}
-
-	return l.flushAndResetLocked()
-}
-
-// +checklocks:l.mu
-func (l *internalLogger) ensureWriterInitializedLocked() io.Writer {
-	if l.gzw == nil {
-		l.buf = gather.NewWriteBuffer()
-		l.gzw = gzip.NewWriter(l.buf)
-		l.startTime = l.m.timeFunc().Unix()
-	}
-
-	return l.gzw
-}
-
-// +checklocks:l.mu
-func (l *internalLogger) flushAndResetLocked() (payload gather.Bytes, closeFunc func()) {
-	if l.gzw == nil {
-		return gather.Bytes{}, func() {}
-	}
-
-	l.logUnexpectedError(l.gzw.Flush())
-	l.logUnexpectedError(l.gzw.Close())
-
-	closeBuf := l.buf.Close
-	res := l.buf.Bytes()
-
-	l.buf = nil
-	l.gzw = nil
-
-	return res, closeBuf
-}
-
-func (l *internalLogger) logUnexpectedError(err error) {
-	if err == nil {
-		return
-	}
-}
-
-func (l *internalLogger) Sync() error {
-	l.mu.Lock()
-	data, closeFunc := l.flushAndResetLocked()
-	l.mu.Unlock()
-
-	l.maybeEncryptAndWriteChunkUnlocked(data, closeFunc)
-
-	return nil
-}
-
-// newInternalLogManager creates a new blobLogManager that will emit logs as repository blobs with a given prefix.
-func newInternalLogManager(ctx context.Context, st blob.Storage, bc crypter) *internalLogManager {
-	return &internalLogManager{
-		ctx:            ctx,
-		st:             st,
-		bc:             bc,
-		flushThreshold: blobLoggerFlushThreshold,
-		timeFunc:       clock.Now,
-	}
-}
diff --git a/repo/content/sessions.go b/repo/content/sessions.go
index fbc63507..3f00a902 100644
--- a/repo/content/sessions.go
+++ b/repo/content/sessions.go
@@ -10,6 +10,7 @@
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/blobcrypto"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/repo/blob"
 )
@@ -111,7 +112,7 @@ func (bm *WriteManager) writeSessionMarkerLocked(ctx context.Context) error {
 	var encrypted gather.WriteBuffer
 	defer encrypted.Close()
 
-	sessionBlobID, err := EncryptBLOB(bm.format, gather.FromSlice(js), BlobIDPrefixSession, bm.currentSessionInfo.ID, &encrypted)
+	sessionBlobID, err := blobcrypto.Encrypt(bm.format, gather.FromSlice(js), BlobIDPrefixSession, blob.ID(bm.currentSessionInfo.ID), &encrypted)
 	if err != nil {
 		return errors.Wrap(err, "unable to encrypt session marker")
 	}
@@ -178,7 +179,7 @@ func (bm *WriteManager) ListActiveSessions(ctx context.Context) (map[SessionID]*
 			return nil, errors.Wrapf(err, "error loading session: %v", b.BlobID)
 		}
 
-		err = DecryptBLOB(bm.format, payload.Bytes(), b.BlobID, &decrypted)
+		err = blobcrypto.Decrypt(bm.format, payload.Bytes(), b.BlobID, &decrypted)
 		if err != nil {
 			return nil, errors.Wrapf(err, "error decrypting session: %v", b.BlobID)
 		}
diff --git a/repo/format/upgrade_lock.go b/repo/format/upgrade_lock.go
index faee00bb..85712f95 100644
--- a/repo/format/upgrade_lock.go
+++ b/repo/format/upgrade_lock.go
@@ -9,10 +9,15 @@
 	"github.com/kopia/kopia/repo/blob"
 )
 
-// BackupBlobIDPrefix is the prefix for all identifiers of the BLOBs that
-// keep a backup copy of the FormatBlobID BLOB for the purposes of rollback
-// during upgrade.
-const BackupBlobIDPrefix = "kopia.repository.backup."
+const (
+	// BackupBlobIDPrefix is the prefix for all identifiers of the BLOBs that
+	// keep a backup copy of the FormatBlobID BLOB for the purposes of rollback
+	// during upgrade.
+	BackupBlobIDPrefix = "kopia.repository.backup."
+
+	// LegacyIndexPoisonBlobID used to pollute V0 indexes after upgrade to prevent legacy clients from corrupting V1 indexes.
+	LegacyIndexPoisonBlobID = "n00000000000000000000000000000000-repository_unreadable_by_this_kopia_version_upgrade_required"
+)
 
 // BackupBlobID gets the upgrade backu pblob-id fro mthe lock.
 func BackupBlobID(l UpgradeLockIntent) blob.ID {
@@ -24,7 +29,7 @@ func BackupBlobID(l UpgradeLockIntent) blob.ID {
 // it updates the existing lock using the output of the UpgradeLock.Update().
 //
 // This method also backs up the original format version on the upgrade lock
-// intent and sets the latest format-version o nthe repository blob. This
+// intent and sets the latest format-version to the repository blob. This
 // should cause the unsupporting clients (non-upgrade capable) to fail
 // connecting to the repository.
 func (m *Manager) SetUpgradeLockIntent(ctx context.Context, l UpgradeLockIntent) (*UpgradeLockIntent, error) {
@@ -53,7 +58,7 @@ func (m *Manager) SetUpgradeLockIntent(ctx context.Context, l UpgradeLockIntent)
 			return nil, errors.Wrap(err, "failed to backup the repo format blob")
 		}
 
-		// set a new lock or revoke an existing lock
+		// set a new lock or revoke an existing lock.
 		m.repoConfig.UpgradeLock = &l
 		// mark the upgrade to the new format version, this will ensure that older
 		// clients won't be able to parse the new version
@@ -71,6 +76,18 @@ func (m *Manager) SetUpgradeLockIntent(ctx context.Context, l UpgradeLockIntent)
 	return m.repoConfig.UpgradeLock.Clone(), nil
 }
 
+// WriteLegacyIndexPoisonBlob writes a "poison blob" that will prevent old kopia clients
+// that have not been upgraded from being able to open the repository after its format
+// has been upgraded.
+func WriteLegacyIndexPoisonBlob(ctx context.Context, st blob.Storage) error {
+	//nolint:wrapcheck
+	return st.PutBlob(
+		ctx,
+		LegacyIndexPoisonBlobID,
+		gather.FromSlice([]byte("The format of this repository has been upgraded and cannot be read by old clients")),
+		blob.PutOptions{})
+}
+
 // CommitUpgrade removes the upgrade lock from the from the repository format
 // blob. This in-effect commits the new repository format t othe repository and
 // resumes all access to the repository.
@@ -86,6 +103,11 @@ func (m *Manager) CommitUpgrade(ctx context.Context) error {
 		return errors.New("no upgrade in progress")
 	}
 
+	// poison V0 index so that old readers won't be able to open it.
+	if err := WriteLegacyIndexPoisonBlob(ctx, m.blobs); err != nil {
+		log(ctx).Errorf("unable to write legacy index poison blob: %v", err)
+	}
+
 	// restore the old format version
 	m.repoConfig.UpgradeLock = nil
 
diff --git a/repo/maintenance/drop_deleted_contents.go b/repo/maintenance/drop_deleted_contents.go
index 90a4195b..9bb26ae9 100644
--- a/repo/maintenance/drop_deleted_contents.go
+++ b/repo/maintenance/drop_deleted_contents.go
@@ -5,7 +5,7 @@
 	"time"
 
 	"github.com/kopia/kopia/repo"
-	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 // DropDeletedContents rewrites indexes while dropping deleted contents above certain age.
@@ -13,7 +13,7 @@ func DropDeletedContents(ctx context.Context, rep repo.DirectRepositoryWriter, d
 	log(ctx).Infof("Dropping contents deleted before %v", dropDeletedBefore)
 
 	//nolint:wrapcheck
-	return rep.ContentManager().CompactIndexes(ctx, content.CompactOptions{
+	return rep.ContentManager().CompactIndexes(ctx, indexblob.CompactOptions{
 		AllIndexes:                       true,
 		DropDeletedBefore:                dropDeletedBefore,
 		DisableEventualConsistencySafety: safety.DisableEventualConsistencySafety,
diff --git a/repo/maintenance/index_compaction.go b/repo/maintenance/index_compaction.go
index 6e105f2a..47ec71a6 100644
--- a/repo/maintenance/index_compaction.go
+++ b/repo/maintenance/index_compaction.go
@@ -3,7 +3,7 @@
 import (
 	"context"
 
-	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 // runTaskIndexCompactionQuick rewrites index blobs to reduce their count but does not drop any contents.
@@ -14,7 +14,7 @@ func runTaskIndexCompactionQuick(ctx context.Context, runParams RunParameters, s
 		const maxSmallBlobsForIndexCompaction = 8
 
 		//nolint:wrapcheck
-		return runParams.rep.ContentManager().CompactIndexes(ctx, content.CompactOptions{
+		return runParams.rep.ContentManager().CompactIndexes(ctx, indexblob.CompactOptions{
 			MaxSmallBlobs:                    maxSmallBlobsForIndexCompaction,
 			DisableEventualConsistencySafety: safety.DisableEventualConsistencySafety,
 		})
diff --git a/repo/open.go b/repo/open.go
index f691a7ee..d262662d 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -25,6 +25,7 @@
 	"github.com/kopia/kopia/repo/blob/storagemetrics"
 	"github.com/kopia/kopia/repo/blob/throttling"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/logging"
 	"github.com/kopia/kopia/repo/manifest"
@@ -386,7 +387,7 @@ func wrapLockingStorage(st blob.Storage, r format.BlobStorageConfiguration) blob
 		prefixes = append(prefixes, string(prefix))
 	}
 
-	prefixes = append(prefixes, content.LegacyIndexBlobPrefix, epoch.EpochManagerIndexUberPrefix, format.KopiaRepositoryBlobID,
+	prefixes = append(prefixes, indexblob.V0IndexBlobPrefix, epoch.EpochManagerIndexUberPrefix, format.KopiaRepositoryBlobID,
 		format.KopiaBlobCfgBlobID)
 
 	return beforeop.NewWrapper(st, nil, nil, nil, func(ctx context.Context, id blob.ID, opts *blob.PutOptions) error {
diff --git a/repo/repository.go b/repo/repository.go
index 77e1d2e2..3f3a7e42 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -14,6 +14,7 @@
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/throttling"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/manifest"
 	"github.com/kopia/kopia/repo/object"
@@ -64,7 +65,7 @@ type DirectRepository interface {
 	BlobReader() blob.Reader
 	BlobVolume() blob.Volume
 	ContentReader() content.Reader
-	IndexBlobs(ctx context.Context, includeInactive bool) ([]content.IndexBlobInfo, error)
+	IndexBlobs(ctx context.Context, includeInactive bool) ([]indexblob.Metadata, error)
 	NewDirectWriter(ctx context.Context, opt WriteSessionOptions) (context.Context, DirectRepositoryWriter, error)
 	AlsoLogToContentLog(ctx context.Context) context.Context
 	UniqueID() []byte
@@ -344,7 +345,7 @@ func (r *directRepository) ContentReader() content.Reader {
 }
 
 // IndexBlobs returns the index blobs in use.
-func (r *directRepository) IndexBlobs(ctx context.Context, includeInactive bool) ([]content.IndexBlobInfo, error) {
+func (r *directRepository) IndexBlobs(ctx context.Context, includeInactive bool) ([]indexblob.Metadata, error) {
 	//nolint:wrapcheck
 	return r.cmgr.IndexBlobs(ctx, includeInactive)
 }


Test patch:
diff --git a/cli/command_repository_upgrade_test.go b/cli/command_repository_upgrade_test.go
index 20ccda71..cd19bb6f 100644
--- a/cli/command_repository_upgrade_test.go
+++ b/cli/command_repository_upgrade_test.go
@@ -1,11 +1,14 @@
 package cli_test
 
 import (
+	"fmt"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/kopia/kopia/cli"
+	"github.com/kopia/kopia/repo/content/index"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/tests/testenv"
 )
@@ -52,6 +55,144 @@ func (s *formatSpecificTestSuite) TestRepositoryUpgrade(t *testing.T) {
 	require.Contains(t, out, "Format version:      3")
 }
 
+func (s *formatSpecificTestSuite) TestRepositoryCorruptedUpgrade(t *testing.T) {
+	env := testenv.NewCLITest(t, s.formatFlags, testenv.NewInProcRunner(t))
+
+	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)
+	out := env.RunAndExpectSuccess(t, "repository", "status", "--upgrade-no-block")
+
+	env.Environment["KOPIA_UPGRADE_LOCK_ENABLED"] = "1"
+
+	switch s.formatVersion {
+	case format.FormatVersion1:
+		require.Contains(t, out, "Format version:      1")
+		// run upgrade first with commit-mode set to never.  this leaves the lock and new index intact so that
+		// the file can be corrupted with "TweakFile".
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--upgrade-owner-id", "owner",
+			"--commit-mode", "never",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have been upgraded.")
+		require.Contains(t, stderr, "Commit mode is set to 'never'.  Skipping commit.")
+		require.Contains(t, stderr, "index validation succeeded")
+		env.TweakFile(t, env.RepoDir, "x*/*/*.f")
+		// then re-run the upgrade with the corrupted index.  This should fail on index validation.
+		_, stderr = env.RunAndExpectFailure(t, "repository", "upgrade",
+			"--upgrade-owner-id", "owner")
+		require.Regexp(t, "failed to load index entries for new index: failed to load index blob with BlobID", stderr)
+	case format.FormatVersion2:
+		require.Contains(t, out, "Format version:      2")
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have already been migrated to the epoch format, no need to drain other clients")
+	default:
+		require.Contains(t, out, "Format version:      3")
+		env.RunAndExpectFailure(t, "repository", "upgrade",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+	}
+}
+
+func (s *formatSpecificTestSuite) TestRepositoryUpgradeCommitNever(t *testing.T) {
+	env := testenv.NewCLITest(t, s.formatFlags, testenv.NewInProcRunner(t))
+
+	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)
+	stdout := env.RunAndExpectSuccess(t, "repository", "status", "--upgrade-no-block")
+
+	env.Environment["KOPIA_UPGRADE_LOCK_ENABLED"] = "1"
+
+	switch s.formatVersion {
+	case format.FormatVersion1:
+		require.Contains(t, stdout, "Format version:      1")
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--commit-mode", "never",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have been upgraded.")
+		require.Contains(t, stderr, "Commit mode is set to 'never'.  Skipping commit.")
+
+		_, stderr = env.RunAndExpectFailure(t, "repository", "status", "--upgrade-no-block")
+		require.Contains(t, stderr, "failed to open repository: repository upgrade in progress")
+	case format.FormatVersion2:
+		require.Contains(t, stdout, "Format version:      2")
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--commit-mode", "never",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have already been migrated to the epoch format, no need to drain other clients")
+		require.Contains(t, stderr, "Commit mode is set to 'never'.  Skipping commit.")
+
+		_, stderr = env.RunAndExpectFailure(t, "repository", "status", "--upgrade-no-block")
+		require.Contains(t, stderr, "failed to open repository: repository upgrade in progress")
+	default:
+		require.Contains(t, stdout, "Format version:      3")
+		env.RunAndExpectFailure(t, "repository", "upgrade",
+			"--commit-mode", "never",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+
+		env.RunAndExpectSuccess(t, "repository", "status", "--upgrade-no-block")
+	}
+}
+
+func (s *formatSpecificTestSuite) TestRepositoryUpgradeCommitAlways(t *testing.T) {
+	env := testenv.NewCLITest(t, s.formatFlags, testenv.NewInProcRunner(t))
+
+	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)
+	out := env.RunAndExpectSuccess(t, "repository", "status", "--upgrade-no-block")
+
+	env.Environment["KOPIA_UPGRADE_LOCK_ENABLED"] = "1"
+
+	switch s.formatVersion {
+	case format.FormatVersion1:
+		require.Contains(t, out, "Format version:      1")
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--commit-mode", "always",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have been upgraded.")
+		require.Contains(t, stderr, "Repository has been successfully upgraded.")
+	case format.FormatVersion2:
+		require.Contains(t, out, "Format version:      2")
+		_, stderr := env.RunAndExpectSuccessWithErrOut(t, "repository", "upgrade",
+			"--commit-mode", "always",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+		require.Contains(t, stderr, "Repository indices have already been migrated to the epoch format, no need to drain other clients")
+		require.Contains(t, stderr, "Repository has been successfully upgraded.")
+	default:
+		require.Contains(t, out, "Format version:      3")
+		env.RunAndExpectFailure(t, "repository", "upgrade",
+			"--commit-mode", "always",
+			"--upgrade-owner-id", "owner",
+			"--io-drain-timeout", "1s", "--allow-unsafe-upgrade",
+			"--status-poll-interval", "1s",
+			"--max-permitted-clock-drift", "1s")
+	}
+
+	out = env.RunAndExpectSuccess(t, "repository", "status", "--upgrade-no-block")
+	require.Contains(t, out, "Epoch Manager:       enabled")
+	require.Contains(t, out, "Index Format:        v2")
+	require.Contains(t, out, "Format version:      3")
+}
+
 func lockRepositoryForUpgrade(t *testing.T, env *testenv.CLITest) {
 	t.Helper()
 
@@ -145,3 +286,135 @@ func (s *formatSpecificTestSuite) TestRepositoryUpgradeStatusWhileLocked(t *test
 	require.Contains(t, out, "Index Format:        v2")
 	require.Contains(t, out, "Format version:      3")
 }
+
+func TestRepositoryUpgrade_checkIndexInfo(t *testing.T) {
+	tcs := []struct {
+		indexInfo0   index.Info
+		indexInfo1   index.Info
+		expectRegexs []string
+	}{
+		{
+			indexInfo0:   &index.InfoStruct{PackBlobID: "a"},
+			indexInfo1:   &index.InfoStruct{PackBlobID: "a"},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{PackBlobID: "a"},
+			indexInfo1: &index.InfoStruct{PackBlobID: "b"},
+			expectRegexs: []string{
+				`do not match: "a", "b".*PackBlobID`,
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{TimestampSeconds: 1},
+			indexInfo1:   &index.InfoStruct{TimestampSeconds: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{TimestampSeconds: 1},
+			indexInfo1: &index.InfoStruct{TimestampSeconds: 2},
+			expectRegexs: []string{
+				"do not match.*TimestampSeconds",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{OriginalLength: 1},
+			indexInfo1:   &index.InfoStruct{OriginalLength: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{OriginalLength: 1},
+			indexInfo1: &index.InfoStruct{OriginalLength: 2},
+			expectRegexs: []string{
+				"do not match.*OriginalLength",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{PackedLength: 1},
+			indexInfo1:   &index.InfoStruct{PackedLength: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{PackedLength: 1},
+			indexInfo1: &index.InfoStruct{PackedLength: 2},
+			expectRegexs: []string{
+				"do not match.*PackedLength",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{PackOffset: 1},
+			indexInfo1:   &index.InfoStruct{PackOffset: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{PackOffset: 1},
+			indexInfo1: &index.InfoStruct{PackOffset: 2},
+			expectRegexs: []string{
+				"do not match.*PackOffset",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{Deleted: true},
+			indexInfo1:   &index.InfoStruct{Deleted: true},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{Deleted: false},
+			indexInfo1: &index.InfoStruct{Deleted: true},
+			expectRegexs: []string{
+				"do not match.*Deleted",
+			},
+		},
+		// simple logic error can make result of this false... so check
+		{
+			indexInfo0:   &index.InfoStruct{Deleted: false},
+			indexInfo1:   &index.InfoStruct{Deleted: false},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{FormatVersion: 1},
+			indexInfo1:   &index.InfoStruct{FormatVersion: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{FormatVersion: 1},
+			indexInfo1: &index.InfoStruct{FormatVersion: 2},
+			expectRegexs: []string{
+				"do not match.*FormatVersion",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{CompressionHeaderID: 1},
+			indexInfo1:   &index.InfoStruct{CompressionHeaderID: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{CompressionHeaderID: 1},
+			indexInfo1: &index.InfoStruct{CompressionHeaderID: 2},
+			expectRegexs: []string{
+				"do not match.*CompressionHeaderID",
+			},
+		},
+		{
+			indexInfo0:   &index.InfoStruct{EncryptionKeyID: 1},
+			indexInfo1:   &index.InfoStruct{EncryptionKeyID: 1},
+			expectRegexs: []string{},
+		},
+		{
+			indexInfo0: &index.InfoStruct{EncryptionKeyID: 1},
+			indexInfo1: &index.InfoStruct{EncryptionKeyID: 2},
+			expectRegexs: []string{
+				"do not match.*EncryptionKeyID",
+			},
+		},
+	}
+	for i, tc := range tcs {
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			report := cli.CheckIndexInfo(tc.indexInfo0, tc.indexInfo1)
+			require.Equal(t, len(report), len(tc.expectRegexs), "unexpected report length")
+			for i := range tc.expectRegexs {
+				require.Regexp(t, tc.expectRegexs[i], report[i], "report does not match regular expression")
+			}
+		})
+	}
+}
diff --git a/repo/content/blob_crypto_test.go b/internal/blobcrypto/blob_crypto_test.go
similarity index 64%
rename from repo/content/blob_crypto_test.go
rename to internal/blobcrypto/blob_crypto_test.go
index bccb3c57..a6204c0c 100644
--- a/repo/content/blob_crypto_test.go
+++ b/internal/blobcrypto/blob_crypto_test.go
@@ -1,4 +1,4 @@
-package content
+package blobcrypto
 
 import (
 	"strings"
@@ -30,31 +30,31 @@ func TestBlobCrypto(t *testing.T) {
 	defer tmp2.Close()
 	defer tmp3.Close()
 
-	id, err := EncryptBLOB(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
+	id, err := Encrypt(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
 	require.NoError(t, err)
 
-	id2, err := EncryptBLOB(cr, gather.FromSlice([]byte{1, 2, 4}), "n", "mysessionid", &tmp2)
+	id2, err := Encrypt(cr, gather.FromSlice([]byte{1, 2, 4}), "n", "mysessionid", &tmp2)
 	require.NoError(t, err)
 
 	require.NotEqual(t, id, id2)
 
-	require.NoError(t, DecryptBLOB(cr, tmp.Bytes(), id, &tmp3))
+	require.NoError(t, Decrypt(cr, tmp.Bytes(), id, &tmp3))
 	require.Equal(t, []byte{1, 2, 3}, tmp3.ToByteSlice())
-	require.NoError(t, DecryptBLOB(cr, tmp2.Bytes(), id2, &tmp3))
+	require.NoError(t, Decrypt(cr, tmp2.Bytes(), id2, &tmp3))
 	require.Equal(t, []byte{1, 2, 4}, tmp3.ToByteSlice())
 
 	// decrypting using invalid ID fails
-	require.Error(t, DecryptBLOB(cr, tmp.Bytes(), id2, &tmp3))
-	require.Error(t, DecryptBLOB(cr, tmp2.Bytes(), id, &tmp3))
+	require.Error(t, Decrypt(cr, tmp.Bytes(), id2, &tmp3))
+	require.Error(t, Decrypt(cr, tmp2.Bytes(), id, &tmp3))
 
 	require.True(t, strings.HasPrefix(string(id), "n"))
 	require.True(t, strings.HasSuffix(string(id), "-mysessionid"), id)
 
 	// negative cases
-	require.Error(t, DecryptBLOB(cr, tmp.Bytes(), "invalid-blob-id", &tmp3))
-	require.Error(t, DecryptBLOB(cr, tmp.Bytes(), "zzz0123456789abcdef0123456789abcde-suffix", &tmp3))
-	require.Error(t, DecryptBLOB(cr, tmp.Bytes(), id2, &tmp3))
-	require.Error(t, DecryptBLOB(cr, gather.FromSlice([]byte{2, 3, 4}), id, &tmp2))
+	require.Error(t, Decrypt(cr, tmp.Bytes(), "invalid-blob-id", &tmp3))
+	require.Error(t, Decrypt(cr, tmp.Bytes(), "zzz0123456789abcdef0123456789abcde-suffix", &tmp3))
+	require.Error(t, Decrypt(cr, tmp.Bytes(), id2, &tmp3))
+	require.Error(t, Decrypt(cr, gather.FromSlice([]byte{2, 3, 4}), id, &tmp2))
 }
 
 type badEncryptor struct{}
@@ -83,7 +83,7 @@ func(output []byte, data gather.Bytes) []byte {
 	defer tmp2.Close()
 	defer tmp3.Close()
 
-	_, err := EncryptBLOB(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
+	_, err := Encrypt(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
 	require.Error(t, err)
 
 	f := &format.ContentFormat{
@@ -97,6 +97,19 @@ func(output []byte, data gather.Bytes) []byte {
 
 	cr.h = hf
 
-	_, err = EncryptBLOB(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
+	_, err = Encrypt(cr, gather.FromSlice([]byte{1, 2, 3}), "n", "mysessionid", &tmp)
 	require.Error(t, err)
 }
+
+type staticCrypter struct {
+	h hashing.HashFunc
+	e encryption.Encryptor
+}
+
+func (p staticCrypter) Encryptor() encryption.Encryptor {
+	return p.e
+}
+
+func (p staticCrypter) HashFunc() hashing.HashFunc {
+	return p.h
+}
diff --git a/repo/content/content_index_recovery_test.go b/repo/content/content_index_recovery_test.go
index 2444713a..18b95224 100644
--- a/repo/content/content_index_recovery_test.go
+++ b/repo/content/content_index_recovery_test.go
@@ -9,6 +9,7 @@
 	"github.com/kopia/kopia/internal/blobtesting"
 	"github.com/kopia/kopia/internal/testlogging"
 	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/content/indexblob"
 )
 
 func (s *contentManagerSuite) TestContentIndexRecovery(t *testing.T) {
@@ -28,7 +29,7 @@ func (s *contentManagerSuite) TestContentIndexRecovery(t *testing.T) {
 	}
 
 	// delete all index blobs
-	require.NoError(t, bm.st.ListBlobs(ctx, LegacyIndexBlobPrefix, func(bi blob.Metadata) error {
+	require.NoError(t, bm.st.ListBlobs(ctx, indexblob.V0IndexBlobPrefix, func(bi blob.Metadata) error {
 		t.Logf("deleting %v", bi.BlobID)
 		return bm.st.DeleteBlob(ctx, bi.BlobID)
 	}))
diff --git a/repo/content/content_manager_test.go b/repo/content/content_manager_test.go
index 4e854689..09b775ea 100644
--- a/repo/content/content_manager_test.go
+++ b/repo/content/content_manager_test.go
@@ -33,6 +33,7 @@
 	"github.com/kopia/kopia/repo/blob/logging"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 )
 
@@ -442,7 +443,7 @@ func (s *contentManagerSuite) TestIndexCompactionDropsContent(t *testing.T) {
 
 	bm = s.newTestContentManagerWithCustomTime(t, st, timeFunc)
 	// this drops deleted entries, including from index #1
-	require.NoError(t, bm.CompactIndexes(ctx, CompactOptions{
+	require.NoError(t, bm.CompactIndexes(ctx, indexblob.CompactOptions{
 		DropDeletedBefore: deleteThreshold,
 		AllIndexes:        true,
 	}))
@@ -522,7 +523,7 @@ func (s *contentManagerSuite) TestContentManagerConcurrency(t *testing.T) {
 
 	validateIndexCount(t, data, 4, 0)
 
-	if err := bm4.CompactIndexes(ctx, CompactOptions{MaxSmallBlobs: 1}); err != nil {
+	if err := bm4.CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}); err != nil {
 		t.Errorf("compaction error: %v", err)
 	}
 
@@ -540,7 +541,7 @@ func (s *contentManagerSuite) TestContentManagerConcurrency(t *testing.T) {
 	verifyContent(ctx, t, bm5, bm2content, seededRandomData(32, 100))
 	verifyContent(ctx, t, bm5, bm3content, seededRandomData(33, 100))
 
-	if err := bm5.CompactIndexes(ctx, CompactOptions{MaxSmallBlobs: 1}); err != nil {
+	if err := bm5.CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}); err != nil {
 		t.Errorf("compaction error: %v", err)
 	}
 }
@@ -551,11 +552,11 @@ func validateIndexCount(t *testing.T, data map[blob.ID][]byte, wantIndexCount, w
 	var indexCnt, compactionLogCnt int
 
 	for blobID := range data {
-		if strings.HasPrefix(string(blobID), LegacyIndexBlobPrefix) || strings.HasPrefix(string(blobID), "x") {
+		if strings.HasPrefix(string(blobID), indexblob.V0IndexBlobPrefix) || strings.HasPrefix(string(blobID), "x") {
 			indexCnt++
 		}
 
-		if strings.HasPrefix(string(blobID), compactionLogBlobPrefix) {
+		if strings.HasPrefix(string(blobID), indexblob.V0CompactionLogBlobPrefix) {
 			compactionLogCnt++
 		}
 	}
@@ -1203,7 +1204,7 @@ func (s *contentManagerSuite) TestFlushWaitsForAllPendingWriters(t *testing.T) {
 	bm.Flush(ctx)
 	t.Logf("<<< end of flushing")
 
-	indexBlobPrefix := blob.ID(LegacyIndexBlobPrefix)
+	indexBlobPrefix := blob.ID(indexblob.V0IndexBlobPrefix)
 	if s.mutableParameters.EpochParameters.Enabled {
 		indexBlobPrefix = "x"
 	}
@@ -1923,7 +1924,7 @@ func (s *contentManagerSuite) verifyVersionCompat(t *testing.T, writeVersion for
 	// make sure we can read everything
 	verifyContentManagerDataSet(ctx, t, mgr, dataSet)
 
-	if err := mgr.CompactIndexes(ctx, CompactOptions{MaxSmallBlobs: 1}); err != nil {
+	if err := mgr.CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}); err != nil {
 		t.Fatalf("unable to compact indexes: %v", err)
 	}
 
diff --git a/repo/content/encrypted_blob_mgr_test.go b/repo/content/indexblob/index_blob_encryption_test.go
similarity index 82%
rename from repo/content/encrypted_blob_mgr_test.go
rename to repo/content/indexblob/index_blob_encryption_test.go
index ac227390..47332aa2 100644
--- a/repo/content/encrypted_blob_mgr_test.go
+++ b/repo/content/indexblob/index_blob_encryption_test.go
@@ -1,4 +1,4 @@
-package content
+package indexblob
 
 import (
 	"testing"
@@ -38,7 +38,7 @@ func TestEncryptedBlobManager(t *testing.T) {
 	enc, err := encryption.CreateEncryptor(f)
 	require.NoError(t, err)
 
-	ebm := encryptedBlobMgr{
+	ebm := EncryptionManager{
 		st:             fs,
 		crypter:        staticCrypter{hf, enc},
 		indexBlobCache: nil,
@@ -47,7 +47,7 @@ func TestEncryptedBlobManager(t *testing.T) {
 
 	ctx := testlogging.Context(t)
 
-	bm, err := ebm.encryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3}), "x", "session1")
+	bm, err := ebm.EncryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3}), "x", "session1")
 	require.NoError(t, err)
 
 	stbm, err := st.GetMetadata(ctx, bm.BlobID)
@@ -58,27 +58,27 @@ func TestEncryptedBlobManager(t *testing.T) {
 	var tmp gather.WriteBuffer
 	defer tmp.Close()
 
-	require.NoError(t, ebm.getEncryptedBlob(ctx, bm.BlobID, &tmp))
+	require.NoError(t, ebm.GetEncryptedBlob(ctx, bm.BlobID, &tmp))
 
 	// data corruption
 	data[bm.BlobID][0] ^= 1
 
-	require.Error(t, ebm.getEncryptedBlob(ctx, bm.BlobID, &tmp))
+	require.Error(t, ebm.GetEncryptedBlob(ctx, bm.BlobID, &tmp))
 
-	require.ErrorIs(t, ebm.getEncryptedBlob(ctx, "no-such-blob", &tmp), blob.ErrBlobNotFound)
+	require.ErrorIs(t, ebm.GetEncryptedBlob(ctx, "no-such-blob", &tmp), blob.ErrBlobNotFound)
 
 	someError := errors.Errorf("some error")
 
 	fs.AddFault(blobtesting.MethodPutBlob).ErrorInstead(someError)
 
-	_, err = ebm.encryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3, 4}), "x", "session1")
+	_, err = ebm.EncryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3, 4}), "x", "session1")
 	require.ErrorIs(t, err, someError)
 
 	someError2 := errors.Errorf("some error 2")
 
 	ebm.crypter = staticCrypter{hf, failingEncryptor{nil, someError2}}
 
-	_, err = ebm.encryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3, 4}), "x", "session1")
+	_, err = ebm.EncryptAndWriteBlob(ctx, gather.FromSlice([]byte{1, 2, 3, 4}), "x", "session1")
 	require.ErrorIs(t, err, someError2)
 }
 
diff --git a/repo/content/index_blob_manager_v0_test.go b/repo/content/indexblob/index_blob_manager_v0_test.go
similarity index 91%
rename from repo/content/index_blob_manager_v0_test.go
rename to repo/content/indexblob/index_blob_manager_v0_test.go
index 5cc1cc22..2ca9f6f3 100644
--- a/repo/content/index_blob_manager_v0_test.go
+++ b/repo/content/indexblob/index_blob_manager_v0_test.go
@@ -1,4 +1,4 @@
-package content
+package indexblob
 
 import (
 	"context"
@@ -426,7 +426,7 @@ type fakeContentIndexEntry struct {
 	Deleted bool
 }
 
-func verifyFakeContentsWritten(ctx context.Context, t *testing.T, m *indexBlobManagerV0, numWritten int, contentPrefix string, deletedContents map[string]bool) error {
+func verifyFakeContentsWritten(ctx context.Context, t *testing.T, m *ManagerV0, numWritten int, contentPrefix string, deletedContents map[string]bool) error {
 	t.Helper()
 
 	if numWritten == 0 {
@@ -460,7 +460,7 @@ func verifyFakeContentsWritten(ctx context.Context, t *testing.T, m *indexBlobMa
 	return nil
 }
 
-func fakeCompaction(ctx context.Context, t *testing.T, m *indexBlobManagerV0, dropDeleted bool) error {
+func fakeCompaction(ctx context.Context, t *testing.T, m *ManagerV0, dropDeleted bool) error {
 	t.Helper()
 
 	t.Logf("fakeCompaction(dropDeleted=%v)", dropDeleted)
@@ -516,7 +516,7 @@ func fakeContentID(prefix string, n int) string {
 	return fmt.Sprintf("%v-%06v", prefix, n)
 }
 
-func deleteFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0, prefix string, numWritten int, deleted map[string]bool, timeFunc func() time.Time) error {
+func deleteFakeContents(ctx context.Context, t *testing.T, m *ManagerV0, prefix string, numWritten int, deleted map[string]bool, timeFunc func() time.Time) error {
 	t.Helper()
 
 	if numWritten == 0 {
@@ -553,7 +553,7 @@ func deleteFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0
 	return err
 }
 
-func undeleteFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0, deleted map[string]bool, timeFunc func() time.Time) error {
+func undeleteFakeContents(ctx context.Context, t *testing.T, m *ManagerV0, deleted map[string]bool, timeFunc func() time.Time) error {
 	t.Helper()
 
 	if len(deleted) == 0 {
@@ -591,7 +591,7 @@ func undeleteFakeContents(ctx context.Context, t *testing.T, m *indexBlobManager
 	return err
 }
 
-func writeFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0, prefix string, count int, numWritten *int, timeFunc func() time.Time) error {
+func writeFakeContents(ctx context.Context, t *testing.T, m *ManagerV0, prefix string, count int, numWritten *int, timeFunc func() time.Time) error {
 	t.Helper()
 
 	t.Logf("writeFakeContents()")
@@ -618,7 +618,7 @@ type fakeIndexData struct {
 	Entries  map[string]fakeContentIndexEntry
 }
 
-func writeFakeIndex(ctx context.Context, t *testing.T, m *indexBlobManagerV0, ndx map[string]fakeContentIndexEntry) ([]blob.Metadata, error) {
+func writeFakeIndex(ctx context.Context, t *testing.T, m *ManagerV0, ndx map[string]fakeContentIndexEntry) ([]blob.Metadata, error) {
 	t.Helper()
 
 	var tmp gather.WriteBuffer
@@ -629,7 +629,7 @@ func writeFakeIndex(ctx context.Context, t *testing.T, m *indexBlobManagerV0, nd
 		Entries:  ndx,
 	}))
 
-	bms, err := m.writeIndexBlobs(ctx, []gather.Bytes{tmp.Bytes()}, "")
+	bms, err := m.WriteIndexBlobs(ctx, []gather.Bytes{tmp.Bytes()}, "")
 	if err != nil {
 		return nil, errors.Wrap(err, "error writing blob")
 	}
@@ -639,7 +639,7 @@ func writeFakeIndex(ctx context.Context, t *testing.T, m *indexBlobManagerV0, nd
 
 var errGetAllFakeContentsRetry = errors.New("retry")
 
-func getAllFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0) (map[string]fakeContentIndexEntry, []IndexBlobInfo, error) {
+func getAllFakeContents(ctx context.Context, t *testing.T, m *ManagerV0) (map[string]fakeContentIndexEntry, []Metadata, error) {
 	t.Helper()
 
 	allContents, allBlobs, err := getAllFakeContentsInternal(ctx, t, m)
@@ -651,10 +651,10 @@ func getAllFakeContents(ctx context.Context, t *testing.T, m *indexBlobManagerV0
 	return allContents, allBlobs, err
 }
 
-func getAllFakeContentsInternal(ctx context.Context, t *testing.T, m *indexBlobManagerV0) (map[string]fakeContentIndexEntry, []IndexBlobInfo, error) {
+func getAllFakeContentsInternal(ctx context.Context, t *testing.T, m *ManagerV0) (map[string]fakeContentIndexEntry, []Metadata, error) {
 	t.Helper()
 
-	blobs, _, err := m.listActiveIndexBlobs(ctx)
+	blobs, _, err := m.ListActiveIndexBlobs(ctx)
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "error listing index blobs")
 	}
@@ -667,7 +667,7 @@ func getAllFakeContentsInternal(ctx context.Context, t *testing.T, m *indexBlobM
 	defer bb.Close()
 
 	for _, bi := range blobs {
-		err := m.getIndexBlob(ctx, bi.BlobID, &bb)
+		err := m.enc.GetEncryptedBlob(ctx, bi.BlobID, &bb)
 		if errors.Is(err, blob.ErrBlobNotFound) {
 			return nil, nil, errGetAllFakeContentsRetry
 		}
@@ -700,8 +700,8 @@ func getAllFakeContentsInternal(ctx context.Context, t *testing.T, m *indexBlobM
 
 func assertBlobCounts(t *testing.T, data blobtesting.DataMap, wantN, wantM, wantL int) {
 	t.Helper()
-	require.Len(t, keysWithPrefix(data, compactionLogBlobPrefix), wantM)
-	require.Len(t, keysWithPrefix(data, LegacyIndexBlobPrefix), wantN)
+	require.Len(t, keysWithPrefix(data, V0CompactionLogBlobPrefix), wantM)
+	require.Len(t, keysWithPrefix(data, V0IndexBlobPrefix), wantN)
 	require.Len(t, keysWithPrefix(data, "l"), wantL)
 }
 
@@ -717,7 +717,7 @@ func keysWithPrefix(data blobtesting.DataMap, prefix blob.ID) []blob.ID {
 	return res
 }
 
-func mustRegisterCompaction(t *testing.T, m *indexBlobManagerV0, inputs, outputs []blob.Metadata) {
+func mustRegisterCompaction(t *testing.T, m *ManagerV0, inputs, outputs []blob.Metadata) {
 	t.Helper()
 
 	t.Logf("compacting %v to %v", inputs, outputs)
@@ -728,12 +728,12 @@ func mustRegisterCompaction(t *testing.T, m *indexBlobManagerV0, inputs, outputs
 	}
 }
 
-func mustWriteIndexBlob(t *testing.T, m *indexBlobManagerV0, data string) blob.Metadata {
+func mustWriteIndexBlob(t *testing.T, m *ManagerV0, data string) blob.Metadata {
 	t.Helper()
 
 	t.Logf("writing index blob %q", data)
 
-	blobMDs, err := m.writeIndexBlobs(testlogging.Context(t), []gather.Bytes{gather.FromSlice([]byte(data))}, "")
+	blobMDs, err := m.WriteIndexBlobs(testlogging.Context(t), []gather.Bytes{gather.FromSlice([]byte(data))}, "")
 	if err != nil {
 		t.Fatalf("failed to write index blob: %v", err)
 	}
@@ -741,7 +741,7 @@ func mustWriteIndexBlob(t *testing.T, m *indexBlobManagerV0, data string) blob.M
 	return blobMDs[0]
 }
 
-func assertIndexBlobList(t *testing.T, m *indexBlobManagerV0, wantMD ...blob.Metadata) {
+func assertIndexBlobList(t *testing.T, m *ManagerV0, wantMD ...blob.Metadata) {
 	t.Helper()
 
 	var want []blob.ID
@@ -749,7 +749,7 @@ func assertIndexBlobList(t *testing.T, m *indexBlobManagerV0, wantMD ...blob.Met
 		want = append(want, it.BlobID)
 	}
 
-	l, _, err := m.listActiveIndexBlobs(testlogging.Context(t))
+	l, _, err := m.ListActiveIndexBlobs(testlogging.Context(t))
 	if err != nil {
 		t.Fatalf("failed to list index blobs: %v", err)
 	}
@@ -764,7 +764,7 @@ func assertIndexBlobList(t *testing.T, m *indexBlobManagerV0, wantMD ...blob.Met
 	require.ElementsMatch(t, got, want)
 }
 
-func newIndexBlobManagerForTesting(t *testing.T, st blob.Storage, localTimeNow func() time.Time) *indexBlobManagerV0 {
+func newIndexBlobManagerForTesting(t *testing.T, st blob.Storage, localTimeNow func() time.Time) *ManagerV0 {
 	t.Helper()
 
 	p := &format.ContentFormat{
@@ -785,15 +785,15 @@ func newIndexBlobManagerForTesting(t *testing.T, st blob.Storage, localTimeNow f
 	st = ownwrites.NewWrapper(
 		st,
 		blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil),
-		[]blob.ID{LegacyIndexBlobPrefix, compactionLogBlobPrefix, cleanupBlobPrefix},
+		[]blob.ID{V0IndexBlobPrefix, V0CompactionLogBlobPrefix, V0CleanupBlobPrefix},
 		15*time.Minute,
 	)
 
 	log := testlogging.Printf(t.Logf, "")
 
-	m := &indexBlobManagerV0{
+	m := &ManagerV0{
 		st: st,
-		enc: &encryptedBlobMgr{
+		enc: &EncryptionManager{
 			st:             st,
 			indexBlobCache: nil,
 			crypter:        staticCrypter{hf, enc},
diff --git a/repo/repository_test.go b/repo/repository_test.go
index 2fafac49..b7f42e6c 100644
--- a/repo/repository_test.go
+++ b/repo/repository_test.go
@@ -26,6 +26,7 @@
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/beforeop"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/object"
 )
@@ -131,7 +132,7 @@ func (s *formatSpecificTestSuite) TestPackingSimple(t *testing.T) {
 	verify(ctx, t, env.RepositoryWriter, oid2a, []byte(content2), "packed-object-2")
 	verify(ctx, t, env.RepositoryWriter, oid3a, []byte(content3), "packed-object-3")
 
-	if err := env.RepositoryWriter.ContentManager().CompactIndexes(ctx, content.CompactOptions{MaxSmallBlobs: 1}); err != nil {
+	if err := env.RepositoryWriter.ContentManager().CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}); err != nil {
 		t.Errorf("optimize error: %v", err)
 	}
 
@@ -141,7 +142,7 @@ func (s *formatSpecificTestSuite) TestPackingSimple(t *testing.T) {
 	verify(ctx, t, env.RepositoryWriter, oid2a, []byte(content2), "packed-object-2")
 	verify(ctx, t, env.RepositoryWriter, oid3a, []byte(content3), "packed-object-3")
 
-	if err := env.RepositoryWriter.ContentManager().CompactIndexes(ctx, content.CompactOptions{MaxSmallBlobs: 1}); err != nil {
+	if err := env.RepositoryWriter.ContentManager().CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}); err != nil {
 		t.Errorf("optimize error: %v", err)
 	}
 
@@ -570,7 +571,7 @@ func TestObjectWritesWithRetention(t *testing.T) {
 		prefixesWithRetention = append(prefixesWithRetention, string(prefix))
 	}
 
-	prefixesWithRetention = append(prefixesWithRetention, content.LegacyIndexBlobPrefix, epoch.EpochManagerIndexUberPrefix,
+	prefixesWithRetention = append(prefixesWithRetention, indexblob.V0IndexBlobPrefix, epoch.EpochManagerIndexUberPrefix,
 		format.KopiaRepositoryBlobID, format.KopiaBlobCfgBlobID)
 
 	// make sure that we cannot set mtime on the kopia objects created due to the
diff --git a/tests/repository_stress_test/repository_stress_test.go b/tests/repository_stress_test/repository_stress_test.go
index b6585072..445343fb 100644
--- a/tests/repository_stress_test/repository_stress_test.go
+++ b/tests/repository_stress_test/repository_stress_test.go
@@ -22,6 +22,7 @@
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob/filesystem"
 	"github.com/kopia/kopia/repo/content"
+	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/logging"
 	"github.com/kopia/kopia/tests/repository_stress_test/repomodel"
 )
@@ -444,7 +445,7 @@ func compact(ctx context.Context, r repo.DirectRepositoryWriter, rs *repomodel.R
 	log.Debugf("compact()")
 
 	return errors.Wrapf(
-		r.ContentManager().CompactIndexes(ctx, content.CompactOptions{MaxSmallBlobs: 1}),
+		r.ContentManager().CompactIndexes(ctx, indexblob.CompactOptions{MaxSmallBlobs: 1}),
 		"compact()")
 }
 
diff --git a/tests/testenv/cli_test_env.go b/tests/testenv/cli_test_env.go
index ef42a818..07d1cc21 100644
--- a/tests/testenv/cli_test_env.go
+++ b/tests/testenv/cli_test_env.go
@@ -4,7 +4,10 @@
 import (
 	"bufio"
 	"io"
+	"io/fs"
+	"math/rand"
 	"os"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -106,6 +109,37 @@ func (e *CLITest) RunAndExpectSuccess(t *testing.T, args ...string) []string {
 	return stdout
 }
 
+// TweakFile writes a xor-ed byte at a random point in a file.  Used to simulate file corruption.
+func (e *CLITest) TweakFile(t *testing.T, dirn, fglob string) {
+	t.Helper()
+
+	const RwUserGroupOther = 0o666
+
+	// find a file within the repository to corrupt
+	mch, err := fs.Glob(os.DirFS(dirn), fglob)
+	require.NoError(t, err)
+	require.Greater(t, len(mch), 0)
+
+	// grab a random file in the directory dirn
+	fn := mch[rand.Intn(len(mch))]
+	f, err := os.OpenFile(path.Join(dirn, fn), os.O_RDWR, os.FileMode(RwUserGroupOther))
+	require.NoError(t, err)
+
+	// find the length of the file, then seek to a random location
+	l, err := f.Seek(0, io.SeekEnd)
+	require.NoError(t, err)
+
+	i := rand.Int63n(l)
+	bs := [1]byte{}
+
+	_, err = f.ReadAt(bs[:], i)
+	require.NoError(t, err)
+
+	// write the byte
+	_, err = f.WriteAt([]byte{^bs[0]}, i)
+	require.NoError(t, err)
+}
+
 // RunAndProcessStderr runs the given command, and streams its output line-by-line to a given function until it returns false.
 func (e *CLITest) RunAndProcessStderr(t *testing.T, callback func(line string) bool, args ...string) (wait func() error, kill func()) {
 	t.Helper()
@@ -143,15 +177,17 @@ func (e *CLITest) RunAndExpectSuccessWithErrOut(t *testing.T, args ...string) (s
 }
 
 // RunAndExpectFailure runs the given command, expects it to fail and returns its output lines.
-func (e *CLITest) RunAndExpectFailure(t *testing.T, args ...string) []string {
+func (e *CLITest) RunAndExpectFailure(t *testing.T, args ...string) (stdout, stderr []string) {
 	t.Helper()
 
-	stdout, _, err := e.Run(t, true, args...)
+	var err error
+
+	stdout, stderr, err = e.Run(t, true, args...)
 	if err == nil {
 		t.Fatalf("'kopia %v' succeeded, but expected failure", strings.Join(args, " "))
 	}
 
-	return stdout
+	return stdout, stderr
 }
 
 // RunAndVerifyOutputLineCount runs the given command and asserts it returns the given number of output lines, then returns them.