Data Protection
Comprehensive data protection strategies for securing sensitive financial and personal data in banking applications.
Overview
Data protection ensures that sensitive information remains confidential and secure throughout its lifecycle. In banking applications, protecting customer data is both a regulatory requirement and a fundamental trust obligation.
Payment card data requires special handling under PCI-DSS. Never store full magnetic stripe data, CVV2, or PIN codes. Always use tokenization for card numbers.
See also: Security Overview for general principles, and Input Validation for protecting data integrity.
Data Classification
Classification Levels
Data classification drives protection decisions. Classify data during design by asking "What's the business impact if exposed?" Public data requires no special protection. Internal data should be protected from external access. Confidential data (customer PII) requires encryption and access logging. Restricted data (payment cards, credentials) demands encryption at rest and in transit, minimal retention, detailed audit trails, and often tokenization. See PCI-DSS Compliance for card data handling.
Data Handling Requirements
| Classification | Encryption at Rest | Encryption in Transit | Access Logging | Retention | Examples |
|---|---|---|---|---|---|
| Public | No | Recommended | No | Indefinite | Marketing materials |
| Internal | Recommended | Yes | No | Per policy | Internal documentation |
| Confidential | Yes | Yes (TLS 1.3) | Yes | Limited | Customer PII, transactions |
| Restricted | Yes (AES-256) | Yes (TLS 1.3) | Yes (detailed) | Minimal | Card data, credentials, keys |
Encryption at Rest
Database Field Encryption
Field-level encryption protects sensitive data even if the database is compromised. AES-256-GCM provides confidentiality and authenticity - GCM mode detects tampering. The IV (Initialization Vector) is a random value ensuring the same plaintext produces different ciphertext each time, preventing pattern analysis. Store the IV alongside encrypted data. Protect encryption keys in a Key Management Service, never hardcode or store in the database.
Field-Level Encryption Pattern
Conceptual Approach:
- Identify sensitive fields requiring encryption (SSN, tax ID, phone number)
- Choose encryption algorithm: AES-256-GCM for authenticated encryption
- Generate random IV for each encryption operation (12 bytes for GCM)
- Store IV with ciphertext: Prepend IV to encrypted data (IV doesn't need to be secret)
- Encode for storage: Base64-encode the IV+ciphertext for text-based storage
- Load encryption key from Key Management Service, never hardcode
Database Schema Considerations:
-- Encrypted fields need larger column sizes
CREATE TABLE customers (
id UUID PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255) UNIQUE, -- Plaintext for search/login
social_security_number VARCHAR(512), -- Encrypted (Base64 encoded)
tax_id VARCHAR(512), -- Encrypted
phone_number VARCHAR(512), -- Encrypted
created_at TIMESTAMP,
updated_at TIMESTAMP
);
Encrypted fields require significantly larger storage than plaintext because the ciphertext (encrypted data) + IV (12 bytes) + authentication tag (16 bytes) are Base64-encoded for storage. A 9-digit SSN (9 bytes plaintext) becomes approximately 80 bytes when encrypted and encoded. Using VARCHAR(512) provides headroom for future encryption algorithm changes.
Encryption Algorithm Flow:
Encryption:
plaintext → UTF-8 bytes → AES-256-GCM(key, IV, plaintext) → ciphertext
→ prepend IV → Base64 encode → store
Decryption:
retrieve → Base64 decode → extract IV and ciphertext
→ AES-256-GCM decrypt(key, IV, ciphertext) → plaintext bytes → UTF-8 string
Key Properties:
- Algorithm: AES-256-GCM (authenticated encryption)
- Key Size: 256 bits (32 bytes)
- IV Size: 96 bits (12 bytes) for GCM mode
- Tag Size: 128 bits (16 bytes) for authentication
- Encoding: Base64 for text storage
- Spring Boot: Use JPA
@Convert(converter = EncryptedStringConverter.class)with custom AttributeConverter - TypeScript/Node.js: Use
crypto.createCipherivwith 'aes-256-gcm' - Python: Use
cryptography.fernetorAESfromCrypto.Cipher - Go: Use
crypto/aeswithcipher.NewGCM
See framework-specific security documentation for implementation details.
File Encryption
Small Files (load entirely into memory):
1. Read entire file content into memory
2. Encrypt using field-level encryption pattern (AES-256-GCM)
3. Write encrypted content to destination file
Large Files (streaming encryption to avoid loading entire file):
1. Generate random IV (12 bytes for GCM)
2. Write IV to output file first
3. Initialize AES-256-GCM cipher with key and IV
4. Read input file in chunks (e.g., 8KB buffers)
5. Encrypt each chunk and write to output file
6. Close streams when complete
Key Considerations:
- Small files (<10MB): Simple read-encrypt-write approach
- Large files (>10MB): Use streaming to avoid memory issues
- File storage: Consider platform-specific APIs:
- iOS: Data Protection API with
.completeFileProtection - Android: EncryptedFile from AndroidX Security library
- Backend: Encrypt before writing to disk, decrypt on read
- iOS: Data Protection API with
Encryption in Transit
TLS Configuration Requirements
Protocol Requirements:
- TLS 1.3: Preferred for new connections
- TLS 1.2: Minimum acceptable version
- TLS 1.1 and earlier: Must be disabled (deprecated, vulnerable)
Cipher Suites (in order of preference):
TLS_AES_256_GCM_SHA384(TLS 1.3)TLS_AES_128_GCM_SHA256(TLS 1.3)TLS_CHACHA20_POLY1305_SHA256(TLS 1.3)TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(TLS 1.2)TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(TLS 1.2)
Key Requirements:
- Certificate: Use certificates from trusted CAs
- Key Type: RSA 2048-bit minimum, or ECDSA P-256
- Certificate Rotation: Automate renewal before expiration
- Keystore Protection: Store keystores in secure locations with strong passwords
Mutual TLS (mTLS) for service-to-service communication:
- Client presents certificate to server
- Server validates client certificate against truststore
- Both parties authenticate each other
- Required for high-security inter-service communication
HTTPS Enforcement
Requirements:
-
Redirect HTTP to HTTPS: All HTTP requests must redirect to HTTPS
-
HSTS Header: Send
Strict-Transport-Securityheader to force HTTPS in browsersmax-age=31536000(1 year minimum)- Include
includeSubDomainsfor all subdomains - Consider
preloadfor inclusion in browser HSTS preload lists
-
Secure Cookies: All cookies must have
Secureflag -
Mixed Content: Prevent loading of HTTP resources on HTTPS pages
HSTS Header Example:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- Spring Boot: Configure in
application.ymlwithserver.ssl.*properties and security headers - Node.js/Express: Use
helmetmiddleware for security headers - Nginx/Apache: Configure TLS in server configuration
- Cloud Load Balancers: Use managed certificates (AWS ACM, Google Cloud Load Balancer)
See framework-specific documentation for TLS configuration details.
Service-to-Service mTLS
Mutual TLS (mTLS) extends TLS by requiring both client and server to present certificates, providing two-way authentication. This ensures only authorized services can connect. Even with network access, attackers cannot communicate with services without a valid client certificate. Both parties verify certificates before exchanging encrypted data.
@Configuration
public class MtlsRestTemplateConfig {
@Value("${mtls.keystore.path}")
private String keystorePath;
@Value("${mtls.keystore.password}")
private String keystorePassword;
@Value("${mtls.truststore.path}")
private String truststorePath;
@Value("${mtls.truststore.password}")
private String truststorePassword;
@Bean
public RestTemplate mtlsRestTemplate() throws Exception {
// Load keystore (client certificate)
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(keystorePath)) {
keyStore.load(in, keystorePassword.toCharArray());
}
// Load truststore (trusted server certificates)
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(truststorePath)) {
trustStore.load(in, truststorePassword.toCharArray());
}
// Create SSL context
SSLContext sslContext = SSLContextBuilder.create()
.loadKeyMaterial(keyStore, keystorePassword.toCharArray())
.loadTrustMaterial(trustStore, null)
.build();
// Create HTTP client with mTLS
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
}
PCI-DSS Compliance
Card Data Handling
PCI-DSS prohibits storing certain card data. Never store full magnetic stripe, CVV2/CVC2/CID codes, or PIN blocks. Use tokenization: send card data to a PCI-compliant tokenization service, receive a token, store only the token and last 4 digits. When processing payments, send the token to the payment gateway for detokenization. This minimizes PCI-DSS scope since you never store actual card data.
Tokenization Service
@Service
public class PaymentCardService {
@Autowired
private TokenizationServiceClient tokenizationClient;
@Autowired
private CardTokenRepository cardTokenRepository;
/**
* Tokenize card details - never store actual card number
*/
public CardToken tokenizeCard(CardDetails cardDetails, UUID customerId) {
// Validate card details (but don't store)
validateCardDetails(cardDetails);
// Send to PCI-compliant tokenization service
TokenizationResponse response = tokenizationClient.tokenize(
cardDetails.getNumber(),
cardDetails.getExpiryMonth(),
cardDetails.getExpiryYear()
);
// Store only the token and last 4 digits
CardToken cardToken = new CardToken();
cardToken.setId(UUID.randomUUID());
cardToken.setCustomerId(customerId);
cardToken.setToken(response.getToken());
cardToken.setLast4Digits(cardDetails.getNumber().substring(12));
cardToken.setBrand(detectCardBrand(cardDetails.getNumber()));
cardToken.setExpiryMonth(cardDetails.getExpiryMonth());
cardToken.setExpiryYear(cardDetails.getExpiryYear());
cardToken.setCreatedAt(LocalDateTime.now());
return cardTokenRepository.save(cardToken);
}
/**
* Process payment using token (never see actual card number)
*/
public PaymentResult processPayment(UUID cardTokenId, BigDecimal amount,
String currency) {
CardToken cardToken = cardTokenRepository.findById(cardTokenId)
.orElseThrow(() -> new NotFoundException("Card token not found"));
// Use token to process payment through gateway
PaymentRequest request = PaymentRequest.builder()
.token(cardToken.getToken())
.amount(amount)
.currency(currency)
.build();
return paymentGatewayClient.charge(request);
}
private void validateCardDetails(CardDetails cardDetails) {
// Basic Luhn algorithm check
if (!isValidCardNumber(cardDetails.getNumber())) {
throw new ValidationException("Invalid card number");
}
// Check expiry
if (isExpired(cardDetails.getExpiryMonth(), cardDetails.getExpiryYear())) {
throw new ValidationException("Card expired");
}
// BAD: NEVER validate or store CVV
// CVV is for one-time validation only
}
private String detectCardBrand(String cardNumber) {
if (cardNumber.startsWith("4")) return "VISA";
if (cardNumber.startsWith("5")) return "MASTERCARD";
if (cardNumber.startsWith("3")) return "AMEX";
return "UNKNOWN";
}
}
The tokenization service replaces the card number with a token. The application never stores the full card number anywhere, drastically reducing PCI-DSS scope. The token is a randomly generated surrogate value with no mathematical relationship to the original card number.
// Card token entity - only store safe data
@Entity
@Table(name = "card_tokens")
public class CardToken {
@Id
private UUID id;
private UUID customerId;
// GOOD: Token from tokenization service (safe to store)
@Column(nullable = false, unique = true)
private String token;
// GOOD: Last 4 digits (safe to store, for display)
@Column(length = 4)
private String last4Digits;
// GOOD: Card brand (safe to store)
private String brand;
// GOOD: Expiry (safe to store)
private Integer expiryMonth;
private Integer expiryYear;
// BAD: NEVER store full card number
// BAD: NEVER store CVV
// BAD: NEVER store magnetic stripe data
// BAD: NEVER store PIN
@CreatedDate
private LocalDateTime createdAt;
}
PCI-DSS Requirements Checklist
| Requirement | Implementation | Status |
|---|---|---|
| GOOD: | 1. Firewall | Network segmentation, firewall rules |
| GOOD: | 2. No default passwords | Force password change on first login |
| GOOD: | 3. Protect stored card data | Tokenization, never store CVV/PIN |
| GOOD: | 4. Encrypt transmission | TLS 1.3, mTLS for service-to-service |
| GOOD: | 5. Anti-virus | Endpoint protection, regular scans |
| GOOD: | 6. Secure systems | Regular patching, security updates |
| GOOD: | 7. Restrict access | Least privilege, RBAC, audit logs |
| GOOD: | 8. Unique IDs | UUID per user, no shared accounts |
| GOOD: | 9. Physical access | Data center security (ops team) |
| GOOD: | 10. Track access | Audit logs, security monitoring |
| GOOD: | 11. Security testing | Quarterly scans, annual pen tests |
| GOOD: | 12. Security policy | Written policy, training, enforcement |
Data Masking
Masking Utilities
public class DataMaskingUtils {
/**
* Mask email address: [email protected] -> j***[email protected]
*/
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String username = parts[0];
if (username.length() <= 2) {
return "***@" + parts[1];
}
return username.charAt(0) + "***" +
username.charAt(username.length() - 1) + "@" + parts[1];
}
/**
* Mask card number: 4532123456789012 -> **** **** **** 9012
*/
public static String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 8) {
return "****";
}
String last4 = cardNumber.substring(cardNumber.length() - 4);
return "**** **** **** " + last4;
}
/**
* Mask SSN: 123-45-6789 -> ***-**-6789
*/
public static String maskSsn(String ssn) {
if (ssn == null || ssn.length() < 4) {
return "***";
}
String last4 = ssn.substring(ssn.length() - 4);
return "***-**-" + last4;
}
/**
* Mask phone: +1 (555) 123-4567 -> +1 (***) ***-4567
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 4) {
return "****";
}
String last4 = phone.substring(phone.length() - 4);
return "*** *** " + last4;
}
/**
* Mask account number: show first 2 and last 4
*/
public static String maskAccountNumber(String accountNumber) {
if (accountNumber == null || accountNumber.length() < 6) {
return "****";
}
String first2 = accountNumber.substring(0, 2);
String last4 = accountNumber.substring(accountNumber.length() - 4);
return first2 + "****" + last4;
}
}
Data masking replaces sensitive characters with asterisks while preserving user recognition. Showing last 4 digits of cards allows identification without exposing full numbers. Email masking retains domains for troubleshooting while hiding usernames. Apply masking consistently in UI, logs, and API responses.
JSON Masking with Jackson
// Custom Jackson serializer for automatic masking
public class EmailMaskingSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeString(DataMaskingUtils.maskEmail(value));
}
}
// Apply to DTOs
public class CustomerResponse {
private UUID id;
private String name;
@JsonSerialize(using = EmailMaskingSerializer.class)
private String email;
@JsonSerialize(using = PhoneMaskingSerializer.class)
private String phoneNumber;
// SSN not exposed in API at all (complete field exclusion)
@JsonIgnore
private String socialSecurityNumber;
// Card numbers always masked
@JsonSerialize(using = CardNumberMaskingSerializer.class)
private String cardNumber;
}
Custom Jackson serializers apply masking automatically during JSON serialization. When serializing objects, Jackson invokes custom serializers for annotated fields, transparently masking data. This ensures consistent masking across all API responses.
Log Masking
@Component
public class MaskingPatternLayout extends PatternLayout {
private static final Pattern SSN_PATTERN =
Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b");
private static final Pattern CARD_PATTERN =
Pattern.compile("\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b");
private static final Pattern EMAIL_PATTERN =
Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b");
@Override
public String doLayout(ILoggingEvent event) {
String message = super.doLayout(event);
// Mask sensitive data in logs
message = SSN_PATTERN.matcher(message)
.replaceAll("***-**-$3");
message = CARD_PATTERN.matcher(message)
.replaceAll("**** **** **** $4");
message = EMAIL_PATTERN.matcher(message)
.replaceAll(match -> DataMaskingUtils.maskEmail(match.group()));
return message;
}
}
Log masking intercepts log messages before writing and applies regex patterns to detect and mask sensitive data. Even if developers accidentally log sensitive data, the masking layout prevents actual values from reaching log files. Configure in your logging framework (Logback, Log4j2) to apply globally.
Key Management
Key Rotation Strategy
Regular key rotation limits the impact of key compromise. If a key is exposed, attackers can only decrypt data encrypted with that version. Monthly rotation limits exposure to 30 days of data. The rotation process generates a new key version, uses it for new encryptions, and gradually re-encrypts existing data. Emergency rotation prioritizes sensitive data. Maintain key versioning to decrypt old data with old keys while encrypting new data with new keys.
Key Management Service Integration
@Service
public class KeyManagementService {
@Autowired
private AwsKmsClient kmsClient;
@Value("${kms.key-id}")
private String kmsKeyId;
/**
* Generate data encryption key (DEK)
*/
public DataEncryptionKey generateDataKey() {
GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
.keyId(kmsKeyId)
.keySpec(DataKeySpec.AES_256)
.build();
GenerateDataKeyResponse response = kmsClient.generateDataKey(request);
return DataEncryptionKey.builder()
.plaintextKey(response.plaintext().asByteArray())
.encryptedKey(response.ciphertextBlob().asByteArray())
.build();
}
/**
* Decrypt data encryption key
*/
public byte[] decryptDataKey(byte[] encryptedKey) {
DecryptRequest request = DecryptRequest.builder()
.ciphertextBlob(SdkBytes.fromByteArray(encryptedKey))
.keyId(kmsKeyId)
.build();
DecryptResponse response = kmsClient.decrypt(request);
return response.plaintext().asByteArray();
}
/**
* Rotate encryption keys
*/
@Scheduled(cron = "0 0 1 * * ?") // Monthly on 1st
public void rotateKeys() {
log.info("Starting key rotation");
// Generate new key version
String newKeyVersion = generateKeyVersion();
// Re-encrypt data with new key
List<EncryptedRecord> records = recordRepository.findByKeyVersion(
getCurrentKeyVersion()
);
for (EncryptedRecord record : records) {
String plaintext = encryptionService.decrypt(
record.getEncryptedData(),
getCurrentKeyVersion()
);
String reencrypted = encryptionService.encrypt(
plaintext,
newKeyVersion
);
record.setEncryptedData(reencrypted);
record.setKeyVersion(newKeyVersion);
recordRepository.save(record);
}
log.info("Key rotation completed: {} records migrated", records.size());
}
}
Key Management Services (KMS) like AWS KMS, Azure Key Vault, or HashiCorp Vault provide key generation, rotation, access control, and audit logging. Envelope encryption generates a Data Encryption Key (DEK) to encrypt data, then encrypts the DEK with a master key in KMS. This scales better than encrypting all data directly with KMS while maintaining security - plaintext DEKs are never persisted.
GDPR Compliance
Right to Erasure (Right to be Forgotten)
@Service
public class GdprDataService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private AuditLogService auditLogService;
@Transactional
public void eraseCustomerData(UUID customerId, String reason) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));
// Log erasure request
auditLogService.log(AuditEvent.builder()
.action("DATA_ERASURE_REQUESTED")
.subjectId(customerId)
.reason(reason)
.build());
// Check for legal holds (can't delete if under investigation)
if (customer.hasLegalHold()) {
throw new BusinessException("Cannot erase data under legal hold");
}
// Anonymize personal data (retain transaction history for compliance)
customer.setName("REDACTED");
customer.setEmail("redacted_" + customerId + "@anonymized.local");
customer.setPhoneNumber(null);
customer.setSocialSecurityNumber(null);
customer.setAddress(null);
customer.setDateOfBirth(null);
customer.setStatus(CustomerStatus.ANONYMIZED);
customer.setAnonymizedAt(LocalDateTime.now());
customerRepository.save(customer);
// Anonymize payment descriptions but keep amounts for audit
List<Payment> payments = paymentRepository.findByCustomerId(customerId);
for (Payment payment : payments) {
payment.setDescription("REDACTED");
payment.setRecipientName("REDACTED");
paymentRepository.save(payment);
}
// Log completion
auditLogService.log(AuditEvent.builder()
.action("DATA_ERASURE_COMPLETED")
.subjectId(customerId)
.recordsAffected(payments.size() + 1)
.build());
}
/**
* Export customer data (Right to Data Portability)
*/
public CustomerDataExport exportCustomerData(UUID customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));
List<Payment> payments = paymentRepository.findByCustomerId(customerId);
return CustomerDataExport.builder()
.personalData(CustomerPersonalData.from(customer))
.payments(payments.stream()
.map(PaymentData::from)
.collect(Collectors.toList()))
.exportedAt(LocalDateTime.now())
.build();
}
}
Related Topics
- Security Overview - Security principles and compliance
- Authentication - Protecting authentication credentials
- Input Validation - Protecting data integrity
- Security Testing - Testing encryption and data protection
- Database Guidelines - Database-level security
Summary
Key Takeaways:
- Encryption Everywhere: AES-256-GCM at rest, TLS 1.3 in transit, never store plaintext PII
- PCI-DSS Compliance: Use tokenization, never store CVV/PIN, encrypt cardholder data
- Data Classification: Classify all data, apply protection based on sensitivity
- Key Management: Rotate keys regularly, use KMS for key storage, version keys
- Data Masking: Mask sensitive data in logs, UI, and APIs
- GDPR Rights: Support right to erasure, data portability, consent management
- Minimize Collection: Only collect data you need, delete when no longer required
- Audit Everything: Log all access to sensitive data for compliance and investigation
- Storing credit card CVV codes (PCI-DSS violation)
- Using ECB mode for encryption (insecure, use GCM)
- Hardcoding encryption keys in source code
- Not masking data in logs (exposes PII)
- Storing unencrypted PII in database
- Not supporting GDPR data erasure requests
- Using weak encryption algorithms (DES, MD5, SHA1)
- Sharing encryption keys between environments