Skip to content

[SPIKE / DO NOT MERGE] Segment storage injection: prove encryption-at-rest is feasible without forking#712

Draft
mahmoud-elmorabea wants to merge 1 commit into
mainfrom
spike/segment-storage-injection-2026-05-19
Draft

[SPIKE / DO NOT MERGE] Segment storage injection: prove encryption-at-rest is feasible without forking#712
mahmoud-elmorabea wants to merge 1 commit into
mainfrom
spike/segment-storage-injection-2026-05-19

Conversation

@mahmoud-elmorabea

Copy link
Copy Markdown
Contributor

Evidence only — DO NOT MERGE

This branch exists to document a spike outcome. It will never be merged into main.

Spike goal

Prove (or disprove) that we can fully influence every byte the Segment analytics-kotlin library persists to disk by injecting a custom StorageProvider via Configuration.storageProvider — without forking the library.

TL;DR

Yes for the event queue + most KVS keys. No for anonymousId / settings (which aren't PII — acceptable under the PII-only scope decision in the parent plan).

  • The event-queue file (app_segment-disk-queue/<writeKey>-N) is fully wrapped: 12 wrapped writes, every byte through Storage.write, no SPIKE_ plaintext on disk after the run. Rollover, upload to the Segment CDP HTTP endpoint, and post-flush removeFile were all observed through the wrapper.
  • segment.app.version, segment.app.build, segment.device.id in the analytics-android-<writeKey> SharedPreferences file all start with WRAPPED::v1::.
  • segment.anonymousId and segment.settings landed plaintext in the same SharedPreferences file. The trace log recorded zero writePrefs calls during the entire run — AndroidKVS writes these directly via SharedPreferences.edit(), bypassing the Storage interface.

What that means for the encryption-at-rest plan

The fork-vs-public-API decision is resolved: we use the public APIs. Phase 3 Android:

  1. Encrypt the event queue via Configuration.storageProvider = CioEncryptedStorageProvider(...). Path validated by this spike.
  2. For userId / traits in the analytics-android-<writeKey> SharedPreferences (PII per the scope): feed analytics-kotlin a custom Context whose getSharedPreferences("analytics-android-<writeKey>", MODE_PRIVATE) returns a CryptoProvider-backed wrapper around regular SharedPreferences. Not AndroidX EncryptedSharedPreferences — Iterable's documented main-thread ANR rules it out.
  3. anonymousId and settings get encrypted as a side effect of wrapping the whole file; they could have been left plaintext too per the PII-only scope, but wrapping the entire file is simpler than per-key carve-outs.

What's on the branch

File Purpose
datapipelines/src/main/kotlin/io/customer/datapipelines/spike/WrappingStorageProvider.kt New StorageProvider wrapping AndroidStorageProvider: traces every Storage call via logcat tag SegmentStorageTrace, wraps writes with WRAPPED::v1:: sentinel + XOR-with-0x55, unwraps reads transparently.
datapipelines/.../extensions/AnalyticsExt.kt Sets storageProvider = WrappingStorageProvider() on the Segment Configuration.
samples/java_layout/.../SampleApplication.java Adds runSegmentStorageSpike() planting SPIKE_ needles via the public CIO API.
SPIKE_REPORT.md Full file-by-file reconciliation table, hexdumps, trace counts, verdict.

Verification

See SPIKE_REPORT.md for the full reconciliation table.

Related

  • Plan: ~/.claude/plans/encryption-at-rest.md (§5 Phase 3, updated with these findings)
  • Companion iOS spike PR on customerio-ios repo

…encryption (no fork needed)

Evidence-only branch. NOT FOR MERGE.

Implements a WrappingStorageProvider that wraps analytics-kotlin's default
AndroidStorageProvider via Configuration.storageProvider. On write/writePrefs
the payload is prepended with a 13-byte sentinel and XORed with 0x55; on
read/readAsStream the mutation is reversed so the library never sees the
wrapping. A spike sequence in the java_layout sample app plants SPIKE_
needles via the public CIO API (identify / track / screen).

Findings (full report in SPIKE_REPORT.md):
- Event-queue path: fully intercepted. 12 wrapped writes to the segment
  event-queue file, all sentinel-prefixed and XORed; zero SPIKE_ plaintext
  in the data directory after the run. Rollover + upload + remove all
  observed through the wrapper.
- KVS path: partial bypass. zero writePrefs calls were observed yet the
  analytics-android-<writeKey>.xml ended up with plaintext anonymousId
  and settings. AndroidKVS writes some keys directly via
  SharedPreferences.edit(), bypassing the Storage interface. Closed by
  feeding analytics-kotlin a custom Context whose getSharedPreferences
  returns a CryptoProvider-backed wrapper, not by forking the library.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Sample app builds 📱

Below you will find the list of the latest versions of the sample apps. It's recommended to always download the latest builds of the sample apps to accurately test the pull request.


@github-actions

Copy link
Copy Markdown

Hey, there @mahmoud-elmorabea 👋🤖. I'm a bot here to help you.

I think the title of this pull request is not in the correct format. Follow the instructions below and then edit the pull request title to a valid format. I'll check again after you make an edit 👍.

This project uses a special format for pull requests titles. Expand this section to learn more (expand by clicking the ᐅ symbol on the left side of this sentence)...

This project uses a special format for pull requests titles. Don't worry, it's easy!

This pull request title should be in this format:

<type>: short description of change being made

If your pull request introduces breaking changes to the code, use this format:

<type>!: short description of breaking change

where <type> is one of the following:

  • feat: - A feature is being added or modified by this pull request. Use this if you made any changes to any of the features of the project.

  • fix: - A bug is being fixed by this pull request. Use this if you made any fixes to bugs in the project.

  • docs: - This pull request is making documentation changes, only.

  • refactor: - A change was made that doesn't fix a bug or add a feature.

  • test: - Adds missing tests or fixes broken tests.

  • style: - Changes that do not effect the code (whitespace, linting, formatting, semi-colons, etc)

  • perf: - Changes improve performance of the code.

  • build: - Changes to the build system (maven, npm, gulp, etc)

  • ci: - Changes to the CI build system (Travis, GitHub Actions, Circle, etc)

  • chore: - Other changes to project that don't modify source code or test files.

  • revert: - Reverts a previous commit that was made.

Examples:

feat: edit profile photo
refactor!: remove deprecated v1 endpoints
build: update npm dependencies
style: run formatter 

Need more examples? Want to learn more about this format? Check out the official docs.

Note: If your pull request does multiple things such as adding a feature and makes changes to the CI server and fixes some bugs then you might want to consider splitting this pull request up into multiple smaller pull requests.

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.42857% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.79%. Comparing base (b5cf24d) to head (4b2d16f).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
...mer/datapipelines/spike/WrappingStorageProvider.kt 60.86% 26 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #712      +/-   ##
============================================
- Coverage     69.07%   65.79%   -3.28%     
+ Complexity      838      793      -45     
============================================
  Files           149      156       +7     
  Lines          4601     4765     +164     
  Branches        628      651      +23     
============================================
- Hits           3178     3135      -43     
- Misses         1189     1389     +200     
- Partials        234      241       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
  • java_layout: spike/segment-storage-injection-2026-05-19 (1779278904)

@github-actions

Copy link
Copy Markdown
  • kotlin_compose: spike/segment-storage-injection-2026-05-19 (1779278891)

@github-actions

Copy link
Copy Markdown

📏 SDK Binary Size Comparison Report

Module Last Recorded Size Current Size Change in Size
core 32.34 KB 32.34 KB ✅ No Change
datapipelines 42.68 KB 45.31 KB ⬆️ +2.63KB
messagingpush 28.93 KB 28.93 KB ✅ No Change
messaginginapp 121.84 KB 121.84 KB ✅ No Change
tracking-migration 22.89 KB 22.89 KB ✅ No Change

@github-actions

Copy link
Copy Markdown

Build available to test
Version: spike-segment-storage-injection-2026-05-19-SNAPSHOT
Repository: https://central.sonatype.com/repository/maven-snapshots/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant