Skip to content

Commit

Permalink
Expose access to XSalsa20Poly1305 primitive
Browse files Browse the repository at this point in the history
This is to start to fix #43

Explicitly expose access to the XSalsa20Poly1305 primitive from the
library.  The primitive class handles all of the input checking, and the
encryption methods.  At the moment, the code has been more or less just
copied across.  Eventually, it might make sense to factor this out into
the SecretBox module.

Also, it's possible that one or other of the method calls might return
something other than a string, augmented with additional information and
methods.
  • Loading branch information
namelessjon committed Mar 13, 2013
1 parent 9be368b commit 04eb156
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 21 deletions.
10 changes: 7 additions & 3 deletions lib/rbnacl/nacl.rb
Expand Up @@ -29,6 +29,9 @@ def self.#{name}(*args)
end
eos
end

module SecretBox
end

SHA256BYTES = 32
wrap_nacl_function :crypto_hash_sha256,
Expand Down Expand Up @@ -63,12 +66,13 @@ def self.#{name}(*args)
:crypto_box_curve25519xsalsa20poly1305_ref_open_afternm,
[:pointer, :pointer, :long_long, :pointer, :pointer]

SECRETBOX_KEYBYTES = 32
wrap_nacl_function :crypto_secretbox,
SecretBox::XSALSA20_POLY1305_KEYBYTES = 32
SecretBox::XSALSA20_POLY1305_NONCEBYTES = 24
wrap_nacl_function :crypto_secretbox_xsalsa20poly1305,
:crypto_secretbox_xsalsa20poly1305_ref,
[:pointer, :pointer, :long_long, :pointer, :pointer]

wrap_nacl_function :crypto_secretbox_open,
wrap_nacl_function :crypto_secretbox_xsalsa20poly1305_open,
:crypto_secretbox_xsalsa20poly1305_ref_open,
[:pointer, :pointer, :long_long, :pointer, :pointer]

Expand Down
102 changes: 102 additions & 0 deletions lib/rbnacl/nacl/secret_box/xsalsa20_poly1305_box.rb
@@ -0,0 +1,102 @@
# encoding: binary
module Crypto
module NaCl
module SecretBox
# The XSalsa20Poly1305Box class boxes and unboxes messages
#
# This class uses the given secret key to encrypt and decrypt messages.
#
# It is VITALLY important that the nonce is a nonce, i.e. it is a number
# used only once for any given pair of keys. If you fail to do this, you
# compromise the privacy of the messages encrypted. Give your nonces a
# different prefix, or have one side use an odd counter and one an even
# counter. Just make sure they are different.
#
# The ciphertexts generated by this class include a 16-byte authenticator
# which is checked as part of the decryption. An invalid authenticator
# will cause the unbox function to raise. The authenticator is not a
# signature. Once you've looked in the box, you've demonstrated the
# ability to create arbitrary valid messages, so messages you send are
# repudiatable. For non-repudiatable messages, sign them before or after
# encryption.
#
# * Encryption: XSalsa20, a fast stream cipher primitive
# * Authentication: Poly1305, a fast one time authentication primitive
#
# **WARNING**: This class provides direct access to a low-level
# cryptographic method. You should not use this class without good
# reason and instead use the Crypto::SecretBox box class which will
# always point to the best primitive the library provides bindings for.
# It also provides a nicer interface, with e.g. encoding of the resulting
# messages as base64.
class XSalsa20Poly1305Box
# Number of bytes for a secret key
KEYBYTES = NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES
# Number of bytes for a nonce
NONCEBYTES = NaCl::SecretBox::XSALSA20_POLY1305_NONCEBYTES

# Create a new SecretBox
#
# Sets up the Box with a secret key fro encrypting and decrypting messages.
#
# @param key [String] The key to encrypt and decrypt with
#
# @raise [Crypto::LengthError] on invalid keys
#
# @return [Crypto::SecretBox] The new Box, ready to use
def initialize(key)
@key = key
Util.check_length(@key, KEYBYTES, "Secret key")
end

# Encrypts a message
#
# Encrypts the message with the given nonce to the key set up when
# initializing the class. Make sure the nonce is unique for any given
# key, or you might as well just send plain text.
#
# This function takes care of the padding required by the NaCL C API.
#
# @param nonce [String] A 24-byte string containing the nonce.
# @param message [String] The message to be encrypted.
#
# @raise [Crypto::LengthError] If the nonce is not valid
#
# @return [String] The ciphertext without the nonce prepended (BINARY encoded)
def box(nonce, message)
Util.check_length(nonce, NONCEBYTES, "Nonce")
msg = Util.prepend_zeros(NaCl::ZEROBYTES, message)
ct = Util.zeros(msg.bytesize)

NaCl.crypto_secretbox_xsalsa20poly1305(ct, msg, msg.bytesize, nonce, @key) || raise(CryptoError, "Encryption failed")
Util.remove_zeros(NaCl::BOXZEROBYTES, ct)
end
alias encrypt box

