Encryption
How Data Border protects sensitive data with AES-256-GCM encryption
Encryption
Data Border uses industry-standard encryption to protect sensitive data at rest and in transit.
Overview
| Data Type | Protection Method |
|---|---|
| Amazon refresh tokens | AES-256-GCM encryption |
| Session cookies | AES encryption |
| Tenant/seller refresh tokens | Stored unencrypted (rotatable) |
| All API traffic | HTTPS/TLS 1.3 |
| Database | SQLite at rest |
| Object storage | S3 server-side encryption |
AES-256-GCM Encryption
Amazon refresh tokens are encrypted using AES-256-GCM, a modern authenticated encryption algorithm.
How It Works
flowchart LR
A[Amazon Refresh Token] --> B[SCRYPT Key Derivation]
C[amazonTokenSecret] --> B
D[Random Salt 32 bytes] --> B
B --> E[Derived Key 256-bit]
E --> F[AES-256-GCM Encrypt]
A --> F
G[Random IV 16 bytes] --> F
F --> H[Encrypted Token]
F --> I[Auth Tag 16 bytes]
H & I & D & G --> J[Base64 Encoded Output]Storage Format
Encrypted data is stored as a single base64 string containing:
[Salt 32 bytes][IV 16 bytes][Auth Tag 16 bytes][Encrypted Data]
| Component | Size | Purpose |
|---|---|---|
| Salt | 32 bytes | Unique per encryption, used with SCRYPT |
| IV | 16 bytes | Initialization vector for GCM |
| Auth Tag | 16 bytes | Authentication/integrity verification |
| Encrypted Data | Variable | The actual encrypted content |
Key Derivation
Keys are derived from the amazonTokenSecret using SCRYPT:
// SCRYPT parameters
const KEY_LENGTH = 32 // 256 bits
const salt = randomBytes(32)
const derivedKey = await scrypt(amazonTokenSecret, salt, KEY_LENGTH)
SCRYPT is a memory-hard function that makes brute-force attacks computationally expensive.
The amazonTokenSecret
This client-provided secret is the encryption key for Amazon tokens.
Requirements
| Requirement | Value |
|---|---|
| Minimum length | 32 bytes when base64 decoded |
| Maximum length | 64 characters |
| Format | Base64 encoded random data |
| Uniqueness | One per seller |
Generating a Secure Secret
# Using OpenSSL
openssl rand -base64 32
# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Security Properties
- Data Border never stores the secret - Only encrypted tokens are stored
- Secret never leaves your WMS - Only sent in request headers over HTTPS
- Per-seller isolation - Each seller has a unique secret
- Non-recoverable - If lost, the seller must re-authorize
If you lose the amazonTokenSecret, the seller's Amazon tokens cannot be decrypted. The seller will need to complete the OAuth flow again to generate new tokens.
Cookie Encryption
Session cookies (used during OAuth) are encrypted with a separate key.
COOKIE_ENCRYPTION_KEY
| Requirement | Value |
|---|---|
| Length | 64 characters |
| Format | Random alphanumeric + symbols |
| Purpose | Encrypt OAuth session state |
Generating the Key
openssl rand -hex 32
What's Encrypted
- OAuth state (prevents CSRF)
sellerRedirectUrl(prevents tampering)- Temporary session data
JWT Signing
JWTs are signed (not encrypted) for authentication.
JWT_SECRET
Used for HS256 signing when JWKS_ENDPOINT is not configured:
# Generate a secure JWT secret
openssl rand -base64 32
JWKS_ENDPOINT (Optional)
For RS256/ES256 validation via external identity provider:
JWKS_ENDPOINT=https://your-idp.com/.well-known/jwks.json
When set, JWTs are validated against the public keys from this endpoint instead of using JWT_SECRET.
Data at Rest
Database
SQLite database on Fly.io volumes:
- Single-tenant isolation per instance
- Litestream continuous backup to S3
- Volume encryption (platform-level)
Object Storage (Tigris/S3)
Labels and files stored with:
- Server-side encryption (SSE-S3)
- Tenant-isolated prefixes
- Signed URLs for time-limited access
Data in Transit
All traffic uses TLS 1.3:
flowchart LR
A[Your WMS] -->|HTTPS| B[Data Border]
B -->|HTTPS| C[Amazon SP-API]
B -->|HTTPS| D[Carrier APIs]
B -->|HTTPS| E[Object Storage]Certificate Validation
- Data Border validates Amazon's TLS certificates
- Carrier certificates are validated
- No certificate pinning (allows rotation)
Encryption Lifecycle
During OAuth
sequenceDiagram
participant WMS
participant ADB
participant Amazon
WMS->>ADB: Initialize OAuth with amazonTokenSecret
ADB->>ADB: Store secret in encrypted session cookie
Amazon->>ADB: Return refresh token
ADB->>ADB: Encrypt token with amazonTokenSecret
ADB->>ADB: Store encrypted token in database
ADB->>WMS: Redirect (secret not stored)During API Calls
sequenceDiagram
participant WMS
participant ADB
participant Amazon
WMS->>ADB: API request with x-amazon-token-secret header
ADB->>ADB: Retrieve encrypted token from database
ADB->>ADB: Decrypt token using provided secret
ADB->>Amazon: Call SP-API with decrypted token
Amazon->>ADB: Response
ADB->>WMS: Return response (token never exposed)Security Considerations
What Data Border Can Access
| Data | Data Border Access |
|---|---|
| Amazon refresh tokens | Only when secret is provided |
| Amazon access tokens | Temporary, in-memory during requests |
| Customer PII | Fetched on-demand, never stored |
| Labels/documents | Stored encrypted in S3 |
What Data Border Cannot Access
- Your
amazonTokenSecret(not stored) - Amazon tokens without the secret
- Data for other tenants
Threat Mitigation
| Threat | Mitigation |
|---|---|
| Database breach | Tokens encrypted, require secret to decrypt |
| Man-in-the-middle | TLS 1.3, certificate validation |
| Secret brute force | SCRYPT memory-hard KDF |
| Token reuse | Tokens isolated per seller |
| Session hijacking | Short-lived encrypted cookies |
Implementation Reference
Encryption Function
async function encrypt(data: string, secret: string): Promise<string> {
const salt = randomBytes(32)
const iv = randomBytes(16)
const key = await scrypt(secret, salt, 32)
const cipher = createCipheriv('aes-256-gcm', key, iv)
let encrypted = cipher.update(data, 'utf8', 'hex')
encrypted += cipher.final('hex')
const tag = cipher.getAuthTag()
return Buffer.concat([salt, iv, tag, Buffer.from(encrypted, 'hex')])
.toString('base64')
}
Decryption Function
async function decrypt(encryptedData: string, secret: string): Promise<string> {
const combined = Buffer.from(encryptedData, 'base64')
const salt = combined.subarray(0, 32)
const iv = combined.subarray(32, 48)
const tag = combined.subarray(48, 64)
const encrypted = combined.subarray(64)
const key = await scrypt(secret, salt, 32)
const decipher = createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(tag)
let decrypted = decipher.update(encrypted, undefined, 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
