commit 2a69e4f15c7c276d681681411553af20237e9813 Author: Andrew Hamon Date: Tue Jul 11 22:13:52 2017 -0500 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0c87a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23ec656 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/doc/ +/lib/ +/bin/ +/.shards/ + +# Libraries don't need dependency lock +# Dependencies will be locked in application that uses them +/shard.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffc7b6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: crystal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6bd501b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Andrew Hamon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..33dd8f7 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# cox + +Crystal bindings for the [libsodium box API](https://download.libsodium.org/doc/public-key_cryptography/authenticated_encryption.html) + +Given a recipients public key, you can encrypt and sign a message for them. Upon +receipt, they can decrypt and authenticate the message as having come from you. + +## Installation + +Add this to your application's `shard.yml`: + +```yaml +dependencies: + cox: + github: andrewhamon/cox +``` + +## Usage + +```crystal +require "cox" + +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) # => "Hello World!" +``` + +## Contributing + +1. Fork it ( https://github.com/andrewhamon/cox/fork ) +2. Create your feature branch (git checkout -b my-new-feature) +3. Commit your changes (git commit -am 'Add some feature') +4. Push to the branch (git push origin my-new-feature) +5. Create a new Pull Request + +## Contributors + +- [andrewhamon](https://github.com/andrewhamon) Andrew Hamon - creator, maintainer diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..bc57988 --- /dev/null +++ b/shard.yml @@ -0,0 +1,9 @@ +name: cox +version: 0.1.0 + +authors: + - Andrew Hamon + +crystal: 0.23.0 + +license: MIT diff --git a/spec/cox_spec.cr b/spec/cox_spec.cr new file mode 100644 index 0000000..1195ac9 --- /dev/null +++ b/spec/cox_spec.cr @@ -0,0 +1,25 @@ +require "./spec_helper" + +describe Cox do + # TODO: Write tests + + it "works" 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 +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..21974ee --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/cox" diff --git a/src/cox.cr b/src/cox.cr new file mode 100644 index 0000000..21c93df --- /dev/null +++ b/src/cox.cr @@ -0,0 +1,30 @@ +require "./cox/*" +require "secure_random" + +module Cox + def self.encrypt(data, nonce : Nonce, recipient_public_key : PublicKey, sender_secret_key : SecretKey) + data_buffer = data.to_slice + data_size = data_buffer.bytesize + output_buffer = Bytes.new(data_buffer.bytesize + LibSodium::MAC_BYTES) + LibSodium.crypto_box_easy(output_buffer.to_unsafe, data_buffer, data_size, nonce.pointer, recipient_public_key.pointer, sender_secret_key.pointer) + output_buffer + end + + def self.encrypt(data, recipient_public_key : PublicKey, sender_secret_key : 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) + data_buffer = data.to_slice + data_size = data_buffer.bytesize + output_buffer = Bytes.new(data_buffer.bytesize - LibSodium::MAC_BYTES) + LibSodium.crypto_box_open_easy(output_buffer.to_unsafe, data_buffer.to_unsafe, data_size, nonce.pointer, sender_public_key.pointer, recipient_secret_key.pointer) + output_buffer + end +end + +if Cox::LibSodium.sodium_init() == -1 + STDERR.puts("Failed to init libsodium") + exit(1) +end diff --git a/src/cox/key.cr b/src/cox/key.cr new file mode 100644 index 0000000..73e1a89 --- /dev/null +++ b/src/cox/key.cr @@ -0,0 +1,21 @@ +module Cox + abstract class Key + abstract def bytes + + def pointer + bytes.to_unsafe + end + + def pointer(size) + bytes.pointer(size) + end + + def to_base64 + Base64.encode(bytes) + end + + def self.from_base64(encoded_key) + new(Base64.decode(encoded_key)) + end + end +end diff --git a/src/cox/key_pair.cr b/src/cox/key_pair.cr new file mode 100644 index 0000000..cfb0afd --- /dev/null +++ b/src/cox/key_pair.cr @@ -0,0 +1,25 @@ +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_LENGTH) + secret_key = Bytes.new(SecretKey::KEY_LENGTH) + + 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/lib_sodium.cr b/src/cox/lib_sodium.cr new file mode 100644 index 0000000..7522dbc --- /dev/null +++ b/src/cox/lib_sodium.cr @@ -0,0 +1,39 @@ +module Cox + @[Link("sodium")] + lib LibSodium + fun sodium_init() : LibC::Int + + fun crypto_box_publickeybytes() : LibC::SizeT + fun crypto_box_secretkeybytes() : LibC::SizeT + fun crypto_box_noncebytes() : LibC::SizeT + fun crypto_box_macbytes() : LibC::SizeT + + PUBLIC_KEY_BYTES = crypto_box_publickeybytes() + SECRET_KEY_BYTES = crypto_box_secretkeybytes() + NONCE_BYTES = crypto_box_macbytes() + MAC_BYTES = crypto_box_macbytes() + + fun crypto_box_keypair( + public_key_output : Pointer(LibC::UChar), + secret_key_output : Pointer(LibC::UChar) + ) + + fun crypto_box_easy( + output : Pointer(LibC::UChar), + data : Pointer(LibC::UChar), + data_size : LibC::ULongLong, + nonce : Pointer(LibC::UChar), + recipient_public_key : Pointer(LibC::UChar), + sender_secret_key : Pointer(LibC::UChar) + ) : LibC::Int + + fun crypto_box_open_easy( + output : Pointer(LibC::UChar), + data : Pointer(LibC::UChar), + data_size : LibC::ULongLong, + nonce : Pointer(LibC::UChar), + sender_public_key : Pointer(LibC::UChar), + recipient_secret_key : Pointer(LibC::UChar) + ) : LibC::Int + end +end diff --git a/src/cox/nonce.cr b/src/cox/nonce.cr new file mode 100644 index 0000000..a5d8382 --- /dev/null +++ b/src/cox/nonce.cr @@ -0,0 +1,28 @@ +require "./lib_sodium" +require "secure_random" + +module Cox + class Nonce + property bytes : Bytes + + NONCE_LENGTH = LibSodium::NONCE_BYTES + + def initialize(@bytes : Bytes) + if bytes.bytesize != NONCE_LENGTH + raise ArgumentError.new("Nonce must be #{NONCE_LENGTH} bytes, got #{bytes.bytesize}") + end + end + + def self.new + new(SecureRandom.random_bytes(NONCE_LENGTH)) + end + + def pointer + bytes.to_unsafe + end + + def pointer(size) + bytes.pointer(size) + end + end +end diff --git a/src/cox/public_key.cr b/src/cox/public_key.cr new file mode 100644 index 0000000..982a074 --- /dev/null +++ b/src/cox/public_key.cr @@ -0,0 +1,15 @@ +require "./lib_sodium" + +module Cox + class PublicKey < Key + property bytes : Bytes + + KEY_LENGTH = LibSodium::PUBLIC_KEY_BYTES + + def initialize(@bytes : Bytes) + if bytes.bytesize != KEY_LENGTH + raise ArgumentError.new("Public key must be #{KEY_LENGTH} bytes, got #{bytes.bytesize}") + end + end + end +end diff --git a/src/cox/secret_key.cr b/src/cox/secret_key.cr new file mode 100644 index 0000000..592dd93 --- /dev/null +++ b/src/cox/secret_key.cr @@ -0,0 +1,15 @@ +require "./lib_sodium" + +module Cox + class SecretKey < Key + property bytes : Bytes + + KEY_LENGTH = LibSodium::SECRET_KEY_BYTES + + def initialize(@bytes : Bytes) + if bytes.bytesize != KEY_LENGTH + raise ArgumentError.new("Secret key must be #{KEY_LENGTH} bytes, got #{bytes.bytesize}") + end + end + end +end diff --git a/src/cox/version.cr b/src/cox/version.cr new file mode 100644 index 0000000..9e380cd --- /dev/null +++ b/src/cox/version.cr @@ -0,0 +1,3 @@ +module Cox + VERSION = "0.1.0" +end