Breaking API changes:

SecretKey renamed to CryptoBox::SecretKey
  PublicKey renamed to CryptoBox::PublicKey
  KeyPair removed.  Use CryptoBox::SecretKey instead.

  Cox.encrypt was removed.  Use `secret_key.pair(...).encrypt`
  Cox.decrypt was removed.  Use `secret_key.pair(...).decrypt`
master
Didactic Drunk 2019-06-28 04:32:16 -07:00
parent 0eb4a8991a
commit a02c54f4a7
11 changed files with 132 additions and 64 deletions

View File

@ -63,26 +63,33 @@ dependencies:
## Usage ## Usage
### CryptoBox easy encryption
```crystal ```crystal
require "cox" require "cox"
data = "Hello World!" data = "Hello World!"
# Alice is the sender # Alice is the sender
alice = Cox::KeyPair.new alice = Cox::CryptoBox::SecretKey.new
# Bob is the recipient # Bob is the recipient
bob = Cox::KeyPair.new bob = Cox::CryptoBox::SecretKey.new
# Precompute a shared secret between alice and bob.
pair = alice.pair bob.public_key
# Encrypt a message for Bob using his public key, signing it with Alice's # Encrypt a message for Bob using his public key, signing it with Alice's
# secret key # secret key
nonce, encrypted = Cox.encrypt(data, bob.public, alice.secret) nonce, encrypted = pair.encrypt data
# Precompute within a block. The shared secret is wiped when the block exits.
bob.pair alice.public_key do |pair|
# Decrypt the message using Bob's secret key, and verify its signature against # Decrypt the message using Bob's secret key, and verify its signature against
# Alice's public key # Alice's public key
decrypted = Cox.decrypt(encrypted, nonce, alice.public, bob.secret) decrypted = Cox.decrypt(encrypted, nonce, alice.public, bob.secret)
String.new(decrypted) # => "Hello World!" String.new(decrypted) # => "Hello World!"
end
``` ```
### Public key signing ### Public key signing

View File

@ -0,0 +1,26 @@
require "../../spec_helper"
describe Cox::CryptoBox::SecretKey do
it "easy encrypt/decrypt" do
data = "Hello World!"
# Alice is the sender
alice = Cox::CryptoBox::SecretKey.new
# Bob is the recipient
bob = Cox::CryptoBox::SecretKey.new
# Encrypt a message for Bob using his public key, signing it with Alice's
# secret key
pair = alice.pair bob.public_key
nonce, encrypted = pair.encrypt_easy data
# Decrypt the message using Bob's secret key, and verify its signature against
# Alice's public key
bob.pair alice.public_key do |pair|
decrypted = pair.decrypt_easy encrypted, nonce: nonce
String.new(decrypted).should eq(data)
end
end
end

View File

@ -1,25 +1,7 @@
require "./spec_helper" require "./spec_helper"
describe Cox do describe Cox do
# TODO: Write tests # Finished in 71 microseconds
# 1 examples, 2 failures, 5 errors, 0 pending
it "works for encrypting" do # What a man.
data = "Hello World!"
# Alice is the sender
alice = Cox::KeyPair.new
# Bob is the recipient
bob = Cox::KeyPair.new
# Encrypt a message for Bob using his public key, signing it with Alice's
# secret key
nonce, encrypted = Cox.encrypt(data, bob.public, alice.secret)
# Decrypt the message using Bob's secret key, and verify its signature against
# Alice's public key
decrypted = Cox.decrypt(encrypted, nonce, alice.public, bob.secret)
String.new(decrypted).should eq(data)
end
end end

View File

@ -13,7 +13,7 @@ end
require "./cox/**" require "./cox/**"
module Cox module Cox
def self.encrypt(data, nonce : Nonce, recipient_public_key : PublicKey, sender_secret_key : SecretKey) def self.encrypt(data, nonce : Nonce, recipient_public_key : CryptoBox::PublicKey, sender_secret_key : CryptoBox::SecretKey)
data_buffer = data.to_slice data_buffer = data.to_slice
data_size = data_buffer.bytesize data_size = data_buffer.bytesize
output_buffer = Bytes.new(data_buffer.bytesize + LibSodium::MAC_SIZE) output_buffer = Bytes.new(data_buffer.bytesize + LibSodium::MAC_SIZE)
@ -23,12 +23,12 @@ module Cox
output_buffer output_buffer
end end
def self.encrypt(data, recipient_public_key : PublicKey, sender_secret_key : SecretKey) def self.encrypt(data, recipient_public_key : CryptoBox::PublicKey, sender_secret_key : CryptoBox::SecretKey)
nonce = Nonce.new nonce = Nonce.new
{nonce, encrypt(data, nonce, recipient_public_key, sender_secret_key)} {nonce, encrypt(data, nonce, recipient_public_key, sender_secret_key)}
end end
def self.decrypt(data, nonce : Nonce, sender_public_key : PublicKey, recipient_secret_key : SecretKey) def self.decrypt(data, nonce : Nonce, sender_public_key : CryptoBox::PublicKey, recipient_secret_key : CryptoBox::SecretKey)
data_buffer = data.to_slice data_buffer = data.to_slice
data_size = data_buffer.bytesize data_size = data_buffer.bytesize
output_buffer = Bytes.new(data_buffer.bytesize - LibSodium::MAC_SIZE) output_buffer = Bytes.new(data_buffer.bytesize - LibSodium::MAC_SIZE)

