Skip to content

Commit

Permalink
Refactor Capabilities into Messages
Browse files Browse the repository at this point in the history
The Keyspace::Capability class was becoming quite gnarly and
overburdened with too many responsibilities. This change moves the
encryption and decryption responsibilities into a separate
Keyspace::Message class.
  • Loading branch information
tarcieri committed Mar 21, 2013
1 parent 17e6ce4 commit 97c36ed
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 140 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Keyspace is built on [RbNaCl][rbnacl], a Ruby wrapper to the
Status
------

![DANGER: EXPERIMENTAL](https://raw.github.com/cryptosphere/cryptosphere/master/images/experimental.png)
![DANGER: EXPERIMENTAL](https://raw.github.com/livingsocial/keyspace/master/images/experimental.png)

Keyspace is still experimental and the design is subject to change. Some of
the planned changes are:
Expand Down
10 changes: 10 additions & 0 deletions lib/keyspace.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'keyspace/version'
require 'keyspace/capability'
require 'keyspace/message'

module Keyspace
# Generic errors surrounding vaults
Expand All @@ -13,4 +14,13 @@ class KeyNotFoundError < VaultError; end

# MIME type used for vault values
MIME_TYPE = "application/octet-stream"

# Number of bytes in Ed25519 signatures (64-bytes)
SIGNATURE_BYTES = Crypto::NaCl::SIGNATUREBYTES

# Size of the symmetric key (32-bytes)
SECRET_KEY_BYTES = Crypto::NaCl::SECRETKEYBYTES

# Number of bytes in a nonce used by SecretBox (24-bytes)
NONCE_BYTES = Crypto::NaCl::NONCEBYTES
end
124 changes: 16 additions & 108 deletions lib/keyspace/capability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,7 @@ class InvalidSignatureError < StandardError; end

# Capabilities provide access to encrypted data
class Capability
# Size of the symmetric key (32-bytes)
SECRET_KEY_BYTES = Crypto::NaCl::SECRETKEYBYTES

# Number of bytes in a nonce used by SecretBox (24-bytes)
NONCE_BYTES = Crypto::NaCl::NONCEBYTES

# Number of bytes in Ed25519 signatures (64-bytes)
SIGNATURE_BYTES = Crypto::NaCl::SIGNATUREBYTES

# Maximum length of a name (as in name/value pair)
MAX_NAME_LENGTH = 256

attr_reader :id, :signing_key, :verify_key, :secret_key, :capabilities
attr_reader :id, :verify_key, :capabilities

# Generate a new writecap. Note: id is not authenticated
def self.generate(id)
Expand Down Expand Up @@ -54,73 +42,31 @@ def initialize(id, caps, signing_key, secret_key = nil)

if caps.include?('w')
@signing_key = Crypto::SigningKey.new(signing_key)
@verify_key = @signing_key.verify_key
@verify_key = @signing_key.verify_key
else
@signing_key = nil
@verify_key = Crypto::VerifyKey.new(signing_key)
end

if secret_key
hkdf = HKDF.new(@secret_key, :algorithm => 'SHA256')
@name_siv_key = hkdf.next_bytes(32)
@name_key = hkdf.next_bytes(SECRET_KEY_BYTES)
@value_key = hkdf.next_bytes(SECRET_KEY_BYTES)
else
@name_siv_key = nil
@name_key = nil
@value_key = nil
@verify_key = Crypto::VerifyKey.new(signing_key)
end
end

# Encrypt a name/value pair for insertion into Keyspace
def encrypt(name, value, timestamp = Time.now)
raise InvalidCapabilityError, "don't have write capability" unless @signing_key
raise ArgumentError, "name too long" if name.to_s.size > MAX_NAME_LENGTH

pack_signed_nvpair(encrypt_name(name.to_s), encrypt_value(value.to_s), timestamp)
end

# Decrypt an encrypted value, checking its authenticity with the verify key
def decrypt(message)
raise InvalidCapabilityError, "don't have read capability" unless secret_key
encrypted_name, encrypted_value, timestamp = unpack_signed_nvpair(message)

[decrypt_name(encrypted_name), decrypt_value(encrypted_value), timestamp]
# Return the signing key if we have write capability
def signing_key
@signing_key or raise InvalidCapabilityError, "don't have write capability"
end

# Determine if the given encrypted value is authentic
def verify(encrypted_value)
signature, message = encrypted_value.unpack("a#{SIGNATURE_BYTES}a*")
@verify_key.verify(message, signature)
# Return the secret key if we have read capability
def secret_key
@secret_key or raise InvalidCapabilityError, "don't have read capability"
end

# Verify which raises if the signature doesn't match
def verify!(encrypted_value)
verify(encrypted_value) or raise InvalidSignatureError, "potentially forged data: signature mismatch"
# Encrypt a message with this capability
def encrypt(message)
message.encrypt(self)
end

# Pack an encrypted value into its serialized representation
def pack_signed_nvpair(encrypted_name, encrypted_value, timestamp)
message = [
encrypted_name.bytesize,
encrypted_name,
encrypted_value.bytesize,
encrypted_value,
timestamp.utc.to_i
].pack("na*na*Q")

signature = @signing_key.sign(message)
signature + message
end

# Parse an encrypted value into its constituent components
def unpack_signed_nvpair(message)
verify!(message)
signature, name_size, rest = message.unpack("a#{SIGNATURE_BYTES}na*")
encrypted_name, value_size, rest = rest.unpack("a#{name_size}na*")
encrypted_value, timestamp = rest.unpack("a#{value_size}Q")

[encrypted_name, encrypted_value, Time.at(timestamp)]
# Decrypt a message with this capability
def decrypt(encrypted_message)
Message.decrypt(self, encrypted_message)
end

# Degrade this capability to a lower level
Expand Down Expand Up @@ -152,7 +98,7 @@ def verifycap?

# Generate a token out of this capability
def to_s
keys = secret_key || ""
keys = @secret_key || ""

if @signing_key
keys += @signing_key.to_bytes
Expand All @@ -167,43 +113,5 @@ def to_s
def inspect
"#<#{self.class} #{to_s}>"
end

# Encrypt names of name/value pairs using Synthetic IVs (SIV)
# SIV is CPA secure, but gives us deterministic encryption for
# the keys of interest. This allows someone else with the same
# key to calculate a deterministic ciphertext representing the
# name of a name/value pair. This keeps names of name/value pairs
# secure while allowing clients to request specific encrypted keys
def encrypt_name(name)
raise InvalidCapabilityError, "don't have read capability" unless @name_key
name = name.to_s

# Use HKDF as our SIV PRG
hkdf = HKDF.new(name, :iv => @name_siv_key, :algorithm => 'SHA256')
nonce = hkdf.next_bytes(NONCE_BYTES)

ciphertext = Crypto::SecretBox.new(@name_key).encrypt(nonce, name)
nonce + ciphertext
end

# Decrypt a SIV-encrypted name
def decrypt_name(message)
nonce, ciphertext = message[0,NONCE_BYTES], message[NONCE_BYTES..-1]
Crypto::SecretBox.new(@name_key).decrypt(nonce, ciphertext)
end

# Encrypt a value with a random nonce
def encrypt_value(value)
nonce = Crypto::Random.random_bytes(NONCE_BYTES)
ciphertext = Crypto::SecretBox.new(@value_key).encrypt(nonce, value)

nonce + ciphertext
end

# Decrypt a value with a random nonce
def decrypt_value(message)
nonce, ciphertext = message[0,NONCE_BYTES], message[NONCE_BYTES..-1]
Crypto::SecretBox.new(@value_key).decrypt(nonce, ciphertext)
end
end
end
7 changes: 3 additions & 4 deletions lib/keyspace/client/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def verifycap

# Retrieve a value from keyspace
def get(name)
encrypted_name = Base32.encode(@capability.encrypt_name(name))
encrypted_name = Base32.encode(Message.encrypted_name(@capability, name))

uri = URI(Keyspace::Client.url)
uri.path = "/vaults/#{id}/#{encrypted_name}"
Expand All @@ -41,8 +41,7 @@ def get(name)
response = http.request Net::HTTP::Get.new(uri.request_uri)

if response.code == "200"
key, value, timestamp = @capability.decrypt(response.body)
value
@capability.decrypt(response.body).value
elsif response.code == "404"
nil
else raise KeyNotFoundError, "couldn't get key: #{response.code} #{response.message}"
Expand Down Expand Up @@ -84,7 +83,7 @@ def save!
http = Net::HTTP.new(uri.host, uri.port)

request = Net::HTTP::Put.new(uri.request_uri)
request.body = @capability.encrypt(key, value)
request.body = Message.new(key, value).encrypt(@capability)
request['Content-Type'] = Keyspace::MIME_TYPE

response = http.request request
Expand Down
140 changes: 140 additions & 0 deletions lib/keyspace/message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
require 'hkdf'

module Keyspace
class Message
# Maximum length of a name (as in name/value pair)
MAX_NAME_LENGTH = 256

class << self
# Determine if the given encrypted value is authentic
def verify(capability, encrypted_message)
signature, message = encrypted_message.unpack("a#{SIGNATURE_BYTES}a*")
capability.verify_key.verify(message, signature)
end

# Verify which raises if the signature doesn't match
def verify!(capability, encrypted_message)
verify(capability, encrypted_message) or raise InvalidSignatureError, "potentially forged data: signature mismatch"
end

# Pack an encrypted value into its serialized representation
def pack(capability, encrypted_name, encrypted_value, timestamp)
message = [
encrypted_name.bytesize,
encrypted_name,
encrypted_value.bytesize,
encrypted_value,
timestamp.to_i
].pack("na*na*Q")

signature = capability.signing_key.sign(message)
signature + message
end

# Obtain the encrypted version of a given name
def encrypted_name(capability, name)
Encryption.encrypt_name(capability.secret_key, name.to_s)
end

# Verify and unpack an encrypted message into its ciphertexts
def unpack(capability, encrypted_message)
verify!(capability, encrypted_message)

signature, name_size, rest = encrypted_message.unpack("a#{SIGNATURE_BYTES}na*")
encrypted_name, value_size, rest = rest.unpack("a#{name_size}na*")
encrypted_value, timestamp = rest.unpack("a#{value_size}Q")

[encrypted_name, encrypted_value, Time.at(timestamp)]
end

# Decrypt an encrypted message into a Message object
def decrypt(capability, encrypted_message)
encrypted_name, encrypted_value, timestamp = unpack(capability, encrypted_message)

name = Encryption.decrypt_name(capability.secret_key, encrypted_name)
value = Encryption.decrypt_value(capability.secret_key, encrypted_value)

# FIXME: Time.at returns local time
new(name, value, timestamp)
end
end

attr_reader :name, :value, :timestamp

def initialize(name, value, timestamp = Time.now)
raise ArgumentError, "name too long" if name.to_s.size > MAX_NAME_LENGTH

@name, @value, @timestamp = name.to_s, value.to_s, timestamp
end

# Encrypt a name/value pair for insertion into Keyspace
def encrypt(capability)
encrypted_name = Encryption.encrypt_name(capability.secret_key, @name)
encrypted_value = Encryption.encrypt_value(capability.secret_key, @value)

self.class.pack(capability, encrypted_name, encrypted_value, timestamp)
end

# Raw encryption operations for names and values
module Encryption
module_function

# Encrypt names of name/value pairs using Synthetic IVs (SIV)
# SIV is CPA secure, but gives us deterministic encryption for
# the keys of interest. This allows someone else with the same
# key to calculate a deterministic ciphertext representing the
# name of a name/value pair. This keeps names of name/value pairs
# secure while allowing clients to request specific encrypted keys
def encrypt_name(secret_key, name)
keys = kdf(secret_key)

# Use HKDF as our SIV PRG
hkdf = HKDF.new(name, :iv => keys[:name_siv_key], :algorithm => 'SHA256')
nonce = hkdf.next_bytes(NONCE_BYTES)

ciphertext = Crypto::SecretBox.new(keys[:name_key]).encrypt(nonce, name)
nonce + ciphertext
end

# Decrypt a SIV-encrypted name
def decrypt_name(secret_key, encrypted_name)
name_key = kdf(secret_key)[:name_key]
nonce = encrypted_name[0, NONCE_BYTES]
ciphertext = encrypted_name[NONCE_BYTES..-1]

Crypto::SecretBox.new(name_key).decrypt(nonce, ciphertext)
end

# Encrypt a value with a random nonce
def encrypt_value(secret_key, value)
value_key = kdf(secret_key)[:value_key]
nonce = Crypto::Random.random_bytes(NONCE_BYTES)

ciphertext = Crypto::SecretBox.new(value_key).encrypt(nonce, value)
nonce + ciphertext
end

# Decrypt a value with a random nonce
def decrypt_value(secret_key, message)
value_key = kdf(secret_key)[:value_key]
nonce = message[0, NONCE_BYTES]
ciphertext = message[NONCE_BYTES..-1]
Crypto::SecretBox.new(value_key).decrypt(nonce, ciphertext)
end

def kdf(secret_key)
hkdf = HKDF.new(secret_key, :algorithm => 'SHA256')

name_siv_key = hkdf.next_bytes(32)
name_key = hkdf.next_bytes(SECRET_KEY_BYTES)
value_key = hkdf.next_bytes(SECRET_KEY_BYTES)

{
:name_siv_key => name_siv_key,
:name_key => name_key,
:value_key => value_key
}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/keyspace/server/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def get(name)
# Store an encrypted value in this vault
def put(message)
# TODO: check timestamp against existing values to prevent replay attacks
encrypted_name, encrypted_value, timestamp = @capability.unpack_signed_nvpair(message)
encrypted_name, encrypted_value, timestamp = Message.unpack(@capability, message)
self.class.store.put(id, encrypted_name, message)
end
alias_method :[]=, :put
Expand Down
9 changes: 0 additions & 9 deletions spec/keyspace/capability_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@

subject { Keyspace::Capability.generate('test_capability') }

it "encrypts and decrypts values for storage in a capability" do
encrypted_value = subject.encrypt(example_name, example_value, example_date)
key, value, date = subject.decrypt(encrypted_value)

key.should eq example_name
value.should eq example_value
date.utc.to_i.should eq example_date.utc.to_i
end

it "degrades writecaps to readcaps" do
subject.should be_writecap
subject.should be_readcap
Expand Down
Loading

0 comments on commit 97c36ed

Please sign in to comment.