Skip to content

Commit 1110ee8

Browse files
authored
Merge pull request #19 from Couchbase-Ecosystem/fix_doc_arrount_encryption
doc: Better doc around encryption
2 parents 617998a + 29e5bb6 commit 1110ee8

2 files changed

Lines changed: 141 additions & 50 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,11 @@ The following types have been tested :
113113
- :datetime (stored as iso8601, use precision: n to store more decimal precision)
114114
- :timestamp (stored as integer)
115115
- :encrypted
116-
- see <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
117-
- You must store a string that can be encoded in json (not binary data), use base64 if needed
116+
- Provides storage format compatible with Couchbase Lite field-level encryption
117+
- See <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
118+
- **Important**: CouchbaseOrm does not perform encryption/decryption - your application must encrypt data before storing it
119+
- Values must be Base64-encoded strings containing pre-encrypted ciphertext
120+
- See the [encryption documentation](https://couchbase-ruby-orm.com/docs/tutorial-ruby-couchbase-orm/encryption) for details
118121
- :array (see below)
119122
- :nested (see below)
120123

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
# Encryption
22

3-
CouchbaseOrm provides built-in support for encrypting sensitive data stored in your Couchbase documents. Encryption allows you to protect confidential information, such as personal data or financial details, by encrypting the values before storing them in the database and decrypting them when retrieving the data.
3+
CouchbaseOrm provides built-in support for storing encrypted data in your Couchbase documents using a structured format. The `:encrypted` type provides a standardized storage format compatible with Couchbase Lite's field-level encryption, but **does not perform encryption/decryption itself**. Your application is responsible for encrypting data before storing it and decrypting it after retrieval.
44

55
## 11.1. Encrypted Attributes
66

77
To mark an attribute as encrypted, you can use the `:encrypted` type when defining the attribute in your model.
88

99
```ruby
10-
# Define the Bank model with an encrypted attribute
10+
# Define the Bank model with encrypted attributes
1111
class Bank < CouchbaseOrm::Base
1212
attribute :name, :string
1313
attribute :account_number, :encrypted
1414
attribute :routing_number, :encrypted, alg: "3DES"
1515
end
1616
```
1717

18-
In this example, the `account_number` and `routing_number` attributes are marked as encrypted. By default, CouchbaseOrm uses the default `CB_MOBILE_CUSTOM` encryption algorithm for encrypting the values. You can specify a different encryption algorithm by providing the `alg` option.
18+
In this example, the `account_number` and `routing_number` attributes are marked as encrypted. The `alg` option specifies the encryption algorithm identifier that will be stored in the document metadata (default is `"CB_MOBILE_CUSTOM"`). This identifier is for documentation purposes and Couchbase Lite compatibility - CouchbaseOrm does not use it for actual encryption.
1919

2020
```plaintext
2121
{
@@ -32,83 +32,171 @@ In this example, the `account_number` and `routing_number` attributes are marked
3232
}
3333
```
3434

35-
When a document is saved, CouchbaseOrm stores the encrypted values in the document with a prefix of `encrypted$`. The encrypted values are stored as JSON objects containing the encryption algorithm (`alg`) and the ciphertext (`ciphertext`) of the encrypted value.
35+
When a document is saved, CouchbaseOrm stores encrypted attributes in the document with a prefix of `encrypted$`. The values are stored as JSON objects containing the encryption algorithm identifier (`alg`) and the ciphertext (`ciphertext`).
3636

37-
You can assign values to encrypted attributes just like any other attribute.
37+
**Important**: You must provide **pre-encrypted** values to encrypted attributes. CouchbaseOrm stores these values as-is in the `ciphertext` field without performing any encryption.
3838

3939
```ruby
40-
bank = Bank.new(name: 'My Bank', account_number: '123456789', routing_number: '987654321')
40+
# You must encrypt the data BEFORE assigning it to the attribute
41+
require 'base64'
42+
43+
# Assuming you have an encryption method (e.g., AES, Tanker, etc.)
44+
encrypted_account = MyEncryptor.encrypt('123456789')
45+
encrypted_routing = MyEncryptor.encrypt('987654321')
46+
47+
# Values must be Base64-encoded strings
48+
bank = Bank.new(
49+
name: 'My Bank',
50+
account_number: Base64.strict_encode64(encrypted_account),
51+
routing_number: Base64.strict_encode64(encrypted_routing)
52+
)
4153
```
4254

43-
When the document is saved, CouchbaseOrm encrypts the value of `ssn` using the configured encryption key.
55+
## 11.2. Complete Example with Encryption
4456

45-
## 11.2. Encryption Process
57+
Here's a complete example showing how to handle encryption in your application:
4658

4759
```ruby
4860
require 'base64'
49-
require 'logger'
61+
require 'openssl'
62+
63+
# Example encryption helper (you should use a proper encryption library)
64+
class SimpleEncryptor
65+
def self.encrypt(plaintext)
66+
# This is a simplified example - use a proper encryption library in production
67+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
68+
cipher.encrypt
69+
cipher.key = ENV['ENCRYPTION_KEY'] # Store securely, never commit to git
70+
cipher.iv = iv = cipher.random_iv
71+
72+
encrypted = cipher.update(plaintext) + cipher.final
73+
# Prepend IV for decryption (in real implementation, handle this properly)
74+
iv + encrypted
75+
end
5076

51-
Bank.all.each(&:destroy)
77+
def self.decrypt(ciphertext_with_iv)
78+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
79+
cipher.decrypt
80+
cipher.key = ENV['ENCRYPTION_KEY']
5281

53-
# Method to print serialized attributes
54-
def expect_serialized_attributes(bank)
55-
serialized_attrs = bank.send(:serialized_attributes)
56-
serialized_attrs.each do |key, value|
57-
puts "#{key}: #{value}"
58-
end
59-
json_attrs = JSON.parse(bank.to_json)
60-
json_attrs.each do |key, value|
61-
puts "#{key}: #{value}"
62-
end
63-
bank.as_json.each do |key, value|
64-
puts "#{key}: #{value}"
82+
# Extract IV and ciphertext
83+
iv = ciphertext_with_iv[0..15]
84+
ciphertext = ciphertext_with_iv[16..]
85+
86+
cipher.iv = iv
87+
cipher.update(ciphertext) + cipher.final
6588
end
6689
end
6790

68-
# Create a new bank record with encrypted attributes
91+
# Create a bank record with encrypted attributes
92+
plaintext_account = "123456789"
93+
plaintext_routing = "987654321"
94+
95+
# 1. Encrypt the sensitive data
96+
encrypted_account = SimpleEncryptor.encrypt(plaintext_account)
97+
encrypted_routing = SimpleEncryptor.encrypt(plaintext_routing)
98+
99+
# 2. Encode as Base64 for storage
69100
bank = Bank.new(
70101
name: "Test Bank",
71-
account_number: Base64.strict_encode64("123456789"),
72-
routing_number: Base64.strict_encode64("987654321")
102+
account_number: Base64.strict_encode64(encrypted_account),
103+
routing_number: Base64.strict_encode64(encrypted_routing)
73104
)
74105

75-
# Print serialized attributes before saving
76-
expect_serialized_attributes(bank)
77-
78-
# Save the bank record to Couchbase
106+
# 3. Save to Couchbase
79107
bank.save!
80108

81-
# Reload the bank record from Couchbase
82-
bank.reload
109+
# 4. Retrieve and decrypt
110+
found_bank = Bank.find(bank.id)
83111

84-
# Print serialized attributes after reloading
85-
expect_serialized_attributes(bank)
112+
# 5. Decode Base64 and decrypt
113+
account_encrypted = Base64.strict_decode64(found_bank.account_number)
114+
routing_encrypted = Base64.strict_decode64(found_bank.routing_number)
86115

87-
# Find the bank record by ID
88-
found_bank = Bank.find(bank.id)
116+
decrypted_account = SimpleEncryptor.decrypt(account_encrypted)
117+
decrypted_routing = SimpleEncryptor.decrypt(routing_encrypted)
89118

90-
# Print serialized attributes after finding
91-
expect_serialized_attributes(found_bank)
119+
puts "Decrypted account: #{decrypted_account}" # => "123456789"
120+
puts "Decrypted routing: #{decrypted_routing}" # => "987654321"
92121
```
93122

94-
## 11.3. Encryption and Decryption Process
123+
## 11.3. Storage Format
124+
125+
CouchbaseOrm handles the storage format for encrypted attributes but does not perform encryption/decryption. Here's what happens:
95126

96-
When an encrypted attribute is assigned a value, CouchbaseOrm encrypts the value using the configured encryption key and algorithm. The encrypted value is then stored in the Couchbase document.
127+
**When saving:**
128+
1. You assign a Base64-encoded ciphertext to the encrypted attribute
129+
2. CouchbaseOrm wraps it in the `encrypted$` format with `alg` and `ciphertext` fields
130+
3. The document is stored in Couchbase with this structure
97131

98-
When retrieving a document with encrypted attributes, CouchbaseOrm automatically decrypts the encrypted values using the same encryption key and algorithm. The decrypted values are then accessible through the model's attributes.
132+
**When loading:**
133+
1. CouchbaseOrm reads the document from Couchbase
134+
2. It unwraps the `encrypted$` format and extracts the `ciphertext` value
135+
3. The Base64-encoded ciphertext is assigned to the attribute
136+
4. Your application must decode and decrypt the value
99137

100-
It's important to keep the encryption key secure and protect it from unauthorized access. If the encryption key is compromised, the encrypted data can be decrypted by anyone who obtains the key.
138+
**Key Points:**
139+
- CouchbaseOrm does **not** require or use any encryption key
140+
- The `alg` field is purely informational (for compatibility with Couchbase Lite)
141+
- All actual encryption/decryption is your application's responsibility
142+
- Values must be valid Base64-encoded strings
101143

102144
## 11.4. Considerations and Best Practices
103145

104-
When using encryption in CouchbaseOrm, consider the following best practices:
146+
When using encrypted attributes in CouchbaseOrm, consider the following best practices:
147+
148+
### Security
149+
- **Encryption is your responsibility**: CouchbaseOrm only provides the storage format. Choose a robust encryption library (e.g., `rbnacl`, `openssl`, or a service like AWS KMS)
150+
- **Key management**: Store encryption keys securely using environment variables, secret managers (AWS Secrets Manager, HashiCorp Vault), or key management services
151+
- **Never commit keys**: Keep encryption keys out of version control systems
152+
- **Key rotation**: Implement a key rotation strategy and maintain the ability to decrypt data encrypted with old keys
153+
- **Use authenticated encryption**: Prefer AEAD modes (like AES-GCM) that provide both confidentiality and integrity
154+
155+
### Performance and Querying
156+
- **Cannot query encrypted fields**: Encrypted attributes cannot be used in WHERE clauses or indexed effectively
157+
- **Consider searchable encryption**: If you need to search encrypted data, investigate specialized solutions like searchable encryption schemes or external encrypted search indexes
158+
- **Selective encryption**: Only encrypt truly sensitive fields to minimize performance overhead
159+
160+
### Implementation Patterns
161+
- **Wrap in accessors**: Create getter/setter methods that automatically handle encryption/decryption:
162+
```ruby
163+
class Bank < CouchbaseOrm::Base
164+
attribute :account_number, :encrypted
165+
166+
def account_number=(plaintext)
167+
encrypted = MyEncryptor.encrypt(plaintext)
168+
super(Base64.strict_encode64(encrypted))
169+
end
170+
171+
def account_number
172+
encrypted = Base64.strict_decode64(super)
173+
MyEncryptor.decrypt(encrypted)
174+
end
175+
end
176+
```
177+
178+
- **Separate concerns**: Consider using a concern or module to encapsulate encryption logic:
179+
```ruby
180+
module EncryptedAttributes
181+
def encrypted_attribute(name)
182+
define_method("#{name}=") do |plaintext|
183+
encrypted = MyEncryptor.encrypt(plaintext)
184+
super(Base64.strict_encode64(encrypted))
185+
end
186+
187+
define_method(name) do
188+
encrypted = Base64.strict_decode64(super())
189+
MyEncryptor.decrypt(encrypted)
190+
end
191+
end
192+
end
193+
```
105194

106-
- Keep the encryption key secure and protect it from unauthorized access. Store the key securely and avoid committing it to version control systems.
107-
- Use strong and unique encryption keys for each environment (development, staging, production) to prevent cross-environment access to encrypted data.
108-
- Be cautious when querying encrypted attributes as it may impact performance. Consider indexing encrypted attributes separately if frequent querying is required.
109-
- If you need to search or query encrypted data frequently, consider using a separate encrypted search index or a dedicated encryption service.
110-
- Ensure that the encryption key is properly rotated and managed. If the encryption key is compromised, you should generate a new key and re-encrypt the affected data.
195+
### Compatibility
196+
- The `encrypted$` format is compatible with Couchbase Lite's field-level encryption
197+
- The `alg` field helps document which encryption algorithm was used, aiding in key rotation and auditing
198+
- Ensure your encryption implementation is compatible across all platforms that access the data (web, mobile, etc.)
111199

112-
Encryption is a powerful tool for protecting sensitive data, but it should be used judiciously. Encrypting every attribute in your model may not be necessary or practical. Focus on encrypting the most sensitive and confidential data while balancing the trade-offs between security and performance.
200+
Encryption is a powerful tool for protecting sensitive data, but it should be used judiciously. Focus on encrypting the most sensitive and confidential data while balancing the trade-offs between security, performance, and functionality.
113201

114202
In the next section, we'll explore logging in CouchbaseOrm and how you can configure and customize logging to monitor and debug your application.

0 commit comments

Comments
 (0)