View File

@ -0,0 +1,32 @@
require "../lib_sodium"
module Cox::CryptoBox
class Pair
# TODO: precompute using crypto_box_beforenm
def initialize(@secret_key : SecretKey, @public_key : PublicKey)
end
def encrypt_easy(src)
encrypt_easy src.to_slice
end
def encrypt_easy(src : Bytes, dst = Bytes.new(src.bytesize + LibSodium::MAC_SIZE), nonce = Nonce.new)
if LibSodium.crypto_box_easy(dst, src, src.bytesize, nonce.to_slice, @public_key.to_slice, @secret_key.to_slice) != 0
raise Error.new("crypto_box_easy")
end
{nonce, dst}
end
def decrypt_easy(src : Bytes, dst = Bytes.new(src.bytesize - LibSodium::MAC_SIZE), nonce = Nonce.new) : Bytes
if LibSodium.crypto_box_open_easy(dst, src, src.bytesize, nonce.to_slice, @public_key.to_slice, @secret_key.to_slice) != 0
raise Error::DecryptionFailed.new("crypto_box_open_easy")
end
dst
end
# TODO detached
def close
# TODO: wipe state
end
end
end

View File

@ -1,11 +1,11 @@
require "./lib_sodium" require "../lib_sodium"
module Cox module Cox::CryptoBox
class PublicKey < Key class PublicKey < Key
property bytes : Bytes
KEY_SIZE = LibSodium::PUBLIC_KEY_SIZE KEY_SIZE = LibSodium::PUBLIC_KEY_SIZE
getter bytes : Bytes
def initialize(@bytes : Bytes) def initialize(@bytes : Bytes)
if bytes.bytesize != KEY_SIZE if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")

View File

@ -0,0 +1,41 @@
require "../lib_sodium"
module Cox::CryptoBox
class SecretKey < Key
KEY_SIZE = LibSodium::SECRET_KEY_SIZE
MAC_SIZE = LibSodium::MAC_SIZE
getter public_key
getter bytes : Bytes
# Generate a new secret/public key pair.
def initialize
pkey = Bytes.new(PublicKey::KEY_SIZE)
@bytes = Bytes.new(KEY_SIZE)
@public_key = PublicKey.new pkey
LibSodium.crypto_box_keypair(pkey, @bytes)
end
# Use existing Secret and Public keys.
def initialize(@bytes : Bytes, pkey : Bytes)
if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")
end
@public_key = PublicKey.new pkey
end
def pair(public_key)
Pair.new self, public_key
end
# Create a new pair and automatically close when exiting the block.
def pair(public_key)
pa = pair public_key
begin
yield pa
ensure
pa.close
end
end
end
end

View File

@ -1,24 +0,0 @@
require "./lib_sodium"
module Cox
class KeyPair
property public : PublicKey
property secret : SecretKey
def initialize(@public, @secret)
end
def self.new(pub : Bytes, sec : Bytes)
new(PublicKey.new(pub), SecretKey.new(sec))
end
def self.new
public_key = Bytes.new(PublicKey::KEY_SIZE)
secret_key = Bytes.new(SecretKey::KEY_SIZE)
LibSodium.crypto_box_keypair(public_key.to_unsafe, secret_key.to_unsafe)
new(public_key, secret_key)
end
end
end

View File

@ -14,8 +14,8 @@ module Cox
end end
end end
def self.new def initialize
new(Random::Secure.random_bytes(NONCE_SIZE)) @bytes = Random::Secure.random_bytes(NONCE_SIZE)
end end
end end
end end

View File

@ -2,16 +2,18 @@ require "../lib_sodium"
module Cox module Cox
class Sign::PublicKey < Key class Sign::PublicKey < Key
property bytes : Bytes
KEY_SIZE = LibSodium::PUBLIC_SIGN_SIZE KEY_SIZE = LibSodium::PUBLIC_SIGN_SIZE
getter bytes : Bytes
def initialize(@bytes : Bytes) def initialize(@bytes : Bytes)
if bytes.bytesize != KEY_SIZE if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")
end end
end end
# Verify signature made by `secret_key.sign_detached(message)`
# Raises on verification failure.
def verify_detached(message, sig : Bytes) def verify_detached(message, sig : Bytes)
verify_detached message.to_slice, sig verify_detached message.to_slice, sig
end end

View File

@ -15,7 +15,7 @@ module Cox
LibSodium.crypto_sign_keypair pkey, @bytes LibSodium.crypto_sign_keypair pkey, @bytes
end end
# Use existing Private and Public keys. # Use existing Secret and Public keys.
def initialize(@bytes : Bytes, pkey : Bytes) def initialize(@bytes : Bytes, pkey : Bytes)
raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}")
@public_key = PublicKey.new pkey @public_key = PublicKey.new pkey
@ -31,6 +31,8 @@ module Cox
# Also needs to differentiate from seed as a single parameter # Also needs to differentiate from seed as a single parameter
# end # end
# Signs message and returns a detached signature.
# Verify using `secret_key.public_key.verify_detached(message, sig)`
def sign_detached(message) def sign_detached(message)
sign_detached message.to_slice sign_detached message.to_slice
end end