diff --git a/README.md b/README.md index 9a0a4ed..578c1b7 100644 --- a/README.md +++ b/README.md @@ -63,26 +63,33 @@ dependencies: ## Usage +### CryptoBox easy encryption ```crystal require "cox" data = "Hello World!" # Alice is the sender -alice = Cox::KeyPair.new +alice = Cox::CryptoBox::SecretKey.new # 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 # secret key -nonce, encrypted = Cox.encrypt(data, bob.public, alice.secret) +nonce, encrypted = pair.encrypt data -# 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) +# 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 + # Alice's public key + decrypted = Cox.decrypt(encrypted, nonce, alice.public, bob.secret) -String.new(decrypted) # => "Hello World!" + String.new(decrypted) # => "Hello World!" +end ``` ### Public key signing diff --git a/spec/cox/crypto_box/secret_key_spec.cr b/spec/cox/crypto_box/secret_key_spec.cr new file mode 100644 index 0000000..c4bcc88 --- /dev/null +++ b/spec/cox/crypto_box/secret_key_spec.cr @@ -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 diff --git a/spec/cox_spec.cr b/spec/cox_spec.cr index 518ec37..d1e56e8 100644 --- a/spec/cox_spec.cr +++ b/spec/cox_spec.cr @@ -1,25 +1,7 @@ require "./spec_helper" describe Cox do - # TODO: Write tests - - it "works for encrypting" do - 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 + # Finished in 71 microseconds + # 1 examples, 2 failures, 5 errors, 0 pending + # What a man. end diff --git a/src/cox.cr b/src/cox.cr index 61caeab..5c85e89 100644 --- a/src/cox.cr +++ b/src/cox.cr @@ -13,7 +13,7 @@ end require "./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_size = data_buffer.bytesize output_buffer = Bytes.new(data_buffer.bytesize + LibSodium::MAC_SIZE) @@ -23,12 +23,12 @@ module Cox output_buffer 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, encrypt(data, nonce, recipient_public_key, sender_secret_key)} 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_size = data_buffer.bytesize output_buffer = Bytes.new(data_buffer.bytesize - LibSodium::MAC_SIZE) diff --git a/src/cox/crypto_box/pair.cr b/src/cox/crypto_box/pair.cr new file mode 100644 index 0000000..213e761 --- /dev/null +++ b/src/cox/crypto_box/pair.cr @@ -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 diff --git a/src/cox/public_key.cr b/src/cox/crypto_box/public_key.cr similarity index 78% rename from src/cox/public_key.cr rename to src/cox/crypto_box/public_key.cr index 57feaf8..783b043 100644 --- a/src/cox/public_key.cr +++ b/src/cox/crypto_box/public_key.cr @@ -1,11 +1,11 @@ -require "./lib_sodium" +require "../lib_sodium" -module Cox +module Cox::CryptoBox class PublicKey < Key - property bytes : Bytes - KEY_SIZE = LibSodium::PUBLIC_KEY_SIZE + getter bytes : Bytes + def initialize(@bytes : Bytes) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") diff --git a/src/cox/crypto_box/secret_key.cr b/src/cox/crypto_box/secret_key.cr new file mode 100644 index 0000000..7f3e826 --- /dev/null +++ b/src/cox/crypto_box/secret_key.cr @@ -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 diff --git a/src/cox/key_pair.cr b/src/cox/key_pair.cr deleted file mode 100644 index 3bea256..0000000 --- a/src/cox/key_pair.cr +++ /dev/null @@ -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 diff --git a/src/cox/nonce.cr b/src/cox/nonce.cr index 77c8115..53dcc58 100644 --- a/src/cox/nonce.cr +++ b/src/cox/nonce.cr @@ -14,8 +14,8 @@ module Cox end end - def self.new - new(Random::Secure.random_bytes(NONCE_SIZE)) + def initialize + @bytes = Random::Secure.random_bytes(NONCE_SIZE) end end end diff --git a/src/cox/sign/public_key.cr b/src/cox/sign/public_key.cr index 163e859..02d5c31 100644 --- a/src/cox/sign/public_key.cr +++ b/src/cox/sign/public_key.cr @@ -2,16 +2,18 @@ require "../lib_sodium" module Cox class Sign::PublicKey < Key - property bytes : Bytes - KEY_SIZE = LibSodium::PUBLIC_SIGN_SIZE + getter bytes : Bytes + def initialize(@bytes : Bytes) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") end end + # Verify signature made by `secret_key.sign_detached(message)` + # Raises on verification failure. def verify_detached(message, sig : Bytes) verify_detached message.to_slice, sig end diff --git a/src/cox/sign/secret_key.cr b/src/cox/sign/secret_key.cr index 5e30eb2..705bedc 100644 --- a/src/cox/sign/secret_key.cr +++ b/src/cox/sign/secret_key.cr @@ -15,7 +15,7 @@ module Cox LibSodium.crypto_sign_keypair pkey, @bytes end - # Use existing Private and Public keys. + # Use existing Secret and Public keys. def initialize(@bytes : Bytes, pkey : Bytes) raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") @public_key = PublicKey.new pkey @@ -31,6 +31,8 @@ module Cox # Also needs to differentiate from seed as a single parameter # end + # Signs message and returns a detached signature. + # Verify using `secret_key.public_key.verify_detached(message, sig)` def sign_detached(message) sign_detached message.to_slice end