Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,11 @@ The following types have been tested :
- :datetime (stored as iso8601, use precision: n to store more decimal precision)
- :timestamp (stored as integer)
- :encrypted
- see <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
- You must store a string that can be encoded in json (not binary data), use base64 if needed
- Provides storage format compatible with Couchbase Lite field-level encryption
- See <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
- **Important**: CouchbaseOrm does not perform encryption/decryption - your application must encrypt data before storing it
- Values must be Base64-encoded strings containing pre-encrypted ciphertext
- See the [encryption documentation](https://couchbase-ruby-orm.com/docs/tutorial-ruby-couchbase-orm/encryption) for details
- :array (see below)
- :nested (see below)

Expand Down
184 changes: 136 additions & 48 deletions docusaurus/docs/tutorial-ruby-couchbase-orm/11-encryption.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# Encryption

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.
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.

## 11.1. Encrypted Attributes

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

```ruby
# Define the Bank model with an encrypted attribute
# Define the Bank model with encrypted attributes
class Bank < CouchbaseOrm::Base
attribute :name, :string
attribute :account_number, :encrypted
attribute :routing_number, :encrypted, alg: "3DES"
end
```

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.
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.

```plaintext
{
Expand All @@ -32,83 +32,171 @@ In this example, the `account_number` and `routing_number` attributes are marked
}
```

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.
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`).

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

```ruby
bank = Bank.new(name: 'My Bank', account_number: '123456789', routing_number: '987654321')
# You must encrypt the data BEFORE assigning it to the attribute
require 'base64'

# Assuming you have an encryption method (e.g., AES, Tanker, etc.)
encrypted_account = MyEncryptor.encrypt('123456789')
encrypted_routing = MyEncryptor.encrypt('987654321')

# Values must be Base64-encoded strings
bank = Bank.new(
name: 'My Bank',
account_number: Base64.strict_encode64(encrypted_account),
routing_number: Base64.strict_encode64(encrypted_routing)
)
```

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

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

```ruby
require 'base64'
require 'logger'
require 'openssl'

# Example encryption helper (you should use a proper encryption library)
class SimpleEncryptor
def self.encrypt(plaintext)
# This is a simplified example - use a proper encryption library in production
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = ENV['ENCRYPTION_KEY'] # Store securely, never commit to git

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example uses AES-256-CBC, which requires a 32-byte (256-bit) encryption key. While OpenSSL might pad or truncate a key of incorrect length, this can lead to security vulnerabilities if not handled carefully. It would be beneficial to explicitly state the required key length in a comment to guide users towards better security practices.

Suggested change
cipher.key = ENV['ENCRYPTION_KEY'] # Store securely, never commit to git
cipher.key = ENV['ENCRYPTION_KEY'] # Must be a 32-byte (256-bit) key. Store securely, never commit to git

cipher.iv = iv = cipher.random_iv

encrypted = cipher.update(plaintext) + cipher.final
# Prepend IV for decryption (in real implementation, handle this properly)
iv + encrypted
end

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

# Method to print serialized attributes
def expect_serialized_attributes(bank)
serialized_attrs = bank.send(:serialized_attributes)
serialized_attrs.each do |key, value|
puts "#{key}: #{value}"
end
json_attrs = JSON.parse(bank.to_json)
json_attrs.each do |key, value|
puts "#{key}: #{value}"
end
bank.as_json.each do |key, value|
puts "#{key}: #{value}"
# Extract IV and ciphertext
iv = ciphertext_with_iv[0..15]
ciphertext = ciphertext_with_iv[16..]

cipher.iv = iv
cipher.update(ciphertext) + cipher.final
end
end

# Create a new bank record with encrypted attributes
# Create a bank record with encrypted attributes
plaintext_account = "123456789"
plaintext_routing = "987654321"

# 1. Encrypt the sensitive data
encrypted_account = SimpleEncryptor.encrypt(plaintext_account)
encrypted_routing = SimpleEncryptor.encrypt(plaintext_routing)

# 2. Encode as Base64 for storage
bank = Bank.new(
name: "Test Bank",
account_number: Base64.strict_encode64("123456789"),
routing_number: Base64.strict_encode64("987654321")
account_number: Base64.strict_encode64(encrypted_account),
routing_number: Base64.strict_encode64(encrypted_routing)
)

# Print serialized attributes before saving
expect_serialized_attributes(bank)

# Save the bank record to Couchbase
# 3. Save to Couchbase
bank.save!

# Reload the bank record from Couchbase
bank.reload
# 4. Retrieve and decrypt
found_bank = Bank.find(bank.id)

# Print serialized attributes after reloading
expect_serialized_attributes(bank)
# 5. Decode Base64 and decrypt
account_encrypted = Base64.strict_decode64(found_bank.account_number)
routing_encrypted = Base64.strict_decode64(found_bank.routing_number)

# Find the bank record by ID
found_bank = Bank.find(bank.id)
decrypted_account = SimpleEncryptor.decrypt(account_encrypted)
decrypted_routing = SimpleEncryptor.decrypt(routing_encrypted)

# Print serialized attributes after finding
expect_serialized_attributes(found_bank)
puts "Decrypted account: #{decrypted_account}" # => "123456789"
puts "Decrypted routing: #{decrypted_routing}" # => "987654321"
```

## 11.3. Encryption and Decryption Process
## 11.3. Storage Format

CouchbaseOrm handles the storage format for encrypted attributes but does not perform encryption/decryption. Here's what happens:

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.
**When saving:**
1. You assign a Base64-encoded ciphertext to the encrypted attribute
2. CouchbaseOrm wraps it in the `encrypted$` format with `alg` and `ciphertext` fields
3. The document is stored in Couchbase with this structure

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.
**When loading:**
1. CouchbaseOrm reads the document from Couchbase
2. It unwraps the `encrypted$` format and extracts the `ciphertext` value
3. The Base64-encoded ciphertext is assigned to the attribute
4. Your application must decode and decrypt the value

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.
**Key Points:**
- CouchbaseOrm does **not** require or use any encryption key
- The `alg` field is purely informational (for compatibility with Couchbase Lite)
- All actual encryption/decryption is your application's responsibility
- Values must be valid Base64-encoded strings

## 11.4. Considerations and Best Practices

When using encryption in CouchbaseOrm, consider the following best practices:
When using encrypted attributes in CouchbaseOrm, consider the following best practices:

### Security
- **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)
- **Key management**: Store encryption keys securely using environment variables, secret managers (AWS Secrets Manager, HashiCorp Vault), or key management services
- **Never commit keys**: Keep encryption keys out of version control systems
- **Key rotation**: Implement a key rotation strategy and maintain the ability to decrypt data encrypted with old keys
- **Use authenticated encryption**: Prefer AEAD modes (like AES-GCM) that provide both confidentiality and integrity

### Performance and Querying
- **Cannot query encrypted fields**: Encrypted attributes cannot be used in WHERE clauses or indexed effectively
- **Consider searchable encryption**: If you need to search encrypted data, investigate specialized solutions like searchable encryption schemes or external encrypted search indexes
- **Selective encryption**: Only encrypt truly sensitive fields to minimize performance overhead

### Implementation Patterns
- **Wrap in accessors**: Create getter/setter methods that automatically handle encryption/decryption:
```ruby
class Bank < CouchbaseOrm::Base
attribute :account_number, :encrypted

def account_number=(plaintext)
encrypted = MyEncryptor.encrypt(plaintext)
super(Base64.strict_encode64(encrypted))
end

def account_number
encrypted = Base64.strict_decode64(super)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this getter override example, super is used without parentheses. While this works correctly here, the following example in the "Separate concerns" section (line 188) uses super(). Using super() is more explicit that you are calling the parent method with no arguments. For consistency and clarity across the documentation, it would be better to use super() in this example as well.

Suggested change
encrypted = Base64.strict_decode64(super)
encrypted = Base64.strict_decode64(super())

MyEncryptor.decrypt(encrypted)
end
end
```

- **Separate concerns**: Consider using a concern or module to encapsulate encryption logic:
```ruby
module EncryptedAttributes
def encrypted_attribute(name)
define_method("#{name}=") do |plaintext|
encrypted = MyEncryptor.encrypt(plaintext)
super(Base64.strict_encode64(encrypted))
end

define_method(name) do
encrypted = Base64.strict_decode64(super())
MyEncryptor.decrypt(encrypted)
end
end
end
```

- Keep the encryption key secure and protect it from unauthorized access. Store the key securely and avoid committing it to version control systems.
- Use strong and unique encryption keys for each environment (development, staging, production) to prevent cross-environment access to encrypted data.
- Be cautious when querying encrypted attributes as it may impact performance. Consider indexing encrypted attributes separately if frequent querying is required.
- If you need to search or query encrypted data frequently, consider using a separate encrypted search index or a dedicated encryption service.
- 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.
### Compatibility
- The `encrypted$` format is compatible with Couchbase Lite's field-level encryption
- The `alg` field helps document which encryption algorithm was used, aiding in key rotation and auditing
- Ensure your encryption implementation is compatible across all platforms that access the data (web, mobile, etc.)

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.
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.

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