# Decrypts a ciphertext
#
# Decrypts the ciphertext with the given nonce using the key setup when
# initializing the class.
#
# This function takes care of the padding required by the NaCL C API.
#
# @param nonce [String] A 24-byte string containing the nonce.
# @param ciphertext [String] The message to be decrypted.
#
# @raise [Crypto::LengthError] If the nonce is not valid
# @raise [Crypto::CryptoError] If the ciphertext cannot be authenticated.
#
# @return [String] The decrypted message (BINARY encoded)
def open(nonce, ciphertext)
Util.check_length(nonce, NONCEBYTES, "Nonce")
ct = Util.prepend_zeros(NaCl::BOXZEROBYTES, ciphertext)
message = Util.zeros(ct.bytesize)

NaCl.crypto_secretbox_xsalsa20poly1305_open(message, ct, ct.bytesize, nonce, @key) || raise(CryptoError, "Decryption failed. Ciphertext failed verification.")
Util.remove_zeros(NaCl::ZEROBYTES, message)
end
alias decrypt open
end
end
end
end
22 changes: 6 additions & 16 deletions lib/rbnacl/secret_box.rb
@@ -1,4 +1,5 @@
# encoding: binary
require 'rbnacl/nacl/secret_box/xsalsa20_poly1305_box'
module Crypto
# The SecretBox class boxes and unboxes messages
#
Expand All @@ -17,8 +18,7 @@ module Crypto
# arbitrary valid messages, so messages you send are repudiatable. For
# non-repudiatable messages, sign them before or after encryption.
class SecretBox
# Number of bytes for a secret key
KEYBYTES = NaCl::SECRETBOX_KEYBYTES
attr_reader :primitive

# Create a new SecretBox
#
Expand All @@ -30,9 +30,9 @@ class SecretBox
# @raise [Crypto::LengthError] on invalid keys
#
# @return [Crypto::SecretBox] The new Box, ready to use
def initialize(key, encoding = :raw)
def initialize(key, encoding = :raw, primitive = NaCl::SecretBox::XSalsa20Poly1305Box)
@key = Encoder[encoding].decode(key) if key
Util.check_length(@key, KEYBYTES, "Secret key")
@primitive = primitive.new(@key)
end

# Encrypts a message
Expand All @@ -50,12 +50,7 @@ def initialize(key, encoding = :raw)
#
# @return [String] The ciphertext without the nonce prepended (BINARY encoded)
def box(nonce, message)
Util.check_length(nonce, Crypto::NaCl::NONCEBYTES, "Nonce")
msg = Util.prepend_zeros(NaCl::ZEROBYTES, message)
ct = Util.zeros(msg.bytesize)

NaCl.crypto_secretbox(ct, msg, msg.bytesize, nonce, @key) || raise(CryptoError, "Encryption failed")
Util.remove_zeros(NaCl::BOXZEROBYTES, ct)
self.primitive.box(nonce, message)
end
alias encrypt box

Expand All @@ -74,12 +69,7 @@ def box(nonce, message)
#
# @return [String] The decrypted message (BINARY encoded)
def open(nonce, ciphertext)
Util.check_length(nonce, Crypto::NaCl::NONCEBYTES, "Nonce")
ct = Util.prepend_zeros(NaCl::BOXZEROBYTES, ciphertext)
message = Util.zeros(ct.bytesize)

NaCl.crypto_secretbox_open(message, ct, ct.bytesize, nonce, @key) || raise(CryptoError, "Decryption failed. Ciphertext failed verification.")
Util.remove_zeros(NaCl::ZEROBYTES, message)
self.primitive.open(nonce, ciphertext)
end
alias decrypt open
end
Expand Down
24 changes: 24 additions & 0 deletions spec/rbnacl/nacl/secret_box/xsalsa20_poly1305_box_spec.rb
@@ -0,0 +1,24 @@
require 'spec_helper'

describe Crypto::NaCl::SecretBox::XSalsa20Poly1305Box do

let (:key) { hex2bytes(Crypto::TestVectors[:secret_key]) }

context "new" do
it "accepts strings" do
expect { described_class.new(key) }.to_not raise_error(Exception)
end

it "raises on a nil key" do
expect { described_class.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)")
end

it "raises on a short key" do
expect { described_class.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)")
end
end

include_examples "box" do
let(:box) { described_class.new(key) }
end
end
4 changes: 2 additions & 2 deletions spec/rbnacl/secret_box_spec.rb
Expand Up @@ -10,11 +10,11 @@
end

it "raises on a nil key" do
expect { Crypto::SecretBox.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)")
expect { Crypto::SecretBox.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES}\)")
end

it "raises on a short key" do
expect { Crypto::SecretBox.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)")
expect { Crypto::SecretBox.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES}\)")
end
end

Expand Down

0 comments on commit 04eb156

Please sign in to comment.