Back to blog
·7 min read·BitAtlas Team

Avoiding Vendor Lock-In Through Encryption and Data Portability

Architectural patterns for EU-compliant applications that let you move your data freely between providers — without sacrificing security or rebuilding from scratch.

EU regulationsvendor lock-inencryption portabilityinteroperabilitydata freedom

Vendor lock-in has always been a technical annoyance. In the EU, it has become a compliance risk. The Data Act (2023), the Cloud Switching Regulation, and the ongoing GDPR enforcement landscape all point in the same direction: regulators expect you to be able to move customer data out of any cloud system — on demand, at low cost, in a usable format — and the storage layer is where most applications fail.

This post looks at the architectural patterns that let you stay portable without rebuilding every time you switch providers, with a focus on how encryption itself becomes the portability mechanism when you design for it from the start.

Why the Storage Layer Locks You In

Most lock-in creeps in through storage, not APIs. When your data is encrypted with provider-managed keys — AWS KMS, Azure Key Vault managed-mode, GCP CMEK with Google as the key custodian — the provider holds the unlock. Migrating means exporting ciphertext you cannot read, negotiating key export (often impossible), and re-encrypting everything on the new platform. In practice, teams don't migrate because the cost is too high.

The GDPR right to data portability (Article 20) and the Data Act portability requirements add regulatory teeth to this. A data controller cannot satisfy a structured, machine-readable export request if the actual bytes are locked inside a provider's key hierarchy. Enforcement actions in Germany and France have made clear that "we'd have to ask AWS for permission" is not an acceptable answer.

The fix is not to stop using cloud storage. The fix is to own the keys before the data reaches the provider.

Pattern 1: Bring Your Own Key with External Key Material

The most widely adopted pattern is BYOK: the cloud provider encrypts your data but uses a key that you control and that you store outside their infrastructure.

// Pseudocode: envelope encryption with an external KMS
async function encryptAndStore(plaintext: Buffer, externalKmsClient: KMSClient): Promise<void> {
  // 1. Generate a fresh data-encryption key (DEK) locally
  const dek = crypto.randomBytes(32);

  // 2. Encrypt the plaintext with the DEK (AES-256-GCM)
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', dek, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const authTag = cipher.getAuthTag();

  // 3. Wrap the DEK with the external KMS (key-encryption key lives there, not at the cloud provider)
  const wrappedDek = await externalKmsClient.encrypt(dek);

  // 4. Store ciphertext + wrappedDek + iv + authTag — the cloud provider holds none of the key material
  await objectStore.put({ ciphertext, wrappedDek, iv, authTag });
}

When you migrate, you export ciphertext and the wrapped DEKs, re-wrap the DEKs using the new provider's key infrastructure (or just unwrap and re-wrap with your own master key), and load everything into the new provider. The data itself is already portable.

The important detail: your external KMS must itself be portable. If you use HashiCorp Vault (self-hosted or HCP), AWS CloudHSM with hardware key import, or a zero-knowledge encrypted key store where only you can access the plaintext keys, you control the entire chain. If you use a provider-managed KMS even for the "external" wrapping layer, you have moved the lock-in one level up rather than removed it.

Pattern 2: Client-Side Encryption Before Upload

A stronger form of portability is encrypting before the data ever reaches any cloud provider. The storage layer becomes a dumb byte store; any provider that can store objects will work.

This is what zero-knowledge storage systems implement. The application (or the agent acting on behalf of the user) encrypts with a key derived from credentials the server never sees. The server stores ciphertext it cannot read. Switching servers is a copy operation: download encrypted bytes, upload encrypted bytes, done.

// Key derivation: the server never sees the passphrase or the derived key
async function deriveEncryptionKey(passphrase: string, salt: Uint8Array): Promise<CryptoKey> {
  const encoder = new TextEncoder();
  const baseKey = await crypto.subtle.importKey('raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']);
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600_000, hash: 'SHA-256' },
    baseKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

From a EU compliance standpoint, this pattern is particularly clean. Because the provider cannot access the plaintext, they do not process personal data within the meaning of GDPR Article 4(2) — they are a processor with no actual visibility into what they hold. Your DPA with them becomes simpler, your DPIA scope shrinks, and your Data Act portability obligations are easier to satisfy because you can fulfil an export request entirely from your own systems.

Pattern 3: Portable Metadata and Schema-First Design

Encryption handles the confidentiality side of portability. Schema portability handles the semantic side.

The most common migration failure is not key management — it is discovering that your data model depends on provider-specific features: DynamoDB's sparse indexes, Firestore's subcollection semantics, Cosmos DB's partition key assumptions. Re-encryption is a weekend project; schema migration is a quarter of work.

Design for this by keeping a provider-agnostic canonical schema alongside your storage code:

// canonical-schema.ts — define shape independent of any store
export interface EncryptedFile {
  id: string;
  ownerPublicKeyFingerprint: string;
  encryptedContentKey: Uint8Array; // key wrapped with owner's public key
  ciphertext: Uint8Array;
  iv: Uint8Array;
  authTag: Uint8Array;
  mimeType: string; // stored as-is, not encrypted
  sizeBytes: number;
  createdAt: string; // ISO 8601
}

Every storage adapter — S3, R2, Azure Blob, a self-hosted MinIO cluster, a BitAtlas vault — implements the same interface. Migration is a loop over records, not a rewrite.

Pattern 4: Signed Export Manifests for Regulators

The Data Act requires that switching assistance be provided without "undue delay" and that providers cannot make porting disproportionately difficult. In practice, demonstrating compliance means being able to produce a verifiable export: a manifest of all records, checksums of exported ciphertext, and a signature that lets the recipient verify nothing was omitted or tampered with.

import hashlib, json, hmac

def build_export_manifest(records: list[dict], signing_key: bytes) -> dict:
    manifest = {
        "version": "1",
        "exported_at": "2026-07-16T00:00:00Z",
        "record_count": len(records),
        "records": [
            {"id": r["id"], "sha256": hashlib.sha256(r["ciphertext"]).hexdigest()}
            for r in records
        ],
    }
    payload = json.dumps(manifest, sort_keys=True).encode()
    manifest["signature"] = hmac.new(signing_key, payload, hashlib.sha256).hexdigest()
    return manifest

Signed manifests let the recipient verify completeness. They also give you an audit trail if a regulator asks whether a GDPR Article 20 request was fully satisfied — the manifest SHA, timestamp, and record count are your evidence.

EU-Specific Considerations

Data Act (Article 23–31): Cloud providers must offer technical interfaces for porting data out. Your architecture should not require your users to go through the provider's export tooling at all — if you own the keys, you can export independently of the provider's cooperation.

Schrems II and adequacy decisions: Portability and data residency interact. When your key material lives outside the EU — even if it is controlled by you — it can trigger data transfer analysis. Keep key material in EU infrastructure if you serve EU data subjects. Self-hosted Vault in a Frankfurt cluster or a zero-knowledge key store with EU-only replication are both viable.

Right to erasure and portability simultaneously: GDPR gives data subjects both rights, and they pull in opposite directions — one says give it all, the other says delete it all. With client-side encryption, erasure is key deletion: shred the user's key material and the ciphertext becomes unrecoverable noise. You can satisfy erasure without touching every object in storage, and you can satisfy portability at any point before erasure by giving the user their keys.

Choosing the Right Architecture for Your Risk Profile

If you control only the key-wrapping hierarchy (BYOK), you accept that the provider handles the raw key operations — fine for most enterprise use cases, but you are still dependent on the provider's KMS export policies.

If you encrypt entirely client-side, the provider truly holds no key material. Migration is a copy-paste. EU compliance is simpler. The tradeoff is that you own key backup and recovery completely — there is no provider to call when a user loses access.

For AI agent workloads, client-side encryption with an MCP-native storage layer gives you both: the agent operates with scoped, revocable keys for a specific session, the storage backend is interchangeable, and you can rotate or revoke access without touching the stored ciphertext. That is a pattern worth designing toward from the start, not retrofitting when a migration or a regulator forces your hand.

The EU regulatory environment is not going to get less demanding about portability. Getting the encryption architecture right early means you can prove compliance to any auditor, switch providers without a rewrite, and give your users genuine control over their data — which is what the regulations were trying to achieve in the first place.

Encrypt your agent's data today

BitAtlas gives your AI agents AES-256-GCM encrypted storage with zero-knowledge guarantees. Free tier, no credit card required.