├── .coveralls.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .yardopts ├── CHANGES.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── images ├── hash.png └── logo.png ├── lib ├── rbnacl.rb └── rbnacl │ ├── aead │ ├── base.rb │ ├── chacha20poly1305_ietf.rb │ ├── chacha20poly1305_legacy.rb │ └── xchacha20poly1305_ietf.rb │ ├── auth.rb │ ├── boxes │ ├── curve25519xsalsa20poly1305.rb │ ├── curve25519xsalsa20poly1305 │ │ ├── private_key.rb │ │ └── public_key.rb │ └── sealed.rb │ ├── group_elements │ └── curve25519.rb │ ├── hash.rb │ ├── hash │ ├── blake2b.rb │ ├── sha256.rb │ └── sha512.rb │ ├── hmac │ ├── sha256.rb │ ├── sha512.rb │ └── sha512256.rb │ ├── init.rb │ ├── key_comparator.rb │ ├── one_time_auths │ └── poly1305.rb │ ├── password_hash.rb │ ├── password_hash │ ├── argon2.rb │ └── scrypt.rb │ ├── random.rb │ ├── secret_boxes │ └── xsalsa20poly1305.rb │ ├── self_test.rb │ ├── serializable.rb │ ├── signatures │ ├── ed25519.rb │ └── ed25519 │ │ ├── signing_key.rb │ │ └── verify_key.rb │ ├── simple_box.rb │ ├── sodium.rb │ ├── sodium │ └── version.rb │ ├── test_vectors.rb │ ├── util.rb │ └── version.rb ├── rbnacl.gemspec ├── spec ├── rbnacl │ ├── aead │ │ ├── chacha20poly1305_ietf_spec.rb │ │ ├── chacha20poly1305_legacy_spec.rb │ │ └── xchacha20poly1305_ietf_spec.rb │ ├── authenticators │ │ └── poly1305_spec.rb │ ├── boxes │ │ ├── curve25519xsalsa20poly1305 │ │ │ ├── private_key_spec.rb │ │ │ └── public_key_spec.rb │ │ ├── curve25519xsalsa20poly1305_spec.rb │ │ └── sealed_spec.rb │ ├── group_element_spec.rb │ ├── hash │ │ └── blake2b_spec.rb │ ├── hash_spec.rb │ ├── hmac │ │ ├── sha256_spec.rb │ │ ├── sha512256_spec.rb │ │ └── sha512_spec.rb │ ├── password_hash │ │ ├── argon2_spec.rb │ │ └── scrypt_spec.rb │ ├── random_spec.rb │ ├── secret_box_spec.rb │ ├── signatures │ │ └── ed25519 │ │ │ ├── signing_key_spec.rb │ │ │ └── verify_key_spec.rb │ ├── simple_box_spec.rb │ ├── sodium_spec.rb │ └── util_spec.rb ├── shared │ ├── aead.rb │ ├── authenticator.rb │ ├── box.rb │ ├── hmac.rb │ ├── key_equality.rb │ ├── sealed_box.rb │ └── serializable.rb └── spec_helper.rb └── tasks ├── rspec.rake └── rubocop.rake /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service-name: travis-pro 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | BUNDLE_WITHOUT: "development" 11 | JRUBY_OPTS: "--dev --debug" 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | ruby: 18 | - ruby-2.6 19 | - ruby-2.7 20 | - ruby-3.0 21 | - ruby-3.1 22 | - ruby-3.2 23 | - ruby-3.3 24 | - ruby-3.4 25 | - jruby-9.4.12 26 | - jruby-10.0.0 27 | os: [ ubuntu-latest ] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{ matrix.ruby }} 34 | bundler-cache: true 35 | - run: bundle exec rspec --format progress 36 | 37 | lint: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: ruby/setup-ruby@v1 42 | with: 43 | ruby-version: 2.6 44 | bundler-cache: true 45 | - run: bundle exec rubocop --format progress --color 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | libsodium 19 | libsodium-*.tar.gz 20 | .rakeTasks 21 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --backtrace 4 | --order random 5 | --warnings 6 | --require spec_helper 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.6 3 | DisplayCopNames: true 4 | Exclude: 5 | - 'vendor/**/*' 6 | 7 | # 8 | # Layout 9 | # 10 | 11 | Metrics/LineLength: 12 | Max: 128 13 | 14 | # 15 | # Metrics 16 | # 17 | 18 | Metrics/AbcSize: 19 | Max: 20 20 | 21 | Metrics/BlockLength: 22 | Max: 128 23 | ExcludedMethods: [ 'describe' ] 24 | 25 | Metrics/ClassLength: 26 | Max: 128 27 | 28 | Metrics/MethodLength: 29 | CountComments: false 30 | Max: 50 31 | 32 | Metrics/CyclomaticComplexity: 33 | Max: 15 34 | 35 | Metrics/PerceivedComplexity: 36 | Max: 15 37 | 38 | # 39 | # Naming 40 | # 41 | 42 | Naming/UncommunicativeMethodParamName: 43 | Enabled: false 44 | 45 | # 46 | # Style 47 | # 48 | 49 | Style/AccessModifierDeclarations: 50 | Enabled: false 51 | 52 | Style/GlobalVars: 53 | Enabled: false 54 | 55 | Style/SafeNavigation: 56 | Enabled: false 57 | 58 | Style/StringLiterals: 59 | EnforcedStyle: double_quotes 60 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown --no-private lib/**/*.rb - README.md LICENSE.txt 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## [7.1.2] (2024-10-15) 2 | 3 | - Test on Ruby 3.3 ([#236]) 4 | - Support alpine 3.19 and up ([#234]) 5 | - Use libsodium constants for scrypt params ([#231]) 6 | - Fix error message generation when empty hmac key is given ([#224]) 7 | - Add ed25519 to curve25519 conversion helpers ([#217]) 8 | 9 | ## [7.1.1] (2020-01-27) 10 | 11 | - Test on Ruby 2.7 ([#208]) 12 | - Add project metadata to the gemspec ([#207]) 13 | - Resolve FFI deprecation warning ([#206]) 14 | 15 | ## [7.1.0] (2019-09-07) 16 | 17 | - Attached signature API ([#197], [#202]) 18 | - Fix the `generichash` state definition ([#200]) 19 | 20 | ## [7.0.0] (2019-05-23) 21 | 22 | - Drop support for Ruby 2.2 ([#194]) 23 | 24 | ## [6.0.1] (2019-01-27) 25 | 26 | - Add fallback `sodium_constants` for Argon2 ([#189]) 27 | - Support libsodium versions used by Heroku ([#186]) 28 | - Sealed boxes ([#184]) 29 | 30 | ## [6.0.0] (2018-11-08) 31 | 32 | - Deprecate rbnacl-libsodium ([#180]) 33 | - Add support for XChaCha20-Poly1305 ([#176]) 34 | - Fix buffer size type in `randombytes_buf` binding ([#174]) 35 | - Add support for argon2id digest ([#174]) 36 | - Support for non-32-byte HMAC-SHA256/512 keys ([#166]) 37 | 38 | ## 5.0.0 (2017-06-13) 39 | 40 | - Support the BLAKE2b Initialize-Update-Finalize API ([#159]) 41 | 42 | ## 4.0.2 (2017-03-12) 43 | 44 | - Raise error on degenerate keys. Fixes #152 ([#157]) 45 | 46 | ## 4.0.1 (2016-12-04) 47 | 48 | - Last minute changes to the ChaCha20Poly1305 API ([#148]) 49 | 50 | ## 4.0.0 (2016-12-04) 51 | 52 | - Add wrappers for ChaCha20Poly1305 AEAD ciphers ([#141]) 53 | - Added support for Argon2 password hash ([#142]) 54 | - Require Ruby 2.2.6+ ([#143]) 55 | 56 | ## 3.4.0 (2015-05-07) 57 | 58 | - Expose `RbNaCl::Signatures::Ed25519#keypair_bytes` ([#135]) 59 | - Expose HMAC-SHA512 with 64-byte keys ([#137]) 60 | 61 | ## 3.3.0 (2015-12-29) 62 | 63 | - Remove use of Thread.exclusive when initializing library ([#128]) 64 | - Add salt/personalisation strings for Blake2b ([#105]) 65 | 66 | ## 3.2.0 (2015-05-31) 67 | 68 | - Fix method signature for blake2b 69 | - RuboCop-friendly codebase 70 | 71 | ## 3.1.2 (2014-08-30) 72 | 73 | - Fix scrypt support with libsodium 0.7.0 (scryptsalsa208sha256) 74 | 75 | ## 3.1.1 (2014-06-14) 76 | 77 | - Fix undefined variable warning 78 | - RSpec 3 fixups 79 | - RuboCop 80 | 81 | ## 3.1.0 (2014-05-22) 82 | 83 | - The scrypt password hashing function: `RbNaCl::PasswordHash.scrypt` 84 | 85 | ## 3.0.1 (2014-05-12) 86 | 87 | - Load gem from `RBNACL_LIBSODIUM_GEM_LIB_PATH` if set. Used by rbnacl-libsodium 88 | gem to use libsodium compiled from a gem. 89 | 90 | ## 3.0.0 (2014-04-22) 91 | 92 | - Rename RandomNonceBox to SimpleBox (backwards compatibility preserved) 93 | - Reverse documented order of SimpleBox/RandomNonceBox initialize parameters. 94 | Technically backwards compatible, but confusing. 95 | - Ensure all strings are ASCII-8BIT/BINARY encoding prior to use 96 | 97 | ## 2.0.0 (2013-11-07) 98 | 99 | - Rename Crypto module to RbNaCl module 100 | - Add encrypt/decrypt aliases for `Crypto::RandomNonceBox` 101 | - `RbNaCl::VerifyKey#verify` operand order was reversed. New operand order is 102 | signature, message instead of message, signature 103 | - `RbNaCL::SecretBox#open`, `RbNaCl::Box#open`, `Auth#verify` and 104 | `VerifyKey#verify` all now raise a (descendent of) CryptoError if the check 105 | fails. This ensures failures are handled by the program. 106 | - `RbNaCl::SecretBox`, Box, etc. are all now aliases for the real 107 | implementations, which are named after the primitives they provide 108 | - Removed encoder functionality. 109 | - Add support for the Blake2b cryptographic hash algorithm. 110 | - Add checks that we have a sufficiently recent version of libsodium (0.4.3+) 111 | - Dropped ruby-1.8 support 112 | - Call the `sodium_init()` function, to select the best algorithms. 113 | - Fix some typos in the documentation 114 | - Changes in the low level binding for libsodium and removal of the NaCl module 115 | - Add a mutex around calls to randombytes in libsodium 116 | 117 | ## 1.1.0 (2013-04-19) 118 | 119 | - Provide API for querying primitives and details about them, such as key 120 | lengths, nonce lengths, etc. 121 | - Fixed bug on passing null bytes to sha256, sha512 functions. 122 | 123 | ## 1.0.0 (2013-03-08) 124 | 125 | - Initial release 126 | 127 | [7.1.2]: https://github.com/RubyCrypto/rbnacl/pull/240 128 | [#236]: https://github.com/RubyCrypto/rbnacl/pull/236 129 | [#234]: https://github.com/RubyCrypto/rbnacl/pull/234 130 | [#231]: https://github.com/RubyCrypto/rbnacl/pull/231 131 | [#224]: https://github.com/RubyCrypto/rbnacl/pull/224 132 | [#217]: https://github.com/RubyCrypto/rbnacl/pull/217 133 | [7.1.1]: https://github.com/RubyCrypto/rbnacl/pull/210 134 | [#208]: https://github.com/RubyCrypto/rbnacl/pull/208 135 | [#207]: https://github.com/RubyCrypto/rbnacl/pull/207 136 | [#206]: https://github.com/RubyCrypto/rbnacl/pull/206 137 | [7.1.0]: https://github.com/RubyCrypto/rbnacl/pull/203 138 | [#202]: https://github.com/RubyCrypto/rbnacl/pull/202 139 | [#200]: https://github.com/RubyCrypto/rbnacl/pull/200 140 | [#197]: https://github.com/RubyCrypto/rbnacl/pull/197 141 | [7.0.0]: https://github.com/RubyCrypto/rbnacl/pull/195 142 | [#194]: https://github.com/RubyCrypto/rbnacl/pull/194 143 | [6.0.1]: https://github.com/RubyCrypto/rbnacl/pull/191 144 | [#189]: https://github.com/RubyCrypto/rbnacl/pull/189 145 | [#186]: https://github.com/RubyCrypto/rbnacl/pull/186 146 | [#184]: https://github.com/RubyCrypto/rbnacl/pull/184 147 | [6.0.0]: https://github.com/RubyCrypto/rbnacl/pull/182 148 | [#180]: https://github.com/RubyCrypto/rbnacl/pull/180 149 | [#176]: https://github.com/RubyCrypto/rbnacl/pull/176 150 | [#174]: https://github.com/RubyCrypto/rbnacl/pull/174 151 | [#172]: https://github.com/RubyCrypto/rbnacl/pull/172 152 | [#166]: https://github.com/RubyCrypto/rbnacl/pull/166 153 | [#159]: https://github.com/RubyCrypto/rbnacl/pull/159 154 | [#157]: https://github.com/RubyCrypto/rbnacl/pull/157 155 | [#148]: https://github.com/RubyCrypto/rbnacl/pull/148 156 | [#143]: https://github.com/RubyCrypto/rbnacl/pull/143 157 | [#142]: https://github.com/RubyCrypto/rbnacl/pull/142 158 | [#141]: https://github.com/RubyCrypto/rbnacl/pull/141 159 | [#137]: https://github.com/RubyCrypto/rbnacl/pull/137 160 | [#135]: https://github.com/RubyCrypto/rbnacl/pull/135 161 | [#128]: https://github.com/RubyCrypto/rbnacl/pull/128 162 | [#105]: https://github.com/RubyCrypto/rbnacl/pull/105 163 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem "guard-rspec" 9 | end 10 | 11 | group :test do 12 | gem "coveralls", require: false 13 | gem "rspec" 14 | gem "rubocop", "= 0.70.0" 15 | end 16 | 17 | group :development, :test do 18 | gem "rake" 19 | end 20 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | guard :rspec do 7 | watch(%r{^spec/.+_spec\.rb$}) 8 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 9 | watch("spec/spec_helper.rb") { "spec" } 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2018 Tony Arcieri, Jonathan Stott 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RbNaCl](https://raw.github.com/RubyCrypto/rbnacl/master/images/logo.png) 2 | ====== 3 | [![Gem Version](https://badge.fury.io/rb/rbnacl.svg)](http://badge.fury.io/rb/rbnacl) 4 | [![Build Status](https://github.com/RubyCrypto/rbnacl/actions/workflows/ci.yml/badge.svg)](https://github.com/RubyCrypto/rbnacl/actions/workflows/ci.yml) 5 | [![Coverage Status](https://coveralls.io/repos/RubyCrypto/rbnacl/badge.svg?branch=master)](https://coveralls.io/r/RubyCrypto/rbnacl) 6 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RubyCrypto/rbnacl/blob/master/LICENSE.txt) 7 | 8 | Ruby binding for [libsodium], a fork of the [Networking and Cryptography][NaCl] 9 | library. 10 | 11 | [libsodium]: https://libsodium.org 12 | [NaCl]: http://nacl.cr.yp.to/ 13 | 14 | ## Why libsodium/NaCl? 15 | 16 | NaCl is a different kind of cryptographic library. In the past crypto 17 | libraries were kitchen sinks of little bits and pieces, like ciphers, 18 | MACs, signature algorithms, and hash functions. To accomplish anything 19 | you had to make a lot of decisions about which specific pieces to use, 20 | and if any of your decisions were wrong, the result was an insecure 21 | system. The choices are also not easy: EAX? GCM? CCM? AES-CTR? CMAC? 22 | OMAC1? AEAD? NIST? CBC? CFB? CTR? ECB? OMGWTFBBQ! 23 | 24 | NaCl puts cryptography on Rails! Instead of making you choose which 25 | cryptographic primitives to use, NaCl provides convention over configuration 26 | in the form of expertly-assembled high-level cryptographic APIs that ensure 27 | not only the confidentiality of your data, but also detect tampering. 28 | These high-level, easy-to-use APIs are designed to be hard to attack by 29 | default in ways primitives exposed by libraries like OpenSSL are not. 30 | 31 | This approach makes NaCl a lot closer to a system like GPG than it is 32 | to the cryptographic primitive APIs in a library like OpenSSL. In addition, 33 | NaCl also uses state-of-the-art encryption, including Curve25519 elliptic 34 | curves and the XSalsa20 stream cipher. This means with NaCl you not only get 35 | a system which is designed to be secure-by-default, you also get one which 36 | is extremely fast with comparatively small cryptographic keys. 37 | 38 | ### Is it any good? 39 | 40 | [Yes.](http://news.ycombinator.com/item?id=3067434) 41 | 42 | ## Supported platforms 43 | 44 | You can use RbNaCl on platforms libsodium is supported (see below). 45 | 46 | This library aims to support and is [tested against][github-action] the following Ruby 47 | versions: 48 | 49 | * Ruby 2.6 50 | * Ruby 2.7 51 | * Ruby 3.0 52 | * Ruby 3.1 53 | * Ruby 3.2 54 | * Ruby 3.3 55 | * Ruby 3.4 56 | * JRuby 9.4 57 | * JRuby 10.0 58 | 59 | If something doesn't work on one of these versions, it's a bug. 60 | 61 | [github-action]: https://github.com/RubyCrypto/rbnacl/actions 62 | 63 | ## Installation 64 | 65 | Note: [Windows installation instructions are available](https://github.com/RubyCrypto/rbnacl/wiki/Installing-libsodium#windows). 66 | 67 | ### libsodium 68 | 69 | To use RbNaCl, you will need to install libsodium: 70 | 71 | https://github.com/jedisct1/libsodium 72 | 73 | At least version `1.0.0` is required. 74 | 75 | For OS X users, libsodium is available via homebrew and can be installed with: 76 | 77 | brew install libsodium 78 | 79 | For FreeBSD users, libsodium is available both via pkgng and ports. To install 80 | a binary package: 81 | 82 | pkg install libsodium 83 | 84 | To install from ports on FreeBSD, use your favorite ports front end (e.g. 85 | portmaster or portupgrade), or use make as follows: 86 | 87 | cd /usr/ports/security/libsodium; make install clean 88 | 89 | ### RbNaCl gem 90 | 91 | Once you have libsodium installed, add this line to your application's Gemfile: 92 | 93 | gem 'rbnacl' 94 | 95 | And then execute: 96 | 97 | $ bundle 98 | 99 | Or install it yourself as: 100 | 101 | $ gem install rbnacl 102 | 103 | Inside of your Ruby program do: 104 | 105 | require 'rbnacl' 106 | 107 | ...to pull it in as a dependency. 108 | 109 | ## Documentation 110 | 111 | RbNaCl's documentation can be found [in the Wiki][wiki]. The following features 112 | are supported: 113 | 114 | * [SimpleBox]: easy-to-use public-key or secret-key encryption "on Rails" 115 | * [Secret-key Encryption][secretkey]: authenticated symmetric encryption using a 116 | single key shared among parties 117 | * [Public-key Encryption][publickey]: securely send messages to a given public 118 | key which can only be decrypted by a secret key 119 | * [Digital Signatures][signatures]: sign messages with a private key which can 120 | be verified by a public one 121 | * [Authenticators][macs]: create codes which can be used to check the 122 | authenticity of messages 123 | * [Hash Functions][hashes]: compute a secure, fixed-length code from a message 124 | which does not reveal the contents of the message 125 | 126 | Additional power-user features are available. Please see the Wiki for further 127 | information. 128 | 129 | [YARD API documentation][yard] is also available. 130 | 131 | [wiki]: https://github.com/RubyCrypto/rbnacl/wiki 132 | [simplebox]: https://github.com/RubyCrypto/rbnacl/wiki/SimpleBox 133 | [secretkey]: https://github.com/RubyCrypto/rbnacl/wiki/Secret-Key-Encryption 134 | [publickey]: https://github.com/RubyCrypto/rbnacl/wiki/Public-Key-Encryption 135 | [signatures]: https://github.com/RubyCrypto/rbnacl/wiki/Digital-Signatures 136 | [macs]: https://github.com/RubyCrypto/rbnacl/wiki/HMAC 137 | [hashes]: https://github.com/RubyCrypto/rbnacl/wiki/Hash-Functions 138 | [yard]: http://www.rubydoc.info/gems/rbnacl 139 | 140 | ## Learn More 141 | 142 | While NaCl has designed to be easier-than-usual to use for a crypto 143 | library, cryptography is an incredibly difficult subject and it's 144 | always helpful to know as much as you can about it before applying 145 | it to a particular use case. That said, the creator of NaCl, Dan 146 | Bernstein, has published a number of papers about NaCl. If you are 147 | interested in learning more about how NaCl works, it's recommended 148 | that you read them: 149 | 150 | * [Cryptography in NaCl](http://cr.yp.to/highspeed/naclcrypto-20090310.pdf) 151 | * [Salsa20 Design](https://cr.yp.to/snuffle/design.pdf) 152 | * [Curve25519: new Diffie-Hellman speed records](http://cr.yp.to/ecdh/curve25519-20060209.pdf) 153 | * [Ed25519: High-speed high-security signatures](http://ed25519.cr.yp.to/ed25519-20110926.pdf) 154 | 155 | For more information on libsodium, please check out the 156 | [Introducing Sodium blog post](http://labs.umbrella.com/2013/03/06/announcing-sodium-a-new-cryptographic-library/) 157 | 158 | Have a general interest in cryptography? Check out the free course 159 | Coursera offers from Stanford University Professor Dan Boneh: 160 | 161 | [http://crypto-class.org](http://crypto-class.org) 162 | 163 | ## Important Questions 164 | 165 | ### Is it "Military Grade™"? 166 | 167 | Only if your military understands twisted Edwards curves 168 | 169 | ### Is it "Bank Grade™"? 170 | 171 | No, that means 3DES, which this library doesn't support, sorry 172 | 173 | ### Does it have a lock with a checkmark? 174 | 175 | Sure, here you go: 176 | 177 | ![Checkmarked Lock](http://i.imgur.com/dwA0Ffi.png) 178 | 179 | ## Contributing 180 | 181 | * Fork this repository on Github 182 | * Make your changes and send a pull request 183 | * If your changes look good, we'll merge 'em 184 | 185 | ## License 186 | 187 | Copyright (c) 2012-2018 Tony Arcieri, Jonathan Stott. Distributed under the MIT License. 188 | See [LICENSE.txt] for further details. 189 | 190 | [LICENSE.txt]: https://github.com/RubyCrypto/rbnacl/blob/master/LICENSE.txt 191 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | Dir[File.expand_path("tasks/**/*.rake", __dir__)].each { |task| load task } 6 | 7 | task default: %w[spec rubocop] 8 | task ci: %w[spec rubocop] 9 | -------------------------------------------------------------------------------- /images/hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyCrypto/rbnacl/4d50984003e123c9596fda58891bca0f3d1ab293/images/hash.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyCrypto/rbnacl/4d50984003e123c9596fda58891bca0f3d1ab293/images/logo.png -------------------------------------------------------------------------------- /lib/rbnacl.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | if defined?(RBNACL_LIBSODIUM_GEM_LIB_PATH) 5 | raise "rbnacl-libsodium is not supported by rbnacl 6.0+. "\ 6 | "Please remove it as a dependency and install libsodium using your system package manager. "\ 7 | "See https://github.com/RubyCrypto/rbnacl#installation" 8 | end 9 | 10 | require "rbnacl/version" 11 | require "rbnacl/sodium" 12 | require "rbnacl/sodium/version" 13 | require "rbnacl/serializable" 14 | require "rbnacl/key_comparator" 15 | require "rbnacl/auth" 16 | require "rbnacl/util" 17 | require "rbnacl/random" 18 | require "rbnacl/simple_box" 19 | require "rbnacl/test_vectors" 20 | require "rbnacl/init" 21 | require "rbnacl/aead/base" 22 | 23 | # NaCl/libsodium for Ruby 24 | module RbNaCl 25 | # Oh no, something went wrong! 26 | # 27 | # This indicates a failure in the operation of a cryptographic primitive such 28 | # as authentication failing on an attempt to decrypt a ciphertext. Classes 29 | # in the library may define more specific subclasses. 30 | class CryptoError < StandardError; end 31 | 32 | # Something, probably a key, is the wrong length 33 | # 34 | # This indicates some argument with an expected length was not that length. 35 | # Since this is probably a cryptographic key, you should check that! 36 | class LengthError < ArgumentError; end 37 | 38 | # An incorrect primitive has been passed to a method 39 | # 40 | # This indicates that an attempt has been made to use something (probably a key) 41 | # with an incorrect primitive 42 | class IncorrectPrimitiveError < ArgumentError; end 43 | 44 | # The signature was forged or otherwise corrupt 45 | class BadSignatureError < CryptoError; end 46 | 47 | # The authenticator was forged or otherwise corrupt 48 | class BadAuthenticatorError < CryptoError; end 49 | 50 | # Public Key Encryption (Box): Curve25519XSalsa20Poly1305 51 | require "rbnacl/boxes/curve25519xsalsa20poly1305" 52 | require "rbnacl/boxes/curve25519xsalsa20poly1305/private_key" 53 | require "rbnacl/boxes/curve25519xsalsa20poly1305/public_key" 54 | 55 | # Sealed boxes 56 | require "rbnacl/boxes/sealed" 57 | 58 | # Secret Key Encryption (SecretBox): XSalsa20Poly1305 59 | require "rbnacl/secret_boxes/xsalsa20poly1305" 60 | 61 | # Digital Signatures: Ed25519 62 | require "rbnacl/signatures/ed25519" 63 | require "rbnacl/signatures/ed25519/signing_key" 64 | require "rbnacl/signatures/ed25519/verify_key" 65 | 66 | # Group Elements: Curve25519 67 | require "rbnacl/group_elements/curve25519" 68 | 69 | # One-time Authentication: Poly1305 70 | require "rbnacl/one_time_auths/poly1305" 71 | 72 | # Hash functions: SHA256/512 and Blake2b 73 | require "rbnacl/hash" 74 | require "rbnacl/hash/sha256" 75 | require "rbnacl/hash/sha512" 76 | require "rbnacl/hash/blake2b" 77 | 78 | # Password hash functions 79 | require "rbnacl/password_hash" 80 | require "rbnacl/password_hash/scrypt" 81 | require "rbnacl/password_hash/argon2" if RbNaCl::Sodium::Version::ARGON2_SUPPORTED 82 | 83 | # HMAC: SHA256/512 and SHA512256 84 | require "rbnacl/hmac/sha256" 85 | require "rbnacl/hmac/sha512256" 86 | require "rbnacl/hmac/sha512" 87 | 88 | # AEAD: ChaCha20-Poly1305 89 | require "rbnacl/aead/chacha20poly1305_legacy" 90 | require "rbnacl/aead/chacha20poly1305_ietf" 91 | require "rbnacl/aead/xchacha20poly1305_ietf" 92 | 93 | # 94 | # Bind aliases used by the public API 95 | # 96 | Box = Boxes::Curve25519XSalsa20Poly1305 97 | PrivateKey = Boxes::Curve25519XSalsa20Poly1305::PrivateKey 98 | PublicKey = Boxes::Curve25519XSalsa20Poly1305::PublicKey 99 | SealedBox = Boxes::Sealed 100 | SecretBox = SecretBoxes::XSalsa20Poly1305 101 | SigningKey = Signatures::Ed25519::SigningKey 102 | VerifyKey = Signatures::Ed25519::VerifyKey 103 | GroupElement = GroupElements::Curve25519 104 | OneTimeAuth = OneTimeAuths::Poly1305 105 | end 106 | 107 | # Select platform-optimized versions of algorithms 108 | RbNaCl::Init.sodium_init 109 | 110 | # Perform self test on load 111 | require "rbnacl/self_test" unless defined?($RBNACL_SELF_TEST) && $RBNACL_SELF_TEST == false 112 | -------------------------------------------------------------------------------- /lib/rbnacl/aead/base.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module AEAD 6 | # Abstract base class for Authenticated Encryption with Additional Data 7 | # 8 | # This construction encrypts a message, and computes an authentication 9 | # tag for the encrypted message and some optional additional data 10 | # 11 | # RbNaCl provides wrappers for both ChaCha20-Poly1305 AEAD implementations 12 | # in libsodium: the original, and the IETF version. 13 | class Base 14 | # Number of bytes in a valid key 15 | KEYBYTES = 0 16 | 17 | # Number of bytes in a valid nonce 18 | NPUBBYTES = 0 19 | 20 | attr_reader :key 21 | private :key 22 | 23 | # Create a new AEAD using the IETF chacha20poly1305 construction 24 | # 25 | # Sets up AEAD with a secret key for encrypting and decrypting messages. 26 | # 27 | # @param key [String] The key to encrypt and decrypt with 28 | # 29 | # @raise [RbNaCl::LengthError] on invalid keys 30 | # 31 | # @return [RbNaCl::AEAD::Chacha20Poly1305IETF] The new AEAD construct, ready to use 32 | def initialize(key) 33 | @key = Util.check_string(key, key_bytes, "Secret key") 34 | end 35 | 36 | # Encrypts and authenticates a message with additional authenticated data 37 | # 38 | # @param nonce [String] An 8-byte string containing the nonce. 39 | # @param message [String] The message to be encrypted. 40 | # @param additional_data [String] The additional authenticated data 41 | # 42 | # @raise [RbNaCl::LengthError] If the nonce is not valid 43 | # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. 44 | # 45 | # @return [String] The encrypted message with the authenticator tag appended 46 | def encrypt(nonce, message, additional_data) 47 | Util.check_length(nonce, nonce_bytes, "Nonce") 48 | 49 | ciphertext_len = Util.zeros(1) 50 | ciphertext = Util.zeros(data_len(message) + tag_bytes) 51 | 52 | success = do_encrypt(ciphertext, ciphertext_len, nonce, message, additional_data) 53 | raise CryptoError, "Encryption failed" unless success 54 | 55 | ciphertext 56 | end 57 | 58 | # Decrypts and verifies an encrypted message with additional authenticated data 59 | # 60 | # @param nonce [String] An 8-byte string containing the nonce. 61 | # @param ciphertext [String] The message to be decrypted. 62 | # @param additional_data [String] The additional authenticated data 63 | # 64 | # @raise [RbNaCl::LengthError] If the nonce is not valid 65 | # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. 66 | # 67 | # @return [String] The decrypted message 68 | def decrypt(nonce, ciphertext, additional_data) 69 | Util.check_length(nonce, nonce_bytes, "Nonce") 70 | 71 | message_len = Util.zeros(1) 72 | message = Util.zeros(data_len(ciphertext) - tag_bytes) 73 | 74 | success = do_decrypt(message, message_len, nonce, ciphertext, additional_data) 75 | raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success 76 | 77 | message 78 | end 79 | 80 | # The crypto primitive for this aead instance 81 | # 82 | # @return [Symbol] The primitive used 83 | def primitive 84 | self.class.primitive 85 | end 86 | 87 | # The nonce bytes for the AEAD class 88 | # 89 | # @return [Integer] The number of bytes in a valid nonce 90 | def self.nonce_bytes 91 | self::NPUBBYTES 92 | end 93 | 94 | # The nonce bytes for the AEAD instance 95 | # 96 | # @return [Integer] The number of bytes in a valid nonce 97 | def nonce_bytes 98 | self.class.nonce_bytes 99 | end 100 | 101 | # The key bytes for the AEAD class 102 | # 103 | # @return [Integer] The number of bytes in a valid key 104 | def self.key_bytes 105 | self::KEYBYTES 106 | end 107 | 108 | # The key bytes for the AEAD instance 109 | # 110 | # @return [Integer] The number of bytes in a valid key 111 | def key_bytes 112 | self.class.key_bytes 113 | end 114 | 115 | # The number bytes in the tag or authenticator from this AEAD class 116 | # 117 | # @return [Integer] number of tag bytes 118 | def self.tag_bytes 119 | self::ABYTES 120 | end 121 | 122 | # The number of bytes in the tag or authenticator for this AEAD instance 123 | # 124 | # @return [Integer] number of tag bytes 125 | def tag_bytes 126 | self.class.tag_bytes 127 | end 128 | 129 | private 130 | 131 | def data_len(data) 132 | return 0 if data.nil? 133 | 134 | data.bytesize 135 | end 136 | 137 | def do_encrypt(_ciphertext, _ciphertext_len, _nonce, _message, _additional_data) 138 | raise NotImplementedError 139 | end 140 | 141 | def do_decrypt(_message, _message_len, _nonce, _ciphertext, _additional_data) 142 | raise NotImplementedError 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/rbnacl/aead/chacha20poly1305_ietf.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module AEAD 6 | # This class contains wrappers for the IETF implementation of 7 | # Authenticated Encryption with Additional Data using ChaCha20-Poly1305 8 | class ChaCha20Poly1305IETF < RbNaCl::AEAD::Base 9 | extend Sodium 10 | if Sodium::Version.supported_version?("1.0.9") 11 | sodium_type :aead 12 | sodium_primitive :chacha20poly1305_ietf 13 | 14 | sodium_constant :KEYBYTES 15 | sodium_constant :NPUBBYTES 16 | sodium_constant :ABYTES 17 | 18 | sodium_function :aead_chacha20poly1305_ietf_encrypt, 19 | :crypto_aead_chacha20poly1305_ietf_encrypt, 20 | %i[pointer pointer pointer ulong_long pointer ulong_long pointer pointer pointer] 21 | 22 | sodium_function :aead_chacha20poly1305_ietf_decrypt, 23 | :crypto_aead_chacha20poly1305_ietf_decrypt, 24 | %i[pointer pointer pointer pointer ulong_long pointer ulong_long pointer pointer] 25 | 26 | private 27 | 28 | def do_encrypt(ciphertext, ciphertext_len, nonce, message, additional_data) 29 | self.class.aead_chacha20poly1305_ietf_encrypt(ciphertext, ciphertext_len, 30 | message, data_len(message), 31 | additional_data, data_len(additional_data), 32 | nil, nonce, @key) 33 | end 34 | 35 | def do_decrypt(message, message_len, nonce, ciphertext, additional_data) 36 | self.class.aead_chacha20poly1305_ietf_decrypt(message, message_len, nil, 37 | ciphertext, data_len(ciphertext), 38 | additional_data, data_len(additional_data), 39 | nonce, @key) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rbnacl/aead/chacha20poly1305_legacy.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module AEAD 6 | # This class contains wrappers for the original libsodium implementation of 7 | # Authenticated Encryption with Additional Data using ChaCha20-Poly1305 8 | class ChaCha20Poly1305Legacy < RbNaCl::AEAD::Base 9 | extend Sodium 10 | 11 | sodium_type :aead 12 | sodium_primitive :chacha20poly1305 13 | sodium_constant :KEYBYTES 14 | sodium_constant :NPUBBYTES 15 | sodium_constant :ABYTES 16 | 17 | sodium_function :aead_chacha20poly1305_encrypt, 18 | :crypto_aead_chacha20poly1305_encrypt, 19 | %i[pointer pointer pointer ulong_long pointer ulong_long pointer pointer pointer] 20 | 21 | sodium_function :aead_chacha20poly1305_decrypt, 22 | :crypto_aead_chacha20poly1305_decrypt, 23 | %i[pointer pointer pointer pointer ulong_long pointer ulong_long pointer pointer] 24 | 25 | private 26 | 27 | def do_encrypt(ciphertext, ciphertext_len, nonce, message, additional_data) 28 | self.class.aead_chacha20poly1305_encrypt(ciphertext, ciphertext_len, 29 | message, data_len(message), 30 | additional_data, data_len(additional_data), 31 | nil, nonce, @key) 32 | end 33 | 34 | def do_decrypt(message, message_len, nonce, ciphertext, additional_data) 35 | self.class.aead_chacha20poly1305_decrypt(message, message_len, nil, 36 | ciphertext, data_len(ciphertext), 37 | additional_data, data_len(additional_data), 38 | nonce, @key) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/rbnacl/aead/xchacha20poly1305_ietf.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module AEAD 6 | # This class contains wrappers for the IETF implementation of 7 | # Authenticated Encryption with Additional Data using ChaCha20-Poly1305 8 | class XChaCha20Poly1305IETF < RbNaCl::AEAD::Base 9 | extend Sodium 10 | if Sodium::Version.supported_version?("1.0.12") 11 | sodium_type :aead 12 | sodium_primitive :xchacha20poly1305_ietf 13 | 14 | sodium_constant :KEYBYTES 15 | sodium_constant :NPUBBYTES 16 | sodium_constant :ABYTES 17 | 18 | sodium_function :aead_xchacha20poly1305_ietf_encrypt, 19 | :crypto_aead_xchacha20poly1305_ietf_encrypt, 20 | %i[pointer pointer pointer ulong_long pointer ulong_long pointer pointer pointer] 21 | 22 | sodium_function :aead_xchacha20poly1305_ietf_decrypt, 23 | :crypto_aead_xchacha20poly1305_ietf_decrypt, 24 | %i[pointer pointer pointer pointer ulong_long pointer ulong_long pointer pointer] 25 | 26 | private 27 | 28 | def do_encrypt(ciphertext, ciphertext_len, nonce, message, additional_data) 29 | self.class.aead_xchacha20poly1305_ietf_encrypt(ciphertext, ciphertext_len, 30 | message, data_len(message), 31 | additional_data, data_len(additional_data), 32 | nil, nonce, @key) 33 | end 34 | 35 | def do_decrypt(message, message_len, nonce, ciphertext, additional_data) 36 | self.class.aead_xchacha20poly1305_ietf_decrypt(message, message_len, nil, 37 | ciphertext, data_len(ciphertext), 38 | additional_data, data_len(additional_data), 39 | nonce, @key) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rbnacl/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Secret Key Authenticators 6 | # 7 | # These provide a means of verifying the integrity of a message, but only 8 | # with the knowledge of a shared key. This can be a preshared key, or one 9 | # that is derived through some cryptographic protocol. 10 | class Auth 11 | # Number of bytes in a valid key 12 | KEYBYTES = 0 13 | 14 | # Number of bytes in a valid authenticator 15 | BYTES = 0 16 | 17 | attr_reader :key 18 | private :key 19 | 20 | # A new authenticator, ready for auth and verification 21 | # 22 | # @param [#to_str] key the key used for authenticators, 32 bytes. 23 | def initialize(key) 24 | @key = Util.check_string(key, key_bytes, "#{self.class} key") 25 | end 26 | 27 | # Compute authenticator for message 28 | # 29 | # @param [#to_str] key the key used for the authenticator 30 | # @param [#to_str] message message to construct an authenticator for 31 | # 32 | # @return [String] The authenticator, as raw bytes 33 | def self.auth(key, message) 34 | new(key).auth(message) 35 | end 36 | 37 | # Verifies the given authenticator with the message. 38 | # 39 | # @param [#to_str] key the key used for the authenticator 40 | # @param [#to_str] authenticator to be checked 41 | # @param [#to_str] message the message to be authenticated 42 | # 43 | # @raise [BadAuthenticatorError] if the tag isn't valid 44 | # @raise [LengthError] if the tag is of the wrong length 45 | # 46 | # @return [Boolean] Was it valid? 47 | def self.verify(key, authenticator, message) 48 | new(key).verify(authenticator, message) 49 | end 50 | 51 | # Compute authenticator for message 52 | # 53 | # @param [#to_str] message the message to authenticate 54 | # 55 | # @return [String] the authenticator as raw bytes 56 | def auth(message) 57 | authenticator = Util.zeros(tag_bytes) 58 | message = message.to_str 59 | compute_authenticator(authenticator, message) 60 | authenticator 61 | end 62 | 63 | # Verifies the given authenticator with the message. 64 | # 65 | # @param [#to_str] authenticator to be checked 66 | # @param [#to_str] message the message to be authenticated 67 | # 68 | # @raise [BadAuthenticatorError] if the tag isn't valid 69 | # @raise [LengthError] if the tag is of the wrong length 70 | # 71 | # @return [Boolean] Was it valid? 72 | def verify(authenticator, message) 73 | auth = authenticator.to_s 74 | Util.check_length(auth, tag_bytes, "Provided authenticator") 75 | verify_message(auth, message) || raise(BadAuthenticatorError, "Invalid authenticator provided, message is corrupt") 76 | end 77 | 78 | # The crypto primitive for this authenticator instance 79 | # 80 | # @return [Symbol] The primitive used 81 | def primitive 82 | self.class.primitive 83 | end 84 | 85 | # The number of key bytes for this Auth class 86 | # 87 | # @return [Integer] number of key bytes 88 | def self.key_bytes 89 | self::KEYBYTES 90 | end 91 | 92 | # The number of key bytes for this Auth instance 93 | # 94 | # @return [Integer] number of key bytes 95 | def key_bytes 96 | self.class.key_bytes 97 | end 98 | 99 | # The number bytes in the tag or authenticator from this Auth class 100 | # 101 | # @return [Integer] number of tag bytes 102 | def self.tag_bytes 103 | self::BYTES 104 | end 105 | 106 | # The number of bytes in the tag or authenticator for this Auth instance 107 | # 108 | # @return [Integer] number of tag bytes 109 | def tag_bytes 110 | self.class.tag_bytes 111 | end 112 | 113 | private 114 | 115 | def compute_authenticator(_authenticator, _message) 116 | raise NotImplementedError 117 | end 118 | 119 | def verify_message(_authenticator, _message) 120 | raise NotImplementedError 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/rbnacl/boxes/curve25519xsalsa20poly1305.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Boxes 6 | # The Box class boxes and unboxes messages between a pair of keys 7 | # 8 | # This class uses the given public and secret keys to derive a shared key, 9 | # which is used with the nonce given to encrypt the given messages and 10 | # decrypt the given ciphertexts. The same shared key will generated from 11 | # both pairing of keys, so given two keypairs belonging to alice (pkalice, 12 | # skalice) and bob(pkbob, skbob), the key derived from (pkalice, skbob) with 13 | # equal that from (pkbob, skalice). This is how the system works: 14 | # 15 | # @example 16 | # # On bob's system 17 | # bobkey = RbNaCl::PrivateKey.generate 18 | # #=> # 19 | # 20 | # # send bobkey.public_key to alice 21 | # # receive alice's public key, alicepk 22 | # # NB: This is actually the hard part of the system. How to do it securely 23 | # # is left as an exercise to for the reader. 24 | # alice_pubkey = "..." 25 | # 26 | # # make a box 27 | # alicebob_box = RbNaCl::Box.new(alice_pubkey, bobkey) 28 | # #=> # 29 | # 30 | # # encrypt a message to alice 31 | # cipher_text = alicebob_box.box("A bad example of a nonce", "Hello, Alice!") 32 | # #=> "..." # a string of bytes, 29 bytes long 33 | # 34 | # # send ["A bad example of a nonce", cipher_text] to alice 35 | # # note that nonces don't have to be secret 36 | # # receive [nonce, cipher_text_to_bob] from alice 37 | # 38 | # # decrypt the reply 39 | # # Alice has been a little more sensible than bob, and has a random nonce 40 | # # that is too fiddly to type here. But there are other choices than just 41 | # # random 42 | # plain_text = alicebob_box.open(nonce, cipher_text_to_bob) 43 | # #=> "Hey there, Bob!" 44 | # 45 | # # we have a new message! 46 | # # But Eve has tampered with this message, by flipping some bytes around! 47 | # # [nonce2, cipher_text_to_bob_honest_love_eve] 48 | # alicebob_box.open(nonce2, cipher_text_to_bob_honest_love_eve) 49 | # 50 | # # BOOM! 51 | # # Bob gets a RbNaCl::CryptoError to deal with! 52 | # 53 | # It is VITALLY important that the nonce is a nonce, i.e. it is a number used 54 | # only once for any given pair of keys. If you fail to do this, you 55 | # compromise the privacy of the the messages encrypted. Also, bear in mind 56 | # the property mentioned just above. Give your nonces a different prefix, or 57 | # have one side use an odd counter and one an even counter. Just make sure 58 | # they are different. 59 | # 60 | # The ciphertexts generated by this class include a 16-byte authenticator which 61 | # is checked as part of the decryption. An invalid authenticator will cause 62 | # the unbox function to raise. The authenticator is not a signature. Once 63 | # you've looked in the box, you've demonstrated the ability to create 64 | # arbitrary valid messages, so messages you send are repudiable. For 65 | # non-repudiable messages, sign them before or after encryption. 66 | class Curve25519XSalsa20Poly1305 67 | extend Sodium 68 | 69 | sodium_type :box 70 | sodium_primitive :curve25519xsalsa20poly1305 71 | sodium_constant :NONCEBYTES 72 | sodium_constant :ZEROBYTES 73 | sodium_constant :BOXZEROBYTES 74 | sodium_constant :BEFORENMBYTES 75 | sodium_constant :PUBLICKEYBYTES 76 | sodium_constant :SECRETKEYBYTES, name: :PRIVATEKEYBYTES 77 | 78 | sodium_function :box_curve25519xsalsa20poly1305_beforenm, 79 | :crypto_box_curve25519xsalsa20poly1305_beforenm, 80 | %i[pointer pointer pointer] 81 | 82 | sodium_function :box_curve25519xsalsa20poly1305_open_afternm, 83 | :crypto_box_curve25519xsalsa20poly1305_open_afternm, 84 | %i[pointer pointer ulong_long pointer pointer] 85 | 86 | sodium_function :box_curve25519xsalsa20poly1305_afternm, 87 | :crypto_box_curve25519xsalsa20poly1305_afternm, 88 | %i[pointer pointer ulong_long pointer pointer] 89 | 90 | # Create a new Box 91 | # 92 | # Sets up the Box for deriving the shared key and encrypting and 93 | # decrypting messages. 94 | # 95 | # @param public_key [String,RbNaCl::PublicKey] The public key to encrypt to 96 | # @param private_key [String,RbNaCl::PrivateKey] The private key to encrypt with 97 | # 98 | # @raise [RbNaCl::LengthError] on invalid keys 99 | # 100 | # @return [RbNaCl::Box] The new Box, ready to use 101 | def initialize(public_key, private_key) 102 | @public_key = public_key.is_a?(PublicKey) ? public_key : PublicKey.new(public_key) 103 | @private_key = private_key.is_a?(PrivateKey) ? private_key : PrivateKey.new(private_key) 104 | raise IncorrectPrimitiveError unless @public_key.primitive == primitive && @private_key.primitive == primitive 105 | end 106 | 107 | # Encrypts a message 108 | # 109 | # Encrypts the message with the given nonce to the keypair set up when 110 | # initializing the class. Make sure the nonce is unique for any given 111 | # keypair, or you might as well just send plain text. 112 | # 113 | # This function takes care of the padding required by the NaCL C API. 114 | # 115 | # @param nonce [String] A 24-byte string containing the nonce. 116 | # @param message [String] The message to be encrypted. 117 | # 118 | # @raise [RbNaCl::LengthError] If the nonce is not valid 119 | # 120 | # @return [String] The ciphertext without the nonce prepended (BINARY encoded) 121 | def box(nonce, message) 122 | Util.check_length(nonce, nonce_bytes, "Nonce") 123 | msg = Util.prepend_zeros(ZEROBYTES, message) 124 | ct = Util.zeros(msg.bytesize) 125 | 126 | success = self.class.box_curve25519xsalsa20poly1305_afternm(ct, msg, msg.bytesize, nonce, beforenm) 127 | raise CryptoError, "Encryption failed" unless success 128 | 129 | Util.remove_zeros(BOXZEROBYTES, ct) 130 | end 131 | alias encrypt box 132 | 133 | # Decrypts a ciphertext 134 | # 135 | # Decrypts the ciphertext with the given nonce using the keypair setup when 136 | # initializing the class. 137 | # 138 | # This function takes care of the padding required by the NaCL C API. 139 | # 140 | # @param nonce [String] A 24-byte string containing the nonce. 141 | # @param ciphertext [String] The message to be decrypted. 142 | # 143 | # @raise [RbNaCl::LengthError] If the nonce is not valid 144 | # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. 145 | # 146 | # @return [String] The decrypted message (BINARY encoded) 147 | def open(nonce, ciphertext) 148 | Util.check_length(nonce, nonce_bytes, "Nonce") 149 | ct = Util.prepend_zeros(BOXZEROBYTES, ciphertext) 150 | message = Util.zeros(ct.bytesize) 151 | 152 | success = self.class.box_curve25519xsalsa20poly1305_open_afternm(message, ct, ct.bytesize, nonce, beforenm) 153 | raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success 154 | 155 | Util.remove_zeros(ZEROBYTES, message) 156 | end 157 | alias decrypt open 158 | 159 | # The crypto primitive for the box class 160 | # 161 | # @return [Symbol] The primitive used 162 | def primitive 163 | self.class.primitive 164 | end 165 | 166 | # The nonce bytes for the box class 167 | # 168 | # @return [Integer] The number of bytes in a valid nonce 169 | def self.nonce_bytes 170 | NONCEBYTES 171 | end 172 | 173 | # The nonce bytes for the box instance 174 | # 175 | # @return [Integer] The number of bytes in a valid nonce 176 | def nonce_bytes 177 | NONCEBYTES 178 | end 179 | 180 | private 181 | 182 | def beforenm 183 | @beforenm ||= begin 184 | key = Util.zeros(BEFORENMBYTES) 185 | success = self.class.box_curve25519xsalsa20poly1305_beforenm(key, @public_key.to_s, @private_key.to_s) 186 | raise CryptoError, "Failed to derive shared key" unless success 187 | 188 | key 189 | end 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/rbnacl/boxes/curve25519xsalsa20poly1305/private_key.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Boxes 6 | class Curve25519XSalsa20Poly1305 7 | # RbNaCl::Box private key. Keep it safe 8 | # 9 | # This class generates and stores NaCL private keys, as well as providing a 10 | # reference to the public key associated with this private key, if that's 11 | # provided. 12 | # 13 | # Note that the documentation for NaCl refers to this as a secret key, but in 14 | # this library its a private key, to avoid confusing the issue with the 15 | # SecretBox, which does symmetric encryption. 16 | class PrivateKey 17 | include KeyComparator 18 | include Serializable 19 | 20 | extend Sodium 21 | 22 | sodium_type :box 23 | sodium_primitive :curve25519xsalsa20poly1305 24 | 25 | sodium_function :box_curve25519xsalsa20poly1305_keypair, 26 | :crypto_box_curve25519xsalsa20poly1305_keypair, 27 | %i[pointer pointer] 28 | 29 | # The size of the key, in bytes 30 | BYTES = Boxes::Curve25519XSalsa20Poly1305::PRIVATEKEYBYTES 31 | 32 | # Initializes a new PrivateKey for key operations. 33 | # 34 | # Takes the (optionally encoded) private key bytes. This class can then be 35 | # used for various key operations, including deriving the corresponding 36 | # PublicKey 37 | # 38 | # @param private_key [String] The private key 39 | # 40 | # @raise [TypeError] If the key is nil 41 | # @raise [RbNaCl::LengthError] If the key is not valid after decoding. 42 | # 43 | # @return A new PrivateKey 44 | def initialize(private_key) 45 | @private_key = Util.check_string(private_key, BYTES, "Private key") 46 | end 47 | 48 | # Generates a new keypair 49 | # 50 | # @raise [RbNaCl::CryptoError] if key generation fails, due to insufficient randomness. 51 | # 52 | # @return [RbNaCl::PrivateKey] A new private key, with the associated public key also set. 53 | def self.generate 54 | pk = Util.zeros(Boxes::Curve25519XSalsa20Poly1305::PUBLICKEYBYTES) 55 | sk = Util.zeros(Boxes::Curve25519XSalsa20Poly1305::PRIVATEKEYBYTES) 56 | box_curve25519xsalsa20poly1305_keypair(pk, sk) || raise(CryptoError, "Failed to generate a key pair") 57 | new(sk) 58 | end 59 | 60 | # The raw bytes of the key 61 | # 62 | # @return [String] the raw bytes. 63 | def to_bytes 64 | @private_key 65 | end 66 | 67 | # the public key associated with this private key 68 | # 69 | # @return [PublicKey] the key 70 | def public_key 71 | @public_key ||= PublicKey.new GroupElements::Curve25519.base.mult(to_bytes) 72 | end 73 | 74 | # The crypto primitive this PrivateKey is to be used for. 75 | # 76 | # @return [Symbol] The primitive 77 | def primitive 78 | self.class.primitive 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/rbnacl/boxes/curve25519xsalsa20poly1305/public_key.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Boxes 6 | class Curve25519XSalsa20Poly1305 7 | # RbNaCl::Box public key. Send it (securely!) to your friends. 8 | # 9 | # This class stores the NaCL public key, and provides some convenience 10 | # functions for working with it. 11 | class PublicKey 12 | include KeyComparator 13 | include Serializable 14 | 15 | # The size of the key, in bytes 16 | BYTES = Boxes::Curve25519XSalsa20Poly1305::PUBLICKEYBYTES 17 | 18 | # Initializes a new PublicKey for key operations. 19 | # 20 | # Takes the (optionally encoded) public key bytes. This can be shared with 21 | # many people and used to establish key pairs with their private key, for 22 | # the exchanging of messages using a RbNaCl::Box 23 | # 24 | # @param public_key [String] The public key 25 | # 26 | # @raise [RbNaCl::LengthError] If the key is not valid after decoding. 27 | # 28 | # @return A new PublicKey 29 | def initialize(public_key) 30 | @public_key = Util.check_string(public_key, BYTES, "Public key") 31 | end 32 | 33 | # The raw bytes of the key 34 | # 35 | # @return [String] the raw bytes. 36 | def to_bytes 37 | @public_key 38 | end 39 | 40 | # The crypto primitive the PublicKey class is to be used for 41 | # 42 | # @return [Symbol] The primitive 43 | def self.primitive 44 | :curve25519xsalsa20poly1305 45 | end 46 | 47 | # The crypto primitive this PublicKey is to be used for. 48 | # 49 | # @return [Symbol] The primitive 50 | def primitive 51 | self.class.primitive 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/rbnacl/boxes/sealed.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Boxes 6 | # Sealed boxes are designed to anonymously send messages to a recipient 7 | # given its public key. 8 | # 9 | # Only the recipient can decrypt these messages, using its private key. 10 | # While the recipient can verify the integrity of the message, it cannot 11 | # verify the identity of the sender. 12 | # 13 | # A message is encrypted using an ephemeral key pair, whose secret part 14 | # is destroyed right after the encryption process. 15 | # 16 | # Without knowing the secret key used for a given message, the sender 17 | # cannot decrypt its own message later. And without additional data, 18 | # a message cannot be correlated with the identity of its sender. 19 | class Sealed 20 | extend Sodium 21 | 22 | sodium_type :box 23 | sodium_constant :SEALBYTES 24 | sodium_primitive :curve25519xsalsa20poly1305 25 | 26 | sodium_function :box_seal, 27 | :crypto_box_seal, 28 | %i[pointer pointer ulong_long pointer] 29 | 30 | sodium_function :box_seal_open, 31 | :crypto_box_seal_open, 32 | %i[pointer pointer ulong_long pointer pointer] 33 | 34 | # WARNING: you should strongly prefer the from_private_key/from_public_key class methods. 35 | # 36 | # Create a new Sealed Box 37 | # 38 | # Sets up the Box for deriving the shared key and encrypting and 39 | # decrypting messages. 40 | # 41 | # @param public_key [String,RbNaCl::PublicKey] The public key to encrypt to 42 | # @param private_key [String,RbNaCl::PrivateKey] The private key to decrypt with 43 | # 44 | # @raise [RbNaCl::LengthError] on invalid keys 45 | # 46 | # @return [RbNaCl::SealedBox] The new Box, ready to use 47 | def initialize(public_key, private_key = nil) 48 | unless private_key.nil? 49 | @private_key = private_key.is_a?(PrivateKey) ? private_key : PrivateKey.new(private_key) 50 | raise IncorrectPrimitiveError unless @private_key.primitive == primitive 51 | 52 | public_key = @private_key.public_key if public_key.nil? 53 | end 54 | 55 | @public_key = public_key.is_a?(PublicKey) ? public_key : PublicKey.new(public_key) 56 | raise IncorrectPrimitiveError unless @public_key.primitive == primitive 57 | end 58 | 59 | # Create a new Sealed Box for decrypting 60 | # 61 | # Sets up the Box for decryption of new messages. 62 | # 63 | # @param private_key [String,RbNaCl::PrivateKey] The private key to decrypt with 64 | # 65 | # @raise [RbNaCl::LengthError] on invalid keys 66 | # 67 | # @return [RbNaCl::SealedBox] The new Box, ready to use 68 | def self.from_private_key(private_key) 69 | new(nil, private_key) 70 | end 71 | 72 | # Create a new Sealed Box for encrypting 73 | # 74 | # Sets up the Box for encryption of new messages. 75 | # 76 | # @param public_key [String,RbNaCl::PublicKey] The public key to encrypt to 77 | # 78 | # @raise [RbNaCl::LengthError] on invalid keys 79 | # 80 | # @return [RbNaCl::SealedBox] The new Box, ready to use 81 | def self.from_public_key(public_key) 82 | new(public_key, nil) 83 | end 84 | 85 | # Encrypts a message 86 | # 87 | # @param message [String] The message to be encrypted. 88 | # 89 | # @raise [RbNaCl::CryptoError] If the encrytion fails. 90 | # 91 | # @return [String] The ciphertext (BINARY encoded) 92 | def box(message) 93 | # No padding needed. 94 | msg = message # variable name to match other RbNaCl code. 95 | # ensure enough space in result 96 | ct = Util.zeros(msg.bytesize + SEALBYTES) 97 | 98 | success = self.class.box_seal(ct, msg, msg.bytesize, @public_key.to_s) 99 | raise CryptoError, "Encryption failed" unless success 100 | 101 | ct 102 | end 103 | alias encrypt box 104 | 105 | # Decrypts a ciphertext 106 | # 107 | # @param ciphertext [String] The message to be decrypted. 108 | # 109 | # @raise [RbNaCl::CryptoError] If no private key is available. 110 | # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. 111 | # 112 | # @return [String] The decrypted message (BINARY encoded) 113 | def open(ciphertext) 114 | raise CryptoError, "Decryption failed. No private key." unless @private_key 115 | 116 | ct = ciphertext 117 | raise CryptoError, "Decryption failed. Ciphertext failed verification." if ct.bytesize < SEALBYTES 118 | 119 | message = Util.zeros(ct.bytesize - SEALBYTES) 120 | 121 | success = self.class.box_seal_open(message, ct, ct.bytesize, @public_key.to_s, @private_key.to_s) 122 | raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success 123 | 124 | message 125 | end 126 | alias decrypt open 127 | 128 | # The crypto primitive for the box class 129 | # 130 | # @return [Symbol] The primitive used 131 | def primitive 132 | self.class.primitive 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/rbnacl/group_elements/curve25519.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module GroupElements 6 | # Points provide the interface to NaCl's Curve25519 high-speed elliptic 7 | # curve cryptography, which can be used for implementing Diffie-Hellman 8 | # and other forms of public key cryptography (e.g. RbNaCl::Box) 9 | # 10 | # Objects of the Point class represent points on Edwards curves. NaCl 11 | # defines a base point (the "standard group element") which we can 12 | # multiply by an arbitrary integer. This is how NaCl computes public 13 | # keys from private keys. 14 | class Curve25519 15 | # NaCl's Curve25519 base point (a.k.a. standard group element), serialized as hex 16 | STANDARD_GROUP_ELEMENT = ["0900000000000000000000000000000000000000000000000000000000000000"].pack("H*").freeze 17 | 18 | # Order of the standard group 19 | STANDARD_GROUP_ORDER = 2**252 + 27_742_317_777_372_353_535_851_937_790_883_648_493 20 | 21 | # Degenerate key (all-zeroes, results in an all-zero shared secret) 22 | DEGENERATE_KEY = "\0" * 32 23 | 24 | include KeyComparator 25 | include Serializable 26 | 27 | extend Sodium 28 | 29 | sodium_type :scalarmult 30 | sodium_primitive :curve25519 31 | 32 | sodium_function :scalarmult_curve25519, 33 | :crypto_scalarmult_curve25519, 34 | %i[pointer pointer pointer] 35 | 36 | # Number of bytes in a scalar on this curve 37 | SCALARBYTES = 32 38 | BYTES = 32 39 | 40 | # Number of bytes in a scalar on this curve 41 | 42 | # Creates a new Point from the given serialization 43 | # 44 | # @param [String] point location of a group element (32-bytes) 45 | # 46 | # @return [RbNaCl::Point] the Point at this location 47 | def initialize(point) 48 | @point = point.to_str 49 | 50 | raise CryptoError, "degenerate key detected" if @point == DEGENERATE_KEY 51 | 52 | # FIXME: really should have a separate constant here for group element size 53 | # Group elements and scalars are both 32-bits, but that's for convenience 54 | Util.check_length(@point, SCALARBYTES, "group element") 55 | end 56 | 57 | # Multiply the given integer by this point 58 | # This ordering is a bit confusing because traditionally the point 59 | # would be the right-hand operand. 60 | # 61 | # @param [String] integer value to multiply with this Point (32-bytes) 62 | # 63 | # @return [RbNaCl::Point] result as a Point object 64 | def mult(integer) 65 | integer = integer.to_str 66 | Util.check_length(integer, SCALARBYTES, "integer") 67 | 68 | result = Util.zeros(SCALARBYTES) 69 | 70 | raise CryptoError, "degenerate key detected" unless self.class.scalarmult_curve25519(result, integer, @point) 71 | 72 | self.class.new(result) 73 | end 74 | 75 | # Return the point serialized as bytes 76 | # 77 | # @return [String] 32-byte string representing this point 78 | def to_bytes 79 | @point 80 | end 81 | 82 | @base_point = new(STANDARD_GROUP_ELEMENT) 83 | 84 | # NaCl's standard base point for all Curve25519 public keys 85 | # 86 | # @return [RbNaCl::Point] standard base point (a.k.a. standard group element) 87 | def self.base 88 | # TODO: better support fixed-based scalar multiplication (this glosses over native support) 89 | @base_point 90 | end 91 | class << self 92 | attr_reader :base_point 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/rbnacl/hash.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Cryptographic hash functions 6 | # 7 | # Cryptographic hash functions take a variable length message and compute a 8 | # fixed length string, the message digest. Even a small change in the input 9 | # data should produce a large change in the digest, and it is 'very difficult' 10 | # to create two messages with the same digest. 11 | # 12 | # A cryptographic hash can be used for checking the integrity of data, but 13 | # there is no secret involved in the hashing, so anyone can create the hash of 14 | # a given message. 15 | # 16 | # RbNaCl provides the SHA-256,SHA-512 as well as the Blake2b hash functions. 17 | module Hash 18 | # Returns the SHA-256 hash of the given data 19 | # 20 | # There's no streaming done, just pass in the data and be done with it. 21 | # 22 | # @param [#to_str] data The data, as a collection of bytes 23 | # 24 | # @raise [CryptoError] If the hashing fails for some reason. 25 | # 26 | # @return [String] The SHA-256 hash digest as raw bytes 27 | def self.sha256(data) 28 | data = data.to_str 29 | digest = Util.zeros(SHA256::BYTES) 30 | SHA256.hash_sha256(digest, data, data.bytesize) || raise(CryptoError, "Hashing failed!") 31 | digest 32 | end 33 | 34 | # Returns the SHA-512 hash of the given data 35 | # 36 | # There's no streaming done, just pass in the data and be done with it. 37 | # 38 | # @param [#to_str] data The data, as a collection of bytes 39 | # 40 | # @raise [CryptoError] If the hashing fails for some reason. 41 | # 42 | # @return [String] The SHA-512 hash digest as raw bytes 43 | def self.sha512(data) 44 | digest = Util.zeros(SHA512::BYTES) 45 | SHA512.hash_sha512(digest, data, data.bytesize) || raise(CryptoError, "Hashing failed!") 46 | digest 47 | end 48 | 49 | # Returns the Blake2b hash of the given data 50 | # 51 | # There's no streaming done, just pass in the data and be done with it. 52 | # This method returns a 64-byte hash by default. 53 | # 54 | # @param [String] data The data, as a collection of bytes 55 | # @option options [Fixnum] digest_size Size in bytes (1-64, default 64) 56 | # @option options [String] key 64-byte (or less) key for keyed mode 57 | # @option options [String] salt Provide a salt to support randomised hashing. 58 | # This is mixed into the parameters block to start the hashing. 59 | # @option options [Personal] personal Provide personalisation string to allow pinning a hash for a particular purpose. 60 | # This is mixed into the parameters block to start the hashing 61 | # 62 | # @raise [CryptoError] If the hashing fails for some reason. 63 | # 64 | # @return [String] The Blake2b hash digest as raw bytes 65 | def self.blake2b(data, options = {}) 66 | Blake2b.digest(data, options) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/rbnacl/hash/blake2b.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Hash 6 | # The Blake2b hash function 7 | # 8 | # Blake2b is based on Blake, a SHA3 finalist which was snubbed in favor of 9 | # Keccak, a much slower hash function but one sufficiently different from 10 | # SHA2 to let the SHA3 judges panel sleep easy. Back in the real world, 11 | # it'd be great if we can calculate hashes quickly if possible. 12 | # 13 | # Blake2b provides for up to 64-bit digests and also supports a keyed mode 14 | # similar to HMAC 15 | class Blake2b 16 | extend Sodium 17 | 18 | sodium_type :generichash 19 | sodium_primitive :blake2b 20 | sodium_constant :BYTES_MIN 21 | sodium_constant :BYTES_MAX 22 | sodium_constant :KEYBYTES_MIN 23 | sodium_constant :KEYBYTES_MAX 24 | sodium_constant :SALTBYTES 25 | sodium_constant :PERSONALBYTES 26 | 27 | sodium_function :generichash_blake2b, 28 | :crypto_generichash_blake2b_salt_personal, 29 | %i[pointer size_t pointer ulong_long pointer size_t pointer pointer] 30 | 31 | sodium_function :generichash_blake2b_init, 32 | :crypto_generichash_blake2b_init_salt_personal, 33 | %i[pointer pointer size_t size_t pointer pointer] 34 | 35 | sodium_function :generichash_blake2b_update, 36 | :crypto_generichash_blake2b_update, 37 | %i[pointer pointer ulong_long] 38 | 39 | sodium_function :generichash_blake2b_final, 40 | :crypto_generichash_blake2b_final, 41 | %i[pointer pointer size_t] 42 | 43 | EMPTY_PERSONAL = ("\0" * PERSONALBYTES).freeze 44 | EMPTY_SALT = ("\0" * SALTBYTES).freeze 45 | 46 | # Calculate a Blake2b digest 47 | # 48 | # @param [String] message Message to be hashed 49 | # @param [Hash] options Blake2b configuration 50 | # @option opts [String] :key for Blake2b keyed mode 51 | # @option opts [Integer] :digest_size size of output digest in bytes 52 | # @option opts [String] :salt Provide a salt to support randomised hashing. 53 | # This is mixed into the parameters block to start the hashing. 54 | # @option opts [Personal] :personal Provide personalisation string to allow pinning a hash for a particular purpose. 55 | # This is mixed into the parameters block to start the hashing 56 | # 57 | # @raise [RbNaCl::LengthError] Invalid length specified for one or more options 58 | # 59 | # @return [String] Blake2b digest of the string as raw bytes 60 | def self.digest(message, options) 61 | opts = validate_opts(options) 62 | digest = Util.zeros(opts[:digest_size]) 63 | generichash_blake2b(digest, opts[:digest_size], message, message.bytesize, 64 | opts[:key], opts[:key_size], opts[:salt], opts[:personal]) || 65 | raise(CryptoError, "Hashing failed!") 66 | digest 67 | end 68 | 69 | # Validate and sanitize values for Blake2b configuration 70 | # 71 | # @param [Hash] options Blake2b configuration 72 | # @option opts [String] :key for Blake2b keyed mode 73 | # @option opts [Integer] :digest_size size of output digest in bytes 74 | # @option opts [String] :salt Provide a salt to support randomised hashing. 75 | # This is mixed into the parameters block to start the hashing. 76 | # @option opts [Personal] :personal Provide personalisation string to allow pinning a hash for a particular purpose. 77 | # This is mixed into the parameters block to start the hashing 78 | # 79 | # @raise [RbNaCl::LengthError] Invalid length specified for one or more options 80 | # 81 | # @return [Hash] opts Configuration hash with sanitized values 82 | def self.validate_opts(opts) 83 | key = opts.fetch(:key, nil) 84 | if key 85 | key_size = key.bytesize 86 | raise LengthError, "key too short" if key_size < KEYBYTES_MIN 87 | raise LengthError, "key too long" if key_size > KEYBYTES_MAX 88 | else 89 | key_size = 0 90 | end 91 | opts[:key_size] = key_size 92 | 93 | digest_size = opts.fetch(:digest_size, BYTES_MAX) 94 | raise LengthError, "digest size too short" if digest_size < BYTES_MIN 95 | raise LengthError, "digest size too long" if digest_size > BYTES_MAX 96 | 97 | opts[:digest_size] = digest_size 98 | 99 | personal = opts.fetch(:personal, EMPTY_PERSONAL) 100 | opts[:personal] = Util.zero_pad(PERSONALBYTES, personal) 101 | 102 | salt = opts.fetch(:salt, EMPTY_SALT) 103 | opts[:salt] = Util.zero_pad(SALTBYTES, salt) 104 | opts 105 | end 106 | 107 | private_class_method :validate_opts 108 | 109 | def self.new(opts = {}) 110 | opts = validate_opts(opts) 111 | super 112 | end 113 | 114 | # Create a new Blake2b hash object 115 | # 116 | # @param [Hash] opts Blake2b configuration 117 | # @option opts [String] :key for Blake2b keyed mode 118 | # @option opts [Integer] :digest_size size of output digest in bytes 119 | # @option opts [String] :salt Provide a salt to support randomised hashing. 120 | # This is mixed into the parameters block to start the hashing. 121 | # @option opts [Personal] :personal Provide personalisation string to allow pinning a hash for a particular purpose. 122 | # This is mixed into the parameters block to start the hashing 123 | # 124 | # @raise [RbNaCl::LengthError] Invalid length specified for one or more options 125 | # 126 | # @return [RbNaCl::Hash::Blake2b] A Blake2b hasher object 127 | def initialize(opts = {}) 128 | @key = opts[:key] 129 | @key_size = opts[:key_size] 130 | @digest_size = opts[:digest_size] 131 | @personal = opts[:personal] 132 | @salt = opts[:salt] 133 | 134 | @incycle = false 135 | @instate = nil 136 | end 137 | 138 | # Initialize state for Blake2b hash calculation, 139 | # this will be called automatically from #update if needed 140 | def reset 141 | @instate.release if @instate 142 | @instate = State.new 143 | self.class.generichash_blake2b_init(@instate.pointer, @key, @key_size, @digest_size, @salt, @personal) || 144 | raise(CryptoError, "Hash init failed!") 145 | @incycle = true 146 | @digest = nil 147 | end 148 | 149 | # Reentrant version of Blake2b digest calculation method 150 | # 151 | # @param [String] message Message to be hashed 152 | def update(message) 153 | reset unless @incycle 154 | self.class.generichash_blake2b_update(@instate.pointer, message, message.bytesize) || 155 | raise(CryptoError, "Hashing failed!") 156 | end 157 | alias << update 158 | 159 | # Finalize digest calculation, return cached digest if any 160 | # 161 | # @return [String] Blake2b digest of the string as raw bytes 162 | def digest 163 | raise(CryptoError, "No message to hash yet!") unless @incycle 164 | return @digest if @digest 165 | 166 | @digest = Util.zeros(@digest_size) 167 | self.class.generichash_blake2b_final(@instate.pointer, @digest, @digest_size) || 168 | raise(CryptoError, "Hash finalization failed!") 169 | @digest 170 | end 171 | 172 | # The crypto_generichash_blake2b_state struct representation 173 | # ref: jedisct1/libsodium/blob/c87df74c7b5969f4/src/libsodium/include/sodium/crypto_generichash_blake2b.h#L23-L25 174 | class State < FFI::Struct 175 | layout :opaque, [:uint8, 384] 176 | end 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/rbnacl/hash/sha256.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Hash 6 | # Provides a binding for the SHA256 function in libsodium 7 | module SHA256 8 | extend Sodium 9 | sodium_type :hash 10 | sodium_primitive :sha256 11 | sodium_constant :BYTES 12 | sodium_function :hash_sha256, 13 | :crypto_hash_sha256, 14 | %i[pointer pointer ulong_long] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rbnacl/hash/sha512.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Hash 6 | # Provides the binding for the SHA512 hash function 7 | module SHA512 8 | extend Sodium 9 | sodium_type :hash 10 | sodium_primitive :sha512 11 | sodium_constant :BYTES 12 | sodium_function :hash_sha512, 13 | :crypto_hash_sha512, 14 | %i[pointer pointer ulong_long] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rbnacl/hmac/sha256.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module HMAC 6 | # Computes an authenticator as HMAC-SHA-256 7 | # 8 | # The authenticator can be used at a later time to verify the provenance of 9 | # the message by recomputing the HMAC over the message and then comparing it to 10 | # the provided authenticator. The class provides methods for generating 11 | # signatures and also has a constant-time implementation for checking them. 12 | # 13 | # This is a secret key authenticator, i.e. anyone who can verify signatures 14 | # can also create them. 15 | # 16 | # @see http://nacl.cr.yp.to/auth.html 17 | class SHA256 < Auth 18 | extend Sodium 19 | 20 | sodium_type :auth 21 | sodium_primitive :hmacsha256 22 | sodium_constant :BYTES 23 | sodium_constant :KEYBYTES 24 | 25 | sodium_function :auth_hmacsha256_init, 26 | :crypto_auth_hmacsha256_init, 27 | %i[pointer pointer size_t] 28 | 29 | sodium_function :auth_hmacsha256_update, 30 | :crypto_auth_hmacsha256_update, 31 | %i[pointer pointer ulong_long] 32 | 33 | sodium_function :auth_hmacsha256_final, 34 | :crypto_auth_hmacsha256_final, 35 | %i[pointer pointer] 36 | 37 | # Create instance without checking key length 38 | # 39 | # RFC 2104 HMAC 40 | # The key for HMAC can be of any length. 41 | # 42 | # see https://tools.ietf.org/html/rfc2104#section-3 43 | def initialize(key) 44 | @key = Util.check_hmac_key(key, "#{self.class} key") 45 | @state = State.new 46 | @authenticator = Util.zeros(tag_bytes) 47 | 48 | self.class.auth_hmacsha256_init(@state, key, key.bytesize) 49 | end 50 | 51 | # Compute authenticator for message 52 | # 53 | # @params [#to_str] message message to construct an authenticator for 54 | def update(message) 55 | self.class.auth_hmacsha256_update(@state, message, message.bytesize) 56 | self.class.auth_hmacsha256_final(@state.clone, @authenticator) 57 | 58 | hexdigest 59 | end 60 | 61 | # Return the authenticator, as raw bytes 62 | # 63 | # @return [String] The authenticator, as raw bytes 64 | def digest 65 | @authenticator 66 | end 67 | 68 | # Return the authenticator, as hex string 69 | # 70 | # @return [String] The authenticator, as hex string 71 | def hexdigest 72 | @authenticator.unpack("H*").last 73 | end 74 | 75 | private 76 | 77 | def compute_authenticator(authenticator, message) 78 | state = State.new 79 | 80 | self.class.auth_hmacsha256_init(state, key, key.bytesize) 81 | self.class.auth_hmacsha256_update(state, message, message.bytesize) 82 | self.class.auth_hmacsha256_final(state, authenticator) 83 | end 84 | 85 | # libsodium crypto_auth_hmacsha256_verify works only for 32 byte keys 86 | # ref: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_auth/hmacsha256/auth_hmacsha256.c#L109 87 | def verify_message(authenticator, message) 88 | correct = Util.zeros(BYTES) 89 | compute_authenticator(correct, message) 90 | Util.verify32(correct, authenticator) 91 | end 92 | 93 | # The crypto_auth_hmacsha256_state struct representation 94 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_auth_hmacsha256.h 95 | class SHA256State < FFI::Struct 96 | layout :state, [:uint32, 8], 97 | :count, :uint64, 98 | :buf, [:uint8, 64] 99 | end 100 | 101 | # The crypto_hash_sha256_state struct representation 102 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_hash_sha256.h 103 | class State < FFI::Struct 104 | layout :ictx, SHA256State, 105 | :octx, SHA256State 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/rbnacl/hmac/sha512.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module HMAC 6 | # Computes an authenticator as HMAC-SHA-512 7 | # 8 | # The authenticator can be used at a later time to verify the provenance of 9 | # the message by recomputing the HMAC over the message and then comparing it to 10 | # the provided authenticator. The class provides methods for generating 11 | # signatures and also has a constant-time implementation for checking them. 12 | # 13 | # This is a secret key authenticator, i.e. anyone who can verify signatures 14 | # can also create them. 15 | # 16 | # @see http://nacl.cr.yp.to/auth.html 17 | class SHA512 < Auth 18 | extend Sodium 19 | 20 | sodium_type :auth 21 | sodium_primitive :hmacsha512 22 | sodium_constant :BYTES 23 | sodium_constant :KEYBYTES 24 | 25 | sodium_function :auth_hmacsha512_init, 26 | :crypto_auth_hmacsha512_init, 27 | %i[pointer pointer size_t] 28 | 29 | sodium_function :auth_hmacsha512_update, 30 | :crypto_auth_hmacsha512_update, 31 | %i[pointer pointer ulong_long] 32 | 33 | sodium_function :auth_hmacsha512_final, 34 | :crypto_auth_hmacsha512_final, 35 | %i[pointer pointer] 36 | 37 | # Create instance without checking key length 38 | # 39 | # RFC 2104 HMAC 40 | # The key for HMAC can be of any length. 41 | # 42 | # see https://tools.ietf.org/html/rfc2104#section-3 43 | def initialize(key) 44 | @key = Util.check_hmac_key(key, "#{self.class} key") 45 | @state = State.new 46 | @authenticator = Util.zeros(tag_bytes) 47 | 48 | self.class.auth_hmacsha512_init(@state, key, key.bytesize) 49 | end 50 | 51 | # Compute authenticator for message 52 | # 53 | # @params [#to_str] message message to construct an authenticator for 54 | def update(message) 55 | self.class.auth_hmacsha512_update(@state, message, message.bytesize) 56 | self.class.auth_hmacsha512_final(@state.clone, @authenticator) 57 | 58 | hexdigest 59 | end 60 | 61 | # Return the authenticator, as raw bytes 62 | # 63 | # @return [String] The authenticator, as raw bytes 64 | def digest 65 | @authenticator 66 | end 67 | 68 | # Return the authenticator, as hex string 69 | # 70 | # @return [String] The authenticator, as hex string 71 | def hexdigest 72 | @authenticator.unpack("H*").last 73 | end 74 | 75 | private 76 | 77 | def compute_authenticator(authenticator, message) 78 | state = State.new 79 | 80 | self.class.auth_hmacsha512_init(state, key, key.bytesize) 81 | self.class.auth_hmacsha512_update(state, message, message.bytesize) 82 | self.class.auth_hmacsha512_final(state, authenticator) 83 | end 84 | 85 | # libsodium crypto_auth_hmacsha512_verify works only for 32 byte keys 86 | # ref: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c#L109 87 | def verify_message(authenticator, message) 88 | correct = Util.zeros(BYTES) 89 | compute_authenticator(correct, message) 90 | Util.verify64(correct, authenticator) 91 | end 92 | 93 | # The crypto_auth_hmacsha512_state struct representation 94 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_auth_hmacsha512.h 95 | class SHA512State < FFI::Struct 96 | layout :state, [:uint64, 8], 97 | :count, [:uint64, 2], 98 | :buf, [:uint8, 128] 99 | end 100 | 101 | # The crypto_hash_sha512_state struct representation 102 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_hash_sha512.h 103 | class State < FFI::Struct 104 | layout :ictx, SHA512State, 105 | :octx, SHA512State 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/rbnacl/hmac/sha512256.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module HMAC 6 | # Computes an authenticator as HMAC-SHA-512 truncated to 256-bits 7 | # 8 | # The authenticator can be used at a later time to verify the provenance of 9 | # the message by recomputing the HMAC over the message and then comparing it to 10 | # the provided authenticator. The class provides methods for generating 11 | # signatures and also has a constant-time implementation for checking them. 12 | # 13 | # This is a secret key authenticator, i.e. anyone who can verify signatures 14 | # can also create them. 15 | # 16 | # @see http://nacl.cr.yp.to/auth.html 17 | class SHA512256 < Auth 18 | extend Sodium 19 | 20 | sodium_type :auth 21 | sodium_primitive :hmacsha512256 22 | sodium_constant :BYTES 23 | sodium_constant :KEYBYTES 24 | 25 | sodium_function :auth_hmacsha512256_init, 26 | :crypto_auth_hmacsha512256_init, 27 | %i[pointer pointer size_t] 28 | 29 | sodium_function :auth_hmacsha512256_update, 30 | :crypto_auth_hmacsha512256_update, 31 | %i[pointer pointer ulong_long] 32 | 33 | sodium_function :auth_hmacsha512256_final, 34 | :crypto_auth_hmacsha512256_final, 35 | %i[pointer pointer] 36 | 37 | # Create instance without checking key length 38 | # 39 | # RFC 2104 HMAC 40 | # The key for HMAC can be of any length. 41 | # 42 | # see https://tools.ietf.org/html/rfc2104#section-3 43 | def initialize(key) 44 | @key = Util.check_hmac_key(key, "#{self.class} key") 45 | @state = State.new 46 | @authenticator = Util.zeros(tag_bytes) 47 | 48 | self.class.auth_hmacsha512256_init(@state, key, key.bytesize) 49 | end 50 | 51 | # Compute authenticator for message 52 | # 53 | # @params [#to_str] message message to construct an authenticator for 54 | def update(message) 55 | self.class.auth_hmacsha512256_update(@state, message, message.bytesize) 56 | self.class.auth_hmacsha512256_final(@state.clone, @authenticator) 57 | 58 | hexdigest 59 | end 60 | 61 | # Return the authenticator, as raw bytes 62 | # 63 | # @return [String] The authenticator, as raw bytes 64 | def digest 65 | @authenticator 66 | end 67 | 68 | # Return the authenticator, as hex string 69 | # 70 | # @return [String] The authenticator, as hex string 71 | def hexdigest 72 | @authenticator.unpack("H*").last 73 | end 74 | 75 | private 76 | 77 | def compute_authenticator(authenticator, message) 78 | state = State.new 79 | 80 | self.class.auth_hmacsha512256_init(state, key, key.bytesize) 81 | self.class.auth_hmacsha512256_update(state, message, message.bytesize) 82 | self.class.auth_hmacsha512256_final(state, authenticator) 83 | end 84 | 85 | def verify_message(authenticator, message) 86 | correct = Util.zeros(BYTES) 87 | compute_authenticator(correct, message) 88 | Util.verify32(correct, authenticator) 89 | end 90 | 91 | # The crypto_auth_hmacsha512256_state struct representation 92 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_auth_hmacsha512256.h 93 | class SHA512256State < FFI::Struct 94 | layout :state, [:uint64, 8], 95 | :count, [:uint64, 2], 96 | :buf, [:uint8, 128] 97 | end 98 | 99 | # The crypto_hash_sha512_state struct representation 100 | # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_hash_sha512.h 101 | class State < FFI::Struct 102 | layout :ictx, SHA512256State, 103 | :octx, SHA512256State 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/rbnacl/init.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Defines the libsodium init function 6 | module Init 7 | extend FFI::Library 8 | ffi_lib ["sodium", "libsodium.so.18", "libsodium.so.23", "libsodium.so.26"] 9 | 10 | attach_function :sodium_init, [], :int 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/rbnacl/key_comparator.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Implements comparisons of keys 6 | # 7 | # This permits both timing invariant equality tests, as well as 8 | # lexicographical sorting. 9 | module KeyComparator 10 | include Comparable 11 | # spaceship operator 12 | # 13 | # @param other [KeyComparator,#to_str] The thing to compare 14 | # 15 | # @return [0] if the keys are equal 16 | # @return [1] if the key is larger than the other key 17 | # @return [-1] if the key is smaller than the other key 18 | # @return [nil] if comparison doesn't make sense 19 | def <=>(other) 20 | if KeyComparator > other.class 21 | other = other.to_bytes 22 | elsif other.respond_to?(:to_str) 23 | other = other.to_str 24 | else 25 | return nil 26 | end 27 | compare32(other) 28 | end 29 | 30 | # equality operator 31 | # 32 | # The equality operator is explicity defined, despite including Comparable 33 | # and having a spaceship operator, so that if equality tests are desired, 34 | # they can be timing invariant, without any chance that the further 35 | # comparisons for greater than and less than can leak information. Maybe 36 | # this is too paranoid, but I don't know how ruby works under the hood with 37 | # comparable. 38 | # 39 | # @param other [KeyComparator,#to_str] The thing to compare 40 | # 41 | # @return [true] if the keys are equal 42 | # @return [false] if they keys are not equal 43 | def ==(other) 44 | if KeyComparator > other.class 45 | other = other.to_bytes 46 | elsif other.respond_to?(:to_str) 47 | other = other.to_str 48 | else 49 | return false 50 | end 51 | Util.verify32(to_bytes, other) 52 | end 53 | 54 | private 55 | 56 | def compare32(other) 57 | if Util.verify32(to_bytes, other) 58 | 0 59 | elsif to_bytes > other 60 | 1 61 | else 62 | -1 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/rbnacl/one_time_auths/poly1305.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module OneTimeAuths 6 | # Computes an authenticator using poly1305 7 | # 8 | # The authenticator can be used at a later time to verify the provenance of 9 | # the message by recomputing the tag over the message and then comparing it to 10 | # the provided authenticator. The class provides methods for generating 11 | # signatures and also has a constant-time implementation for checking them. 12 | # 13 | # As the name suggests, this is a **ONE TIME** authenticator. Computing an 14 | # authenticator for two messages using the same key probably gives an 15 | # attacker enough information to forge further authenticators for the same 16 | # key. 17 | # 18 | # This is a secret key authenticator, i.e. anyone who can verify signatures 19 | # can also create them. 20 | # 21 | # @see http://nacl.cr.yp.to/onetimeauth.html 22 | class Poly1305 < Auth 23 | extend Sodium 24 | 25 | sodium_type :onetimeauth 26 | sodium_primitive :poly1305 27 | sodium_constant :BYTES 28 | sodium_constant :KEYBYTES 29 | 30 | sodium_function :onetimeauth_poly1305, 31 | :crypto_onetimeauth_poly1305, 32 | %i[pointer pointer ulong_long pointer] 33 | 34 | sodium_function :onetimeauth_poly1305_verify, 35 | :crypto_onetimeauth_poly1305_verify, 36 | %i[pointer pointer ulong_long pointer] 37 | 38 | private 39 | 40 | def compute_authenticator(authenticator, message) 41 | self.class.onetimeauth_poly1305(authenticator, message, message.bytesize, key) 42 | end 43 | 44 | def verify_message(authenticator, message) 45 | self.class.onetimeauth_poly1305_verify(authenticator, message, message.bytesize, key) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/rbnacl/password_hash.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Password hashing functions 6 | # 7 | # These hash functions are designed specifically for the purposes of securely 8 | # storing passwords in a way that they can be checked against a supplied 9 | # password but an attacker who obtains a hash cannot easily reverse them back 10 | # into the original password. 11 | # 12 | # Unlike normal hash functions, which are intentionally designed to hash data 13 | # as quickly as they can while remaining secure, password hashing functions 14 | # are intentionally designed to be slow so they are hard for attackers to 15 | # brute force. 16 | # 17 | # All password hashing functions take a "salt" value which should be randomly 18 | # generated on a per-password basis (using RbNaCl::Random, accept no 19 | # substitutes) 20 | # 21 | # All of them also take a CPU work factor, which increases the amount of 22 | # computation needed to produce the digest. 23 | module PasswordHash 24 | # scrypt: the original sequential memory-hard password hashing function. 25 | # 26 | # @param [String] password to be hashed 27 | # @param [String] salt to make the digest unique 28 | # @param [Integer] opslimit the CPU cost (e.g. 2**20) 29 | # @param [Integer] memlimit the memory cost (e.g. 2**24) 30 | # @param [Integer] digest_size of the output 31 | # 32 | # @raise [CryptoError] If calculating the digest fails for some reason. 33 | # 34 | # @return [String] The scrypt digest as raw bytes 35 | def self.scrypt(password, salt, opslimit = SCrypt::OPSLIMIT_SENSITIVE, memlimit = SCrypt::MEMLIMIT_SENSITIVE, 36 | digest_size = 64) 37 | SCrypt.new(opslimit, memlimit, digest_size).digest(password, salt) 38 | end 39 | 40 | # argon2: state of the art in the design of memory-hard hashing functions 41 | # (default digest algorithm). 42 | # 43 | # @param [String] password to be hashed 44 | # @param [String] salt to make the digest unique 45 | # @param [Integer] opslimit the CPU cost (3..10) 46 | # @param [Integer] memlimit the memory cost, in bytes 47 | # @param [Integer] digest_size of the output 48 | # 49 | # @raise [CryptoError] If calculating the digest fails for some reason. 50 | # 51 | # @return [String] The argon2 digest as raw bytes 52 | def self.argon2(password, salt, opslimit, memlimit, digest_size = 64) 53 | argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt) 54 | end 55 | 56 | # argon2i: argon2, using argon2i digest algorithm. 57 | # 58 | # @param [String] password to be hashed 59 | # @param [String] salt to make the digest unique 60 | # @param [Integer] opslimit the CPU cost (3..10) 61 | # @param [Integer] memlimit the memory cost, in bytes 62 | # @param [Integer] digest_size of the output 63 | # 64 | # @raise [CryptoError] If calculating the digest fails for some reason. 65 | # 66 | # @return [String] The argon2i digest as raw bytes 67 | def self.argon2i(password, salt, opslimit, memlimit, digest_size = 64) 68 | argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt, :argon2i) 69 | end 70 | 71 | # argon2id: argon2, using argon2id digest algorithm. 72 | # 73 | # @param [String] password to be hashed 74 | # @param [String] salt to make the digest unique 75 | # @param [Integer] opslimit the CPU cost (3..10) 76 | # @param [Integer] memlimit the memory cost, in bytes 77 | # @param [Integer] digest_size of the output 78 | # 79 | # @raise [CryptoError] If calculating the digest fails for some reason. 80 | # 81 | # @return [String] The argon2id digest as raw bytes 82 | def self.argon2id(password, salt, opslimit, memlimit, digest_size = 64) 83 | argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt, :argon2id) 84 | end 85 | 86 | # argon2_str: crypt-style password digest 87 | # 88 | # @param [String] password to be hashed 89 | # @param [Integer] opslimit the CPU cost (3..10) 90 | # @param [Integer] memlimit the memory cost, in bytes 91 | # 92 | # @raise [CryptoError] If calculating the digest fails for some reason. 93 | # 94 | # @return [String] The argon2i digest as crypt-style string 95 | def self.argon2_str(password, opslimit = :interactive, memlimit = :interactive) 96 | argon2_supported? && Argon2.new(opslimit, memlimit).digest_str(password) 97 | end 98 | 99 | # argon2_valid?: verify crypt-style password digest 100 | # 101 | # @param [String] password to verify 102 | # @param [String] str_digest to verify 103 | # 104 | # @return [Boolean] true if digest was created using password 105 | def self.argon2_valid?(password, str_digest) 106 | argon2_supported? && Argon2.digest_str_verify(password, str_digest) 107 | end 108 | 109 | class << self 110 | protected 111 | 112 | def argon2_supported? 113 | if RbNaCl::Sodium::Version::ARGON2_SUPPORTED 114 | true 115 | else 116 | raise NotImplementedError, "argon2 requires libsodium version >= 1.0.9" \ 117 | " (currently running #{RbNaCl::Sodium::Version::STRING})" 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/rbnacl/password_hash/argon2.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module PasswordHash 6 | # Since version 1.0.9, Sodium provides a password hashing scheme called 7 | # Argon2. Argon2 summarizes the state of the art in the design of memory- 8 | # hard functions. It aims at the highest memory filling rate and effective 9 | # use of multiple computing units, while still providing defense against 10 | # tradeoff attacks. It prevents ASICs from having a significant advantage 11 | # over software implementations. 12 | class Argon2 13 | extend Sodium 14 | 15 | sodium_type :pwhash 16 | 17 | sodium_constant :ALG_DEFAULT 18 | sodium_constant :ALG_ARGON2I13 19 | sodium_constant :ALG_ARGON2ID13 if Sodium::Version::ARGON2ID_SUPPORTED 20 | 21 | sodium_constant :SALTBYTES, fallback: 16 22 | sodium_constant :STRBYTES, fallback: 128 23 | sodium_constant :OPSLIMIT_INTERACTIVE, fallback: 4 24 | sodium_constant :MEMLIMIT_INTERACTIVE, fallback: 2**25 # (32mb) 25 | sodium_constant :OPSLIMIT_MODERATE, fallback: 6 26 | sodium_constant :MEMLIMIT_MODERATE, fallback: 2**27 # (128mb) 27 | sodium_constant :OPSLIMIT_SENSITIVE, fallback: 8 28 | sodium_constant :MEMLIMIT_SENSITIVE, fallback: 2**29 # (512mb) 29 | sodium_constant :MEMLIMIT_MIN, fallback: 8192 30 | sodium_constant :MEMLIMIT_MAX, fallback: 4_294_967_296 31 | sodium_constant :OPSLIMIT_MIN, fallback: 3 32 | sodium_constant :OPSLIMIT_MAX, fallback: 10 33 | 34 | ARGON2_MIN_OUTLEN = 16 35 | ARGON2_MAX_OUTLEN = 0xFFFFFFFF 36 | 37 | sodium_function_with_return_code( 38 | :pwhash, 39 | :crypto_pwhash, 40 | %i[pointer ulong_long pointer ulong_long pointer ulong_long size_t int] 41 | ) 42 | 43 | sodium_function( 44 | :pwhash_str, 45 | :crypto_pwhash_str, 46 | %i[pointer pointer ulong_long ulong_long size_t] 47 | ) 48 | 49 | sodium_function( 50 | :pwhash_str_verify, 51 | :crypto_pwhash_str_verify, 52 | %i[pointer pointer ulong_long] 53 | ) 54 | 55 | ARGON_ERROR_CODES = { 56 | -1 => "ARGON2_OUTPUT_PTR_NULL", -2 => "ARGON2_OUTPUT_TOO_SHORT", 57 | -3 => "ARGON2_OUTPUT_TOO_LONG", -4 => "ARGON2_PWD_TOO_SHORT", 58 | -5 => "ARGON2_PWD_TOO_LONG", -6 => "ARGON2_SALT_TOO_SHORT", 59 | -7 => "ARGON2_SALT_TOO_LONG", -8 => "ARGON2_AD_TOO_SHORT", 60 | -9 => "ARGON2_AD_TOO_LONG", -10 => "ARGON2_SECRET_TOO_SHORT", 61 | -11 => "ARGON2_SECRET_TOO_LONG", -12 => "ARGON2_TIME_TOO_SMALL", 62 | -13 => "ARGON2_TIME_TOO_LARGE", -14 => "ARGON2_MEMORY_TOO_LITTLE", 63 | -15 => "ARGON2_MEMORY_TOO_MUCH", -16 => "ARGON2_LANES_TOO_FEW", 64 | -17 => "ARGON2_LANES_TOO_MANY", -18 => "ARGON2_PWD_PTR_MISMATCH", 65 | -19 => "ARGON2_SALT_PTR_MISMATCH", -20 => "ARGON2_SECRET_PTR_MISMATCH", 66 | -21 => "ARGON2_AD_PTR_MISMATCH", -22 => "ARGON2_MEMORY_ALLOCATION_ERROR", 67 | -23 => "ARGON2_FREE_MEMORY_CBK_NULL", -24 => "ARGON2_ALLOCATE_MEMORY_CBK_NULL", 68 | -25 => "ARGON2_INCORRECT_PARAMETER", -26 => "ARGON2_INCORRECT_TYPE", 69 | -27 => "ARGON2_OUT_PTR_MISMATCH", -28 => "ARGON2_THREADS_TOO_FEW", 70 | -29 => "ARGON2_THREADS_TOO_MANY", -30 => "ARGON2_MISSING_ARGS", 71 | -31 => "ARGON2_ENCODING_FAIL", -32 => "ARGON2_DECODING_FAIL", 72 | -33 => "ARGON2_THREAD_FAIL", -34 => "ARGON2_DECODING_LENGTH_FAIL", 73 | -35 => "ARGON2_VERIFY_MISMATCH" 74 | }.freeze 75 | 76 | # Create a new Argon2 password hash object 77 | # 78 | # opslimit and memlimit may be an integer, or one of the following 79 | # symbols: 80 | # 81 | # [:interactive] Suitable for interactive online operations. This 82 | # requires 32 Mb of dedicated RAM. 83 | # [:moderate] A compromise between interactive and sensitive. This 84 | # requires 128 Mb of dedicated RAM, and takes about 0.7 85 | # seconds on a 2.8 Ghz Core i7 CPU. 86 | # [:sensitive] For highly sensitive and non-interactive operations. This 87 | # requires 128 Mb of dedicated RAM, and takes about 0.7 88 | # seconds on a 2.8 Ghz Core i7 CPU 89 | # 90 | # @param [Integer] opslimit the CPU cost (1..10) 91 | # @param [Integer] memlimit the memory cost (e.g. 2**24) 92 | # @param [Integer] digest_size the byte length of the resulting digest 93 | # 94 | # @return [RbNaCl::PasswordHash::Argon2] An Argon2 password hasher object 95 | def initialize(opslimit, memlimit, digest_size = nil) 96 | @opslimit = self.class.opslimit_value(opslimit) 97 | @memlimit = self.class.memlimit_value(memlimit) 98 | @digest_size = self.class.digest_size_value(digest_size) if digest_size 99 | end 100 | 101 | # Calculate an Argon2 digest for a given password and salt 102 | # 103 | # @param [String] password to be hashed 104 | # @param [String] salt to make the digest unique 105 | # @param [Symbol] digest algorithm to use (may be :argon2i or :argon2id) 106 | # if nil, the default is determined by libsodium 107 | # (argon2i for libsodium < 1.0.15, and argon2id for 108 | # libsodium >= 1.0.15). 109 | # 110 | # @return [String] scrypt digest of the string as raw bytes 111 | def digest(password, salt, algo = nil) 112 | raise ArgumentError, "digest_size is required" unless @digest_size 113 | 114 | digest = Util.zeros(@digest_size) 115 | salt = Util.check_string(salt, SALTBYTES, "salt") 116 | 117 | if algo.nil? 118 | algorithm = ALG_DEFAULT 119 | elsif algo == :argon2i 120 | algorithm = ALG_ARGON2I13 121 | elsif algo == :argon2id && Sodium::Version::ARGON2ID_SUPPORTED 122 | algorithm = ALG_ARGON2ID13 123 | else 124 | raise ArgumentError, "digest algorithm is not supported" 125 | end 126 | 127 | status = self.class.pwhash( 128 | digest, @digest_size, 129 | password, password.bytesize, salt, 130 | @opslimit, @memlimit, algorithm 131 | ) 132 | raise CryptoError, ARGON_ERROR_CODES[status] if status.nonzero? 133 | 134 | digest 135 | end 136 | 137 | # Calculate an Argon2 digest in the form of a crypt-style string. 138 | # The resulting string encodes the parameters and salt. 139 | # 140 | # @param [String] password to be hashed 141 | # 142 | # @return [String] argon2 digest string 143 | def digest_str(password) 144 | raise ArgumentError, "password must be a String" unless password.is_a?(String) 145 | 146 | result = Util.zeros(STRBYTES) 147 | 148 | ok = self.class.pwhash_str( 149 | result, 150 | password, password.bytesize, 151 | @opslimit, @memlimit 152 | ) 153 | raise CryptoError, "unknown error in Argon2#digest_str" unless ok 154 | 155 | result.delete("\x00") 156 | end 157 | 158 | # Compares a password with a digest string 159 | # 160 | # @param [String] password to be hashed 161 | # @param [String] digest_string to compare to 162 | # 163 | # @return [boolean] true if password matches digest_string 164 | def self.digest_str_verify(password, digest_string) 165 | raise ArgumentError, "password must be a String" unless password.is_a?(String) 166 | raise ArgumentError, "digest_string must be a String" unless digest_string.is_a?(String) 167 | 168 | pwhash_str_verify( 169 | digest_string, 170 | password, password.bytesize 171 | ) 172 | end 173 | 174 | # Clamps opslimit to an acceptable range (3..10) 175 | # 176 | # @param [Integer] opslimit value to be checked 177 | # 178 | # @raise [ArgumentError] if the value is out of range 179 | # 180 | # @return [Integer] opslimit a valid value for opslimit 181 | def self.opslimit_value(opslimit) 182 | case opslimit 183 | when :interactive then OPSLIMIT_INTERACTIVE 184 | when :moderate then OPSLIMIT_MODERATE 185 | when :sensitive then OPSLIMIT_SENSITIVE 186 | when OPSLIMIT_MIN..OPSLIMIT_MAX then opslimit.to_i 187 | else 188 | raise ArgumentError, "opslimit must be within the range 3..10" 189 | end 190 | end 191 | 192 | # Clamps memlimit between 8192 bytes and 4 TB (eg. 2**32) 193 | # 194 | # @param [Integer] memlimit, in bytes 195 | # 196 | # @raise [ArgumentError] if the value is out of range 197 | # 198 | # @return [Integer] memlimit a valid value for memlimit 199 | def self.memlimit_value(memlimit) 200 | case memlimit 201 | when :interactive then MEMLIMIT_INTERACTIVE 202 | when :moderate then MEMLIMIT_MODERATE 203 | when :sensitive then MEMLIMIT_SENSITIVE 204 | when MEMLIMIT_MIN..MEMLIMIT_MAX then memlimit.to_i 205 | else 206 | raise ArgumentError, "memlimit must be within the range 2**(13..32)" 207 | end 208 | end 209 | 210 | # Clamps digest size between 16..4294967295 211 | # 212 | # @raise [LengthError] if the value is out of range 213 | # 214 | # @return [Integer] digest_size a valid value for digest size 215 | def self.digest_size_value(digest_size) 216 | digest_size = digest_size.to_i 217 | raise LengthError, "digest size too short" if digest_size < ARGON2_MIN_OUTLEN 218 | raise LengthError, "digest size too long" if digest_size > ARGON2_MAX_OUTLEN 219 | 220 | digest_size 221 | end 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /lib/rbnacl/password_hash/scrypt.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module PasswordHash 6 | # The scrypt sequential memory hard password hashing function 7 | # 8 | # scrypt is a password hash (or password based KDF). That is to say, where 9 | # most hash functions are designed to be fast because hashing is often a 10 | # bottleneck, scrypt is slow by design, because it's trying to "strengthen" 11 | # the password by combining it with a random "salt" value then perform a 12 | # series of operation on the result which are slow enough to defeat 13 | # brute-force password cracking attempts. 14 | # 15 | # scrypt is similar to the bcrypt and pbkdf2 password hashes in that it's 16 | # designed to strengthen passwords, but includes a new design element 17 | # called "sequential memory hardness" which helps defeat attempts by 18 | # attackers to compensate for their lack of memory (since they're typically 19 | # on GPUs or FPGAs) with additional computation. 20 | class SCrypt 21 | extend Sodium 22 | sodium_type :pwhash 23 | sodium_primitive :scryptsalsa208sha256 24 | 25 | sodium_constant :SALTBYTES 26 | sodium_constant :OPSLIMIT_SENSITIVE 27 | sodium_constant :MEMLIMIT_SENSITIVE 28 | 29 | sodium_function :scrypt, 30 | :crypto_pwhash_scryptsalsa208sha256, 31 | %i[pointer ulong_long pointer ulong_long pointer ulong_long size_t] 32 | 33 | # Create a new SCrypt password hash object 34 | # 35 | # @param [Integer] opslimit the CPU cost (e.g. 2**20) 36 | # @param [Integer] memlimit the memory cost (e.g. 2**24) 37 | # 38 | # @return [RbNaCl::PasswordHash::SCrypt] An SCrypt password hasher object 39 | def initialize(opslimit, memlimit, digest_size = 64) 40 | # TODO: sanity check these parameters 41 | @opslimit = opslimit 42 | @memlimit = memlimit 43 | 44 | # TODO: check digest size validity 45 | # raise LengthError, "digest size too short" if @digest_size < BYTES_MIN 46 | # raise LengthError, "digest size too long" if @digest_size > BYTES_MAX 47 | 48 | @digest_size = digest_size 49 | end 50 | 51 | # Calculate an scrypt digest for a given password and salt 52 | # 53 | # @param [String] password to be hashed 54 | # @param [String] salt to make the digest unique 55 | # 56 | # @return [String] scrypt digest of the string as raw bytes 57 | def digest(password, salt) 58 | digest = Util.zeros(@digest_size) 59 | salt = Util.check_string(salt, SALTBYTES, "salt") 60 | 61 | success = self.class.scrypt(digest, @digest_size, password, password.bytesize, salt, @opslimit, @memlimit) 62 | raise CryptoError, "scrypt failed!" unless success 63 | 64 | digest 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/rbnacl/random.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Functions for random number generation 6 | # 7 | # This uses the underlying source of random number generation on the OS, so 8 | # /dev/urandom on UNIX-like systems, and the MS crypto providor on windows. 9 | module Random 10 | extend Sodium 11 | 12 | @mutex = Mutex.new 13 | 14 | sodium_function :c_random_bytes, 15 | :randombytes_buf, 16 | %i[pointer size_t] 17 | 18 | # Returns a string of random bytes 19 | # 20 | # @param [Integer] n number of random bytes desired 21 | # 22 | # @return [String] random bytes. 23 | def self.random_bytes(n = 32) 24 | buf = RbNaCl::Util.zeros(n) 25 | @mutex.synchronize { c_random_bytes(buf, n) } 26 | buf 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rbnacl/secret_boxes/xsalsa20poly1305.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module SecretBoxes 6 | # The SecretBox class boxes and unboxes messages 7 | # 8 | # This class uses the given secret key to encrypt and decrypt messages. 9 | # 10 | # It is VITALLY important that the nonce is a nonce, i.e. it is a number used 11 | # only once for any given pair of keys. If you fail to do this, you 12 | # compromise the privacy of the messages encrypted. Give your nonces a 13 | # different prefix, or have one side use an odd counter and one an even counter. 14 | # Just make sure they are different. 15 | # 16 | # The ciphertexts generated by this class include a 16-byte authenticator which 17 | # is checked as part of the decryption. An invalid authenticator will cause 18 | # the unbox function to raise. The authenticator is not a signature. Once 19 | # you've looked in the box, you've demonstrated the ability to create 20 | # arbitrary valid messages, so messages you send are repudiable. For 21 | # non-repudiable messages, sign them before or after encryption. 22 | class XSalsa20Poly1305 23 | extend Sodium 24 | 25 | sodium_type :secretbox 26 | sodium_primitive :xsalsa20poly1305 27 | sodium_constant :KEYBYTES 28 | sodium_constant :NONCEBYTES 29 | sodium_constant :ZEROBYTES 30 | sodium_constant :BOXZEROBYTES 31 | 32 | sodium_function :secretbox_xsalsa20poly1305, 33 | :crypto_secretbox_xsalsa20poly1305, 34 | %i[pointer pointer ulong_long pointer pointer] 35 | 36 | sodium_function :secretbox_xsalsa20poly1305_open, 37 | :crypto_secretbox_xsalsa20poly1305_open, 38 | %i[pointer pointer ulong_long pointer pointer] 39 | 40 | # Create a new SecretBox 41 | # 42 | # Sets up the Box with a secret key fro encrypting and decrypting messages. 43 | # 44 | # @param key [String] The key to encrypt and decrypt with 45 | # 46 | # @raise [RbNaCl::LengthError] on invalid keys 47 | # 48 | # @return [RbNaCl::SecretBox] The new Box, ready to use 49 | def initialize(key) 50 | @key = Util.check_string(key, KEYBYTES, "Secret key") 51 | end 52 | 53 | # Encrypts a message 54 | # 55 | # Encrypts the message with the given nonce to the key set up when 56 | # initializing the class. Make sure the nonce is unique for any given 57 | # key, or you might as well just send plain text. 58 | # 59 | # This function takes care of the padding required by the NaCL C API. 60 | # 61 | # @param nonce [String] A 24-byte string containing the nonce. 62 | # @param message [String] The message to be encrypted. 63 | # 64 | # @raise [RbNaCl::LengthError] If the nonce is not valid 65 | # 66 | # @return [String] The ciphertext without the nonce prepended (BINARY encoded) 67 | def box(nonce, message) 68 | Util.check_length(nonce, nonce_bytes, "Nonce") 69 | msg = Util.prepend_zeros(ZEROBYTES, message) 70 | ct = Util.zeros(msg.bytesize) 71 | 72 | success = self.class.secretbox_xsalsa20poly1305(ct, msg, msg.bytesize, nonce, @key) 73 | raise CryptoError, "Encryption failed" unless success 74 | 75 | Util.remove_zeros(BOXZEROBYTES, ct) 76 | end 77 | alias encrypt box 78 | 79 | # Decrypts a ciphertext 80 | # 81 | # Decrypts the ciphertext with the given nonce using the key setup when 82 | # initializing the class. 83 | # 84 | # This function takes care of the padding required by the NaCL C API. 85 | # 86 | # @param nonce [String] A 24-byte string containing the nonce. 87 | # @param ciphertext [String] The message to be decrypted. 88 | # 89 | # @raise [RbNaCl::LengthError] If the nonce is not valid 90 | # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. 91 | # 92 | # @return [String] The decrypted message (BINARY encoded) 93 | def open(nonce, ciphertext) 94 | Util.check_length(nonce, nonce_bytes, "Nonce") 95 | ct = Util.prepend_zeros(BOXZEROBYTES, ciphertext) 96 | message = Util.zeros(ct.bytesize) 97 | 98 | success = self.class.secretbox_xsalsa20poly1305_open(message, ct, ct.bytesize, nonce, @key) 99 | raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success 100 | 101 | Util.remove_zeros(ZEROBYTES, message) 102 | end 103 | alias decrypt open 104 | 105 | # The crypto primitive for the SecretBox instance 106 | # 107 | # @return [Symbol] The primitive used 108 | def primitive 109 | self.class.primitive 110 | end 111 | 112 | # The nonce bytes for the SecretBox class 113 | # 114 | # @return [Integer] The number of bytes in a valid nonce 115 | def self.nonce_bytes 116 | NONCEBYTES 117 | end 118 | 119 | # The nonce bytes for the SecretBox instance 120 | # 121 | # @return [Integer] The number of bytes in a valid nonce 122 | def nonce_bytes 123 | NONCEBYTES 124 | end 125 | 126 | # The key bytes for the SecretBox class 127 | # 128 | # @return [Integer] The number of bytes in a valid key 129 | def self.key_bytes 130 | KEYBYTES 131 | end 132 | 133 | # The key bytes for the SecretBox instance 134 | # 135 | # @return [Integer] The number of bytes in a valid key 136 | def key_bytes 137 | KEYBYTES 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/rbnacl/self_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | start = Time.now if $DEBUG 5 | 6 | # NaCl/libsodium for Ruby 7 | module RbNaCl 8 | class SelfTestFailure < RbNaCl::CryptoError; end 9 | 10 | # Self-test performed at startup 11 | module SelfTest 12 | module_function 13 | 14 | def vector(name) 15 | [TEST_VECTORS[name]].pack("H*") 16 | end 17 | 18 | def box_test 19 | alicepk = RbNaCl::PublicKey.new(vector(:alice_public)) 20 | bobsk = RbNaCl::PrivateKey.new(vector(:bob_private)) 21 | 22 | box = RbNaCl::Box.new(alicepk, bobsk) 23 | box_common_test(box) 24 | end 25 | 26 | def secret_box_test 27 | box = SecretBox.new(vector(:secret_key)) 28 | box_common_test(box) 29 | end 30 | 31 | def box_common_test(box) 32 | nonce = vector :box_nonce 33 | message = vector :box_message 34 | ciphertext = vector :box_ciphertext 35 | 36 | raise SelfTestFailure, "failed to generate correct ciphertext" unless box.encrypt(nonce, message) == ciphertext 37 | raise SelfTestFailure, "failed to decrypt ciphertext correctly" unless box.decrypt(nonce, ciphertext) == message 38 | 39 | begin 40 | passed = false 41 | corrupt_ct = ciphertext.dup 42 | corrupt_ct[23] = " " 43 | box.decrypt(nonce, corrupt_ct) 44 | rescue CryptoError 45 | passed = true 46 | ensure 47 | passed || raise(SelfTestFailure, "failed to detect corrupt ciphertext") 48 | end 49 | end 50 | 51 | def digital_signature_test 52 | signing_key = SigningKey.new(vector(:sign_private)) 53 | verify_key = signing_key.verify_key 54 | 55 | unless verify_key.to_s == vector(:sign_public) 56 | # :nocov: 57 | raise SelfTestFailure, "failed to generate verify key correctly" 58 | # :nocov: 59 | end 60 | 61 | message = vector :sign_message 62 | signature = signing_key.sign(message) 63 | 64 | unless signature == vector(:sign_signature) 65 | # :nocov: 66 | raise SelfTestFailure, "failed to generate correct signature" 67 | # :nocov: 68 | end 69 | 70 | unless verify_key.verify(signature, message) 71 | # :nocov: 72 | raise SelfTestFailure, "failed to verify a valid signature" 73 | # :nocov: 74 | end 75 | 76 | begin 77 | passed = false 78 | bad_signature = signature[0, 63] + "0" 79 | verify_key.verify(bad_signature, message) 80 | rescue CryptoError 81 | passed = true 82 | ensure 83 | passed || raise(SelfTestFailure, "failed to detect corrupt ciphertext") 84 | end 85 | end 86 | 87 | def sha256_test 88 | message = vector :sha256_message 89 | digest = vector :sha256_digest 90 | 91 | raise SelfTestFailure, "failed to generate a correct SHA256 digest" unless RbNaCl::Hash.sha256(message) == digest 92 | end 93 | 94 | def hmac_test(klass, tag) 95 | authenticator = klass.new(vector("auth_key_#{klass.key_bytes}".to_sym)) 96 | 97 | message = vector :auth_message 98 | 99 | raise SelfTestFailure, "#{klass} generated incorrect authentication tag" unless authenticator.auth(message) == vector(tag) 100 | raise SelfTestFailure, "#{klass} failed to verify authentication tag" unless authenticator.verify(vector(tag), message) 101 | 102 | begin 103 | passed = false 104 | authenticator.verify(vector(tag), message + " ") 105 | rescue CryptoError 106 | passed = true 107 | ensure 108 | passed || raise(SelfTestFailure, "failed to detect corrupt ciphertext") 109 | end 110 | end 111 | end 112 | end 113 | 114 | RbNaCl::SelfTest.box_test 115 | RbNaCl::SelfTest.secret_box_test 116 | RbNaCl::SelfTest.digital_signature_test 117 | RbNaCl::SelfTest.sha256_test 118 | RbNaCl::SelfTest.hmac_test RbNaCl::HMAC::SHA256, :auth_hmacsha256 119 | RbNaCl::SelfTest.hmac_test RbNaCl::HMAC::SHA512256, :auth_hmacsha512256 120 | RbNaCl::SelfTest.hmac_test RbNaCl::OneTimeAuth, :auth_onetime 121 | 122 | puts "POST Completed in #{Time.now - start} s" if $DEBUG 123 | -------------------------------------------------------------------------------- /lib/rbnacl/serializable.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Serialization features shared across all "key-like" classes 6 | module Serializable 7 | def to_s 8 | to_bytes 9 | end 10 | 11 | def to_str 12 | to_bytes 13 | end 14 | 15 | # Inspect this key 16 | # 17 | # @return [String] a string representing this key 18 | def inspect 19 | "#<#{self.class}:#{Util.bin2hex(to_bytes)[0, 8]}>" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rbnacl/signatures/ed25519.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Signatures 6 | # The EdDSA signature system implemented using the Ed25519 elliptic curve 7 | module Ed25519 8 | extend Sodium 9 | 10 | sodium_type :sign 11 | sodium_primitive :ed25519 12 | sodium_constant :SEEDBYTES 13 | sodium_constant :PUBLICKEYBYTES, name: :VERIFYKEYBYTES 14 | sodium_constant :SECRETKEYBYTES, name: :SIGNINGKEYBYTES 15 | sodium_constant :BYTES, name: :SIGNATUREBYTES 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rbnacl/signatures/ed25519/signing_key.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Signatures 6 | module Ed25519 7 | # Private key for producing digital signatures using the Ed25519 algorithm. 8 | # Ed25519 provides a 128-bit security level, that is to say, all known attacks 9 | # take at least 2^128 operations, providing the same security level as 10 | # AES-128, NIST P-256, and RSA-3072. 11 | # 12 | # Signing keys are produced from a 32-byte (256-bit) random seed value. 13 | # This value can be passed into the SigningKey constructor as a String 14 | # whose bytesize is 32. 15 | # 16 | # The public VerifyKey can be computed from the private 32-byte seed value 17 | # as well, eliminating the need to store a "keypair". 18 | # 19 | # SigningKey produces 64-byte (512-bit) signatures. The signatures are 20 | # deterministic: signing the same message will always produce the same 21 | # signature. This prevents "entropy failure" seen in other signature 22 | # algorithms like DSA and ECDSA, where poor random number generators can 23 | # leak enough information to recover the private key. 24 | class SigningKey 25 | include KeyComparator 26 | include Serializable 27 | 28 | extend Sodium 29 | 30 | sodium_type :sign 31 | sodium_primitive :ed25519 32 | 33 | sodium_function :sign_ed25519, 34 | :crypto_sign_ed25519, 35 | %i[pointer pointer pointer ulong_long pointer] 36 | 37 | sodium_function :sign_ed25519_seed_keypair, 38 | :crypto_sign_ed25519_seed_keypair, 39 | %i[pointer pointer pointer] 40 | 41 | sodium_function :to_private_key, 42 | :crypto_sign_ed25519_sk_to_curve25519, 43 | %i[pointer pointer] 44 | 45 | attr_reader :verify_key 46 | 47 | # Generate a random SigningKey 48 | # 49 | # @return [RbNaCl::SigningKey] Freshly-generated random SigningKey 50 | def self.generate 51 | new RbNaCl::Random.random_bytes(Ed25519::SEEDBYTES) 52 | end 53 | 54 | # Create a SigningKey from a seed value 55 | # 56 | # @param seed [String] Random 32-byte value (i.e. private key) 57 | # 58 | # @return [RbNaCl::SigningKey] Key which can sign messages 59 | def initialize(seed) 60 | seed = seed.to_s 61 | 62 | Util.check_length(seed, Ed25519::SEEDBYTES, "seed") 63 | 64 | pk = Util.zeros(Ed25519::VERIFYKEYBYTES) 65 | sk = Util.zeros(Ed25519::SIGNINGKEYBYTES) 66 | 67 | self.class.sign_ed25519_seed_keypair(pk, sk, seed) || raise(CryptoError, "Failed to generate a key pair") 68 | 69 | @seed = seed 70 | @signing_key = sk 71 | @verify_key = VerifyKey.new(pk) 72 | end 73 | 74 | # Sign a message using this key 75 | # 76 | # @param message [String] Message to be signed by this key 77 | # 78 | # @return [String] Signature as bytes 79 | def sign(message) 80 | sign_attached(message)[0, signature_bytes] 81 | end 82 | 83 | # Sign a message using this key, attaching the signature to the message 84 | # 85 | # @param message [String] Message to be signed by this key 86 | # 87 | # @return [String] Signature and the message as bytes 88 | def sign_attached(message) 89 | buffer = Util.prepend_zeros(signature_bytes, message) 90 | buffer_len = Util.zeros(FFI::Type::LONG_LONG.size) 91 | self.class.sign_ed25519(buffer, buffer_len, message, message.bytesize, @signing_key) 92 | buffer 93 | end 94 | 95 | # Return the raw seed value of this key 96 | # 97 | # @return [String] seed used to create this key 98 | def to_bytes 99 | @seed 100 | end 101 | 102 | # Return the raw 64 byte value of this key 103 | # 104 | # @return [String] The signature key bytes. Left half is 32-byte 105 | # curve25519 private scalar, right half is 32-byte group element 106 | def keypair_bytes 107 | @signing_key 108 | end 109 | 110 | # The crypto primitive this SigningKey class uses for signatures 111 | # 112 | # @return [Symbol] The primitive 113 | def primitive 114 | self.class.primitive 115 | end 116 | 117 | # The size of signatures generated by the SigningKey class 118 | # 119 | # @return [Integer] The number of bytes in a signature 120 | def self.signature_bytes 121 | Ed25519::SIGNATUREBYTES 122 | end 123 | 124 | # The size of signatures generated by the SigningKey instance 125 | # 126 | # @return [Integer] The number of bytes in a signature 127 | def signature_bytes 128 | Ed25519::SIGNATUREBYTES 129 | end 130 | 131 | # Return a new curve25519 (x25519) private key converted from this key 132 | # 133 | # it's recommeneded to read https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 134 | # as it encourages using distinct keys for signing and for encryption 135 | # 136 | # @return [RbNaCl::PrivateKey] 137 | def to_curve25519_private_key 138 | buffer = Util.zeros(Boxes::Curve25519XSalsa20Poly1305::PrivateKey::BYTES) 139 | self.class.crypto_sign_ed25519_sk_to_curve25519(buffer, @signing_key) 140 | Boxes::Curve25519XSalsa20Poly1305::PrivateKey.new(buffer) 141 | end 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/rbnacl/signatures/ed25519/verify_key.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | module Signatures 6 | module Ed25519 7 | # The public key counterpart to an Ed25519 SigningKey for producing digital 8 | # signatures. Like the name says, VerifyKeys can be used to verify that a 9 | # given digital signature is authentic. 10 | # 11 | # For more information on the Ed25519 digital signature system, please see 12 | # the SigningKey documentation. 13 | class VerifyKey 14 | include KeyComparator 15 | include Serializable 16 | 17 | extend Sodium 18 | 19 | sodium_type :sign 20 | sodium_primitive :ed25519 21 | 22 | sodium_function :sign_ed25519_open, 23 | :crypto_sign_ed25519_open, 24 | %i[pointer pointer pointer ulong_long pointer] 25 | 26 | sodium_function :to_public_key, 27 | :crypto_sign_ed25519_pk_to_curve25519, 28 | %i[pointer pointer] 29 | 30 | # Create a new VerifyKey object from a public key. 31 | # 32 | # @param key [String] Ed25519 public key 33 | # 34 | # @return [RbNaCl::VerifyKey] Key which can verify messages 35 | def initialize(key) 36 | @key = key.to_str 37 | Util.check_length(@key, Ed25519::VERIFYKEYBYTES, "key") 38 | end 39 | 40 | # Verify a signature for a given message 41 | # 42 | # Raises if the signature is invalid. 43 | # 44 | # @param signature [String] Alleged signature to be checked 45 | # @param message [String] Message to be authenticated 46 | # 47 | # @raise [BadSignatureError] if the signature check fails 48 | # @raise [LengthError] if the signature is of the wrong length 49 | # 50 | # @return [Boolean] was the signature authentic? 51 | def verify(signature, message) 52 | signature = signature.to_str 53 | Util.check_length(signature, signature_bytes, "signature") 54 | verify_attached(signature + message) 55 | end 56 | 57 | # Verify a signature for a given signed message 58 | # 59 | # Raises if the signature is invalid. 60 | # 61 | # @param signed_message [String] Message combined with signature to be authenticated 62 | # 63 | # @raise [BadSignatureError] if the signature check fails 64 | # 65 | # @return [Boolean] was the signature authentic? 66 | def verify_attached(signed_message) 67 | raise LengthError, "Signed message can not be nil" if signed_message.nil? 68 | raise LengthError, "Signed message can not be shorter than a signature" if signed_message.bytesize <= signature_bytes 69 | 70 | buffer = Util.zeros(signed_message.bytesize) 71 | buffer_len = Util.zeros(FFI::Type::LONG_LONG.size) 72 | 73 | success = self.class.sign_ed25519_open(buffer, buffer_len, signed_message, signed_message.bytesize, @key) 74 | raise(BadSignatureError, "signature was forged/corrupt") unless success 75 | 76 | true 77 | end 78 | 79 | # Return the raw key in byte format 80 | # 81 | # @return [String] raw key as bytes 82 | def to_bytes 83 | @key 84 | end 85 | 86 | # The crypto primitive this VerifyKey class uses for signatures 87 | # 88 | # @return [Symbol] The primitive 89 | def primitive 90 | self.class.primitive 91 | end 92 | 93 | # The size of signatures verified by the VerifyKey class 94 | # 95 | # @return [Integer] The number of bytes in a signature 96 | def self.signature_bytes 97 | Ed25519::SIGNATUREBYTES 98 | end 99 | 100 | # The size of signatures verified by the VerifyKey instance 101 | # 102 | # @return [Integer] The number of bytes in a signature 103 | def signature_bytes 104 | Ed25519::SIGNATUREBYTES 105 | end 106 | 107 | # Return a new curve25519 (x25519) public key converted from this key 108 | # 109 | # it's recommeneded to read https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 110 | # as it encourages using distinct keys for signing and for encryption 111 | # 112 | # @return [RbNaCl::PublicKey] 113 | def to_curve25519_public_key 114 | buffer = Util.zeros(Boxes::Curve25519XSalsa20Poly1305::PublicKey::BYTES) 115 | self.class.crypto_sign_ed25519_pk_to_curve25519(buffer, @key) 116 | Boxes::Curve25519XSalsa20Poly1305::PublicKey.new(buffer) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/rbnacl/simple_box.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | require "forwardable" 5 | 6 | # NaCl/libsodium for Ruby 7 | module RbNaCl 8 | # The simplest nonce strategy that could possibly work 9 | # 10 | # This class implements the simplest possible nonce generation strategy to 11 | # wrap a RbNaCl::Box or RbNaCl::SecretBox. A 24-byte random nonce is used 12 | # for the encryption and is prepended to the message. When it is time to 13 | # open the box, the message is split into nonce and ciphertext, and then the 14 | # box is decrypted. 15 | # 16 | # Thanks to the size of the nonce, the chance of a collision is negligible. For 17 | # example, after encrypting 2^64 messages, the odds of their having been 18 | # repeated nonce is approximately 2^-64. As an additional convenience, the 19 | # ciphertexts may be encoded or decoded by any of the encoders implemented in 20 | # the library. 21 | # 22 | # The resulting ciphertexts are 40 bytes longer than the plain text (24 byte 23 | # nonce plus a 16 byte authenticator). This might be annoying if you're 24 | # encrypting tweets, but for files represents a fairly small overhead. 25 | # 26 | # Some caveats: 27 | # 28 | # * If your random source is broken, so is the security of the messages. You 29 | # have bigger problems than just this library at that point, but it's worth 30 | # saying. 31 | # * The confidentiality of your messages is assured with this strategy, but 32 | # there is no protection against messages being reordered and replayed by an 33 | # active adversary. 34 | class SimpleBox 35 | extend Forwardable 36 | def_delegators :@box, :nonce_bytes, :primitive 37 | 38 | # Create a new SimpleBox 39 | # 40 | # @param box [SecretBox, Box] the SecretBox or Box to use. 41 | # 42 | # @return [SimpleBox] Ready for use 43 | def initialize(box) 44 | @box = box 45 | end 46 | 47 | # Use a secret key to create a SimpleBox 48 | # 49 | # This is a convenience method. It takes a secret key and instantiates a 50 | # SecretBox under the hood, then returns the new SimpleBox. 51 | # 52 | # @param secret_key [String] The secret key, 32 bytes long. 53 | # 54 | # @return [SimpleBox] Ready for use 55 | def self.from_secret_key(secret_key) 56 | new(SecretBox.new(secret_key)) 57 | end 58 | 59 | # Use a pair of keys to create a SimpleBox 60 | # 61 | # This is a convenience method. It takes a pair of keys and instantiates a 62 | # Box under the hood, then returns the new SimpleBox. 63 | # 64 | # @param public_key [PublicKey, String] The RbNaCl public key, as class or string 65 | # @param private_key [PrivateKey, String] The RbNaCl private key, as class or string 66 | # 67 | # @return [SimpleBox] Ready for use 68 | def self.from_keypair(public_key, private_key) 69 | new(Box.new(public_key, private_key)) 70 | end 71 | 72 | # Encrypts the message with a random nonce 73 | # 74 | # Encrypts the message with a random nonce, then returns the ciphertext with 75 | # the nonce prepended. Optionally encodes the message using an encoder. 76 | # 77 | # @param message [String] The message to encrypt 78 | # 79 | # @return [String] The enciphered message 80 | def box(message) 81 | nonce = generate_nonce 82 | cipher_text = @box.box(nonce, message) 83 | nonce + cipher_text 84 | end 85 | alias encrypt box 86 | 87 | # Decrypts the ciphertext with a random nonce 88 | # 89 | # Takes a ciphertext, optionally decodes it, then splits the nonce off the 90 | # front and uses this to decrypt. Returns the message. 91 | # 92 | # @param enciphered_message [String] The message to decrypt. 93 | # 94 | # @raise [CryptoError] If the message has been tampered with. 95 | # 96 | # @return [String] The decoded message 97 | def open(enciphered_message) 98 | nonce, ciphertext = extract_nonce(enciphered_message.to_s) 99 | @box.open(nonce, ciphertext) 100 | end 101 | alias decrypt open 102 | 103 | private 104 | 105 | def generate_nonce 106 | Random.random_bytes(nonce_bytes) 107 | end 108 | 109 | def extract_nonce(bytes) 110 | nonce = bytes.slice(0, nonce_bytes) 111 | [nonce, bytes.slice(nonce_bytes..-1)] 112 | end 113 | end 114 | 115 | # Backwards compatibility with the old RandomNonceBox name 116 | RandomNonceBox = SimpleBox 117 | end 118 | -------------------------------------------------------------------------------- /lib/rbnacl/sodium.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | require "ffi" 5 | 6 | module RbNaCl 7 | # Provides helpers for defining the libsodium bindings 8 | module Sodium 9 | def self.extended(klass) 10 | klass.extend FFI::Library 11 | klass.ffi_lib ["sodium", "libsodium.so.18", "libsodium.so.23", "libsodium.so.26"] 12 | end 13 | 14 | def sodium_type(type = nil) 15 | return @type if type.nil? 16 | 17 | @type = type 18 | end 19 | 20 | def sodium_primitive(primitive = nil) 21 | if primitive.nil? 22 | @primitive if defined?(@primitive) 23 | else 24 | @primitive = primitive 25 | end 26 | end 27 | 28 | def primitive 29 | sodium_primitive 30 | end 31 | 32 | def sodium_constant(constant, name: constant, fallback: nil) 33 | fn_name_components = ["crypto", sodium_type, sodium_primitive, constant.to_s.downcase] 34 | fn_name = fn_name_components.compact.join("_") 35 | 36 | begin 37 | attach_function fn_name, [], :size_t 38 | rescue FFI::NotFoundError 39 | raise if fallback.nil? 40 | 41 | define_singleton_method fn_name, -> { fallback } 42 | end 43 | 44 | const_set(name, public_send(fn_name)) 45 | end 46 | 47 | def sodium_function(name, function, arguments) 48 | module_eval <<-RUBY, __FILE__, __LINE__ + 1 49 | attach_function #{function.inspect}, #{arguments.inspect}, :int 50 | def self.#{name}(*args) 51 | ret = #{function}(*args) 52 | ret == 0 53 | end 54 | RUBY 55 | end 56 | 57 | def sodium_function_with_return_code(name, function, arguments) 58 | module_eval <<-RUBY, __FILE__, __LINE__ + 1 59 | attach_function #{function.inspect}, #{arguments.inspect}, :int 60 | def self.#{name}(*args) 61 | #{function}(*args) 62 | end 63 | RUBY 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/rbnacl/sodium/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | require "rbnacl/sodium" 5 | 6 | module RbNaCl 7 | module Sodium 8 | # libsodium version API 9 | module Version 10 | MINIMUM_LIBSODIUM_VERSION = [0, 4, 3].freeze 11 | MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2 = [1, 0, 9].freeze 12 | MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2ID = [1, 0, 13].freeze 13 | 14 | extend Sodium 15 | attach_function :sodium_version_string, [], :string 16 | 17 | STRING = sodium_version_string 18 | MAJOR, MINOR, PATCH = STRING.split(".").map(&:to_i) 19 | 20 | INSTALLED_VERSION = [MAJOR, MINOR, PATCH].freeze 21 | 22 | case INSTALLED_VERSION <=> MINIMUM_LIBSODIUM_VERSION 23 | when -1 24 | raise "Sorry, you need to install libsodium #{MINIMUM_LIBSODIUM_VERSION}+. You have #{Version::STRING} installed" 25 | end 26 | 27 | ARGON2_SUPPORTED = (INSTALLED_VERSION <=> MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2) != -1 28 | ARGON2ID_SUPPORTED = (INSTALLED_VERSION <=> MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2ID) != -1 29 | 30 | # Determine if a given feature is supported based on Sodium version 31 | def self.supported_version?(version) 32 | Gem::Version.new(sodium_version_string) >= Gem::Version.new(version) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rbnacl/test_vectors.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | # NaCl/libsodium for Ruby 5 | # rubocop:disable Metrics/ModuleLength 6 | module RbNaCl 7 | # Reference library of test vectors used to verify the software is correct 8 | TEST_VECTORS = { 9 | # 10 | # Curve25519 test vectors 11 | # Taken from the NaCl distribution 12 | # 13 | alice_private: "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", 14 | alice_public: "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a", 15 | bob_private: "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", 16 | bob_public: "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", 17 | alice_mult_bob: "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742", 18 | 19 | # 20 | # Box test vectors 21 | # Taken from the NaCl distribution 22 | # 23 | secret_key: "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389", 24 | box_nonce: "69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37", 25 | box_message: "be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffc" \ 26 | "e5ecbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb31" \ 27 | "0e3be8250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde" \ 28 | "048977eb48f59ffd4924ca1c60902e52f0a089bc76897040e082f93776384864" \ 29 | "5e0705", 30 | 31 | box_ciphertext: "f3ffc7703f9400e52a7dfb4b3d3305d98e993b9f48681273c29650ba32fc76ce" \ 32 | "48332ea7164d96a4476fb8c531a1186ac0dfc17c98dce87b4da7f011ec48c972" \ 33 | "71d2c20f9b928fe2270d6fb863d51738b48eeee314a7cc8ab932164548e526ae" \ 34 | "90224368517acfeabd6bb3732bc0e9da99832b61ca01b6de56244a9e88d5f9b3" \ 35 | "7973f622a43d14a6599b1f654cb45a74e355a5", 36 | 37 | # 38 | # Ed25519 test vectors 39 | # Taken from the Python test vectors: http://ed25519.cr.yp.to/python/sign.input 40 | # 41 | sign_private: "b18e1d0045995ec3d010c387ccfeb984d783af8fbb0f40fa7db126d889f6dadd", 42 | sign_public: "77f48b59caeda77751ed138b0ec667ff50f8768c25d48309a8f386a2bad187fb", 43 | sign_keypair: "b18e1d0045995ec3d010c387ccfeb984d783af8fbb0f40fa7db126d889f6dadd" \ 44 | "77f48b59caeda77751ed138b0ec667ff50f8768c25d48309a8f386a2bad187fb", 45 | sign_message: "916c7d1d268fc0e77c1bef238432573c39be577bbea0998936add2b50a653171" \ 46 | "ce18a542b0b7f96c1691a3be6031522894a8634183eda38798a0c5d5d79fbd01" \ 47 | "dd04a8646d71873b77b221998a81922d8105f892316369d5224c9983372d2313" \ 48 | "c6b1f4556ea26ba49d46e8b561e0fc76633ac9766e68e21fba7edca93c4c7460" \ 49 | "376d7f3ac22ff372c18f613f2ae2e856af40", 50 | sign_signature: "6bd710a368c1249923fc7a1610747403040f0cc30815a00f9ff548a896bbda0b" \ 51 | "4eb2ca19ebcf917f0f34200a9edbad3901b64ab09cc5ef7b9bcc3c40c0ff7509", 52 | sign_curve25519_private: "38e5cdf33bc9e13086f58a3fea86d574e85e7865cffa5e8c9335f200a41d036c", 53 | sign_curve25519_public: "35488a98f7ec26ae27099809afb27587b198b1197b5bcb0dec41153db2bf9952", 54 | 55 | # 56 | # SHA256 test vectors 57 | # Taken from the NSRL test vectors: http://www.nsrl.nist.gov/testdata/ 58 | sha256_message: "6162636462636465636465666465666765666768666768696768696a68696a6b" \ 59 | "696a6b6c6a6b6c6d6b6c6d6e6c6d6e6f6d6e6f706e6f7071", 60 | sha256_digest: "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", 61 | sha256_empty: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 62 | 63 | # 64 | # SHA512 test vectors 65 | # self-created (FIXME: find standard test vectors) 66 | sha512_message: "54686520717569636b2062726f776e20666f78206a756d7073206f7665722074" \ 67 | "6865206c617a7920646f672e", 68 | sha512_digest: "91ea1245f20d46ae9a037a989f54f1f790f0a47607eeb8a14d12890cea77a1bb" \ 69 | "c6c7ed9cf205e67b7f2b8fd4c7dfd3a7a8617e45f3c463d481c7e586c39ac1ed", 70 | sha512_empty: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" \ 71 | "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", 72 | 73 | # Blake2b test vectors 74 | # self-created? (TODO: double check, fix) 75 | blake2b_message: "54686520717569636b2062726f776e20666f78206a756d7073206f7665722074" \ 76 | "6865206c617a7920646f67", 77 | blake2b_digest: "a8add4bdddfd93e4877d2746e62817b116364a1fa7bc148d95090bc7333b3673" \ 78 | "f82401cf7aa2e4cb1ecd90296e3f14cb5413f8ed77be73045b13914cdcd6a918", 79 | blake2b_empty: "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419" \ 80 | "d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce", 81 | 82 | # from the Blake2 paper(?) (TODO: double check) 83 | blake2b_keyed_message: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" \ 84 | "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" \ 85 | "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f" \ 86 | "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" \ 87 | "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" \ 88 | "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf" \ 89 | "c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf" \ 90 | "e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe", 91 | blake2b_key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" \ 92 | "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", 93 | blake2b_keyed_digest: "142709d62e28fcccd0af97fad0f8465b971e82201dc51070faa0372aa43e9248" \ 94 | "4be1c1e73ba10906d5d1853db6a4106e0a7bf9800d373d6dee2d46d62ef2a461", 95 | 96 | # Generated using the blake2 reference code 97 | blake2b_personal: "000102030405060708090a0b0c0d0e0f", 98 | 99 | blake2b_personal_digest: "7c86d3f929c9ac7f08c7940095da7c1cad2cf29db2e7a25fb05d99163e587cbd" \ 100 | "f3564e8ce727b734a0559ee76f6ff5aeebd4e1e8872f1829174c9b1a9dab80e3", 101 | 102 | blake2b_salt: "000102030405060708090a0b0c0d0e0f", 103 | 104 | blake2b_salt_digest: "16e2e2cfb97e6061bccf2fcc1e605e117dee806c959ef2ad01249d4d12ce98cb" \ 105 | "c993f400003ba57449f60a7b071ffdaff9c0acb16891a01a9b397ffe89db96bb", 106 | 107 | blake2b_personal_short: "0001020304050607", 108 | 109 | blake2b_personal_short_digest: "41b984967f852308710a6042d25f5faf4a84900b2001039075dab13aecfab7c8" \ 110 | "40def9506326563fbb355b3da629181d97d2556e4624711d68f8f655b7cbb435", 111 | 112 | blake2b_salt_short: "0001020304050607", 113 | 114 | blake2b_salt_short_digest: "873f35a1ca28febc872d6f842a8cd23136f3a2c22c19e8f0dac4cc704ced3371"\ 115 | "abe5105f65d344cd48bad8aba755620f63f1e0b35ae4439bf871ffe72485a309", 116 | 117 | # scrypt test vectors 118 | # Taken from http://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#page-14 119 | scrypt_password: "4a857e2ee8aa9b6056f2424e84d24a72473378906ee04a46cb05311502d5250b" \ 120 | "82ad86b83c8f20a23dbb74f6da60b0b6ecffd67134d45946ac8ebfb3064294bc" \ 121 | "097d43ced68642bfb8bbbdd0f50b30118f5e", 122 | scrypt_salt: "39d82eef32010b8b79cc5ba88ed539fbaba741100f2edbeca7cc171ffeabf258", 123 | scrypt_opslimit: 33_554_432, 124 | scrypt_memlimit: 1_073_741_824, 125 | scrypt_digest: "11a4c60b98411758ba9e89a28587c074ae674c367326c79a999e415110b14460" \ 126 | "5921bd3c897098a837fa40d9eef5338268754ea5e243f630a58fa698df95d1ed", 127 | 128 | # argon2 vectors 129 | # from libsodium/test/default/pwhash_argon2i.c 130 | argon2i_password: "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ 131 | "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ 132 | "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ 133 | "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6", 134 | argon2i_salt: "5541fbc995d5c197ba290346d2c559de", 135 | argon2i_outlen: 155, 136 | argon2i_opslimit: 5, 137 | argon2i_memlimit: 7_256_678, 138 | argon2i_digest: "23b803c84eaa25f4b44634cc1e5e37792c53fcd9b1eb20f865329c68e09cbfa9f19" \ 139 | "68757901b383fce221afe27713f97914a041395bbe1fb70e079e5bed2c7145b1f61" \ 140 | "54046f5958e9b1b29055454e264d1f2231c316f26be2e3738e83a80315e9a0951ce" \ 141 | "4b137b52e7d5ee7b37f7d936dcee51362bcf792595e3c896ad5042734fc90c92cae" \ 142 | "572ce63ff659a2f7974a3bd730d04d525d253ccc38", 143 | 144 | # from libsodium/test/default/pwhash_argon2id.c 145 | argon2id_password: "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ 146 | "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ 147 | "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ 148 | "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6", 149 | argon2id_salt: "5541fbc995d5c197ba290346d2c559de", 150 | argon2id_outlen: 155, 151 | argon2id_opslimit: 5, 152 | argon2id_memlimit: 7_256_678, 153 | argon2id_digest: "18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3ced6" \ 154 | "15337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e7348249d135b" \ 155 | "38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2bea47f91f0" \ 156 | "fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5679534a0700e" \ 157 | "246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1", 158 | 159 | # argon2_str vectors 160 | # from libsodium/test/default/pwhash.c 161 | argon2_str_digest: "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw" \ 162 | "$TNnWIwlu1061JHrnCqIAmjs3huSxYIU+0jWipu7Kc9M", 163 | argon2_str_passwd: "password", 164 | 165 | # Auth test vectors 166 | # Taken from NaCl distribution 167 | # 168 | auth_key_32: "eea6a7251c1e72916d11c2cb214d3c252539121d8e234e652d651fa4c8cff880", 169 | auth_key_64: "eaaa4c73ef13e7e9a53011304c5be141da9c3713b5ca822037ed57aded31b70a" \ 170 | "50a0dd80843d580fe5b57e470bb534333e907a624cf02873c6b9eaba70e0fc7e", 171 | auth_message: "8e993b9f48681273c29650ba32fc76ce48332ea7164d96a4476fb8c531a1186a" \ 172 | "c0dfc17c98dce87b4da7f011ec48c97271d2c20f9b928fe2270d6fb863d51738" \ 173 | "b48eeee314a7cc8ab932164548e526ae90224368517acfeabd6bb3732bc0e9da" \ 174 | "99832b61ca01b6de56244a9e88d5f9b37973f622a43d14a6599b1f654cb45a74" \ 175 | "e355a5", 176 | auth_onetime: "f3ffc7703f9400e52a7dfb4b3d3305d9", 177 | # self-created (FIXME: find standard test vectors) 178 | auth_hmacsha256: "7f7b9b707e8790ca8620ff94df5e6533ddc8e994060ce310c9d7de04d44aabc3", 179 | auth_hmacsha512256: "b2a31b8d4e01afcab2ee545b5caf4e3d212a99d7b3a116a97cec8e83c32e107d", 180 | auth_hmacsha512: "b2a31b8d4e01afcab2ee545b5caf4e3d212a99d7b3a116a97cec8e83c32e107d" \ 181 | "270e3921f69016c267a63ab4b226449a0dee0dc7dcb897a9bce9d27d788f8e8d", 182 | 183 | # HMAC-SHA Identifiers and Test Vectors 184 | # ref: https://tools.ietf.org/html/rfc4231#section-4.8 185 | # 186 | auth_hmac_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ 187 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ 188 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ 189 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ 190 | "aaaaaa", 191 | auth_hmac_data: "5468697320697320612074657374207573696e672061206c6172676572207468" \ 192 | "616e20626c6f636b2d73697a65206b657920616e642061206c61726765722074" \ 193 | "68616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565" \ 194 | "647320746f20626520686173686564206265666f7265206265696e6720757365" \ 195 | "642062792074686520484d414320616c676f726974686d2e", 196 | auth_hmacsha256_tag: "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", 197 | auth_hmacsha512_tag: "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944" \ 198 | "b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58", 199 | auth_hmacsha512256_tag: "bfaae3b4292b56d6170154cc089af73f79e089ecf27d4720eed6fd0a7ffcccf1", 200 | 201 | auth_hmacsha256_mult_tag: "367a7a7e8292759844dcf820c90daa5fea5a4b769e537038cd0dc28290fbf2cb", 202 | auth_hmacsha512_mult_tag: "1006b7bef1e24725ed55049c8b787b7b174f4afbe197124a389205c499956a90" \ 203 | "fea5c44b616a9e1a286d024c2880c67ae0e1ec7524530f15ae1086b144192d93", 204 | auth_hmacsha512256_mult_tag: "bf280508996bba2bd590a2c1662d8c47fcceb8111bfcc4bdff5f2c28b0301449", 205 | # AEAD ChaCha20-Poly1305 original implementation test vectors 206 | # Taken from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 207 | aead_chacha20poly1305_orig_key: "4290bcb154173531f314af57f3be3b5006da371ece272afa1b5dbdd1100a1007", 208 | aead_chacha20poly1305_orig_message: "86d09974840bded2a5ca", 209 | aead_chacha20poly1305_orig_nonce: "cd7cf67be39c794a", 210 | aead_chacha20poly1305_orig_ad: "87e229d4500845a079c0", 211 | aead_chacha20poly1305_orig_ciphertext: "e3e446f7ede9a19b62a4677dabf4e3d24b876bb284753896e1d6", 212 | 213 | # AEAD ChaCha20-Poly1305 IETF test vectors 214 | # Taken from https://tools.ietf.org/html/rfc7539 215 | aead_chacha20poly1305_ietf_key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 216 | aead_chacha20poly1305_ietf_message: "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" \ 217 | "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" \ 218 | "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" \ 219 | "637265656e20776f756c642062652069742e", 220 | aead_chacha20poly1305_ietf_nonce: "070000004041424344454647", 221 | aead_chacha20poly1305_ietf_ad: "50515253c0c1c2c3c4c5c6c7", 222 | aead_chacha20poly1305_ietf_ciphertext: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" \ 223 | "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" \ 224 | "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" \ 225 | "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" \ 226 | "0691", 227 | 228 | # Jank AEAD XChaCha20-Poly1305 test vectors 229 | # Unfortunately, I couldn't find any public variants of these, so I used: 230 | # https://github.com/jedisct1/libsodium/blob/1.0.16/test/default/aead_xchacha20poly1305.c 231 | # Doubly unfortunately, that doesn't even have a ciphertext vector. I 232 | # generated one using crypto_aead_xchacha20poly1305_ietf_encrypt on 233 | # libsodium 1.0.16 234 | aead_xchacha20poly1305_ietf_key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 235 | aead_xchacha20poly1305_ietf_message: "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" \ 236 | "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" \ 237 | "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" \ 238 | "637265656e20776f756c642062652069742e", 239 | aead_xchacha20poly1305_ietf_nonce: "07000000404142434445464748494a4b0000000000000000", 240 | aead_xchacha20poly1305_ietf_ad: "50515253c0c1c2c3c4c5c6c7", 241 | aead_xchacha20poly1305_ietf_ciphertext: "453c0693a7407f04ff4c56aedb17a3c0a1afff01174930fc22287c33dbcf0ac8" \ 242 | "b89ad929530a1bb3ab5e69f24c7f6070c8f840c9abb4f69fbfc8a7ff5126faee" \ 243 | "bbb55805ee9c1cf2ce5a57263287aec5780f04ec324c3514122cfc3231fc1a8b" \ 244 | "718a62863730a2702bb76366116bed09e0fd5c6d84b6b0c1abaf249d5dd0f7f5" \ 245 | "a7ea" 246 | }.freeze 247 | end 248 | # rubocop:enable Metrics/ModuleLength 249 | -------------------------------------------------------------------------------- /lib/rbnacl/util.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | module RbNaCl 5 | # Various utility functions 6 | module Util 7 | extend Sodium 8 | 9 | sodium_function :c_verify16, :crypto_verify_16, %i[pointer pointer] 10 | sodium_function :c_verify32, :crypto_verify_32, %i[pointer pointer] 11 | sodium_function :c_verify64, :crypto_verify_64, %i[pointer pointer] 12 | 13 | module_function 14 | 15 | # Returns a string of n zeros 16 | # 17 | # Lots of the functions require us to create strings to pass into functions of a specified size. 18 | # 19 | # @param [Integer] n the size of the string to make 20 | # 21 | # @return [String] A nice collection of zeros 22 | def zeros(n = 32) 23 | zeros = "\0" * n 24 | # make sure they're 8-bit zeros, not 7-bit zeros. Otherwise we might get 25 | # encoding errors later 26 | zeros.respond_to?(:force_encoding) ? zeros.force_encoding("ASCII-8BIT") : zeros 27 | end 28 | 29 | # Prepends a message with zeros 30 | # 31 | # Many functions require a string with some zeros prepended. 32 | # 33 | # @param [Integer] n The number of zeros to prepend 34 | # @param [String] message The string to be prepended 35 | # 36 | # @return [String] a bunch of zeros 37 | def prepend_zeros(n, message) 38 | zeros(n) + message 39 | end 40 | 41 | # Remove zeros from the start of a message 42 | # 43 | # Many functions require a string with some zeros prepended, then need them removing after. 44 | # Note: this modifies the passed in string 45 | # 46 | # @param [Integer] n The number of zeros to remove 47 | # @param [String] message The string to be slice 48 | # 49 | # @return [String] less a bunch of zeros 50 | def remove_zeros(n, message) 51 | message.slice!(n, message.bytesize - n) 52 | end 53 | 54 | # Pad a string out to n characters with zeros 55 | # 56 | # @param [Integer] n The length of the resulting string 57 | # @param [String] message the message to be padded 58 | # 59 | # @raise [RbNaCl::LengthError] If the string is too long 60 | # 61 | # @return [String] A string, n bytes long 62 | def zero_pad(n, message) 63 | len = message.bytesize 64 | if len == n 65 | message 66 | elsif len > n 67 | raise LengthError, "String too long for zero-padding to #{n} bytes" 68 | else 69 | message + zeros(n - len) 70 | end 71 | end 72 | 73 | # Check the length of the passed in string 74 | # 75 | # In several places through the codebase we have to be VERY strict with 76 | # what length of string we accept. This method supports that. 77 | # 78 | # @raise [RbNaCl::LengthError] If the string is not the right length 79 | # 80 | # @param string [String] The string to compare 81 | # @param length [Integer] The desired length 82 | # @param description [String] Description of the string (used in the error) 83 | def check_length(string, length, description) 84 | if string.nil? 85 | # code below is runs only in test cases 86 | # nil can't be converted to str with #to_str method 87 | raise LengthError, 88 | "#{description} was nil (Expected #{length.to_int})", 89 | caller 90 | end 91 | 92 | if string.bytesize != length.to_int 93 | raise LengthError, 94 | "#{description} was #{string.bytesize} bytes (Expected #{length.to_int})", 95 | caller 96 | end 97 | true 98 | end 99 | 100 | # Check a passed in string, converting the argument if necessary 101 | # 102 | # In several places through the codebase we have to be VERY strict with 103 | # the strings we accept. This method supports that. 104 | # 105 | # @raise [ArgumentError] If we cannot convert to a string with #to_str 106 | # @raise [RbNaCl::LengthError] If the string is not the right length 107 | # 108 | # @param string [#to_str] The input string 109 | # @param length [Integer] The only acceptable length of the string 110 | # @param description [String] Description of the string (used in the error) 111 | def check_string(string, length, description) 112 | check_string_validation(string) 113 | string = string.to_s 114 | check_length(string, length, description) 115 | 116 | string 117 | end 118 | 119 | # Check a passed in string, convertion if necessary 120 | # 121 | # This method will check the key, and raise error 122 | # if argument is not a string, and if it's empty string. 123 | # 124 | # RFC 2104 HMAC 125 | # The key for HMAC can be of any length (keys longer than B bytes are 126 | # first hashed using H). However, less than L bytes is strongly 127 | # discouraged as it would decrease the security strength of the 128 | # function. Keys longer than L bytes are acceptable but the extra 129 | # length would not significantly increase the function strength. (A 130 | # longer key may be advisable if the randomness of the key is 131 | # considered weak.) 132 | # 133 | # see https://tools.ietf.org/html/rfc2104#section-3 134 | # 135 | # 136 | # @raise [ArgumentError] If we cannot convert to a string with #to_str 137 | # @raise [RbNaCl::LengthError] If the string is empty 138 | # 139 | # @param string [#to_str] The input string 140 | # @param description [String] Description of the string (used in the error) 141 | def check_hmac_key(string, description) 142 | check_string_validation(string) 143 | 144 | string = string.to_str 145 | 146 | if string.bytesize.zero? 147 | raise LengthError, 148 | "#{description} was #{string.bytesize} bytes (Expected more than 0)", 149 | caller 150 | end 151 | 152 | string 153 | end 154 | 155 | # Check a passed string is it valid 156 | # 157 | # Raise an error if passed argument is invalid 158 | # 159 | # @raise [TypeError] If string cannot convert to a string with #to_str 160 | # @raise [EncodingError] If string have wrong encoding 161 | # 162 | # @param string [#to_str] The input string 163 | def check_string_validation(string) 164 | raise TypeError, "can't convert #{string.class} into String with #to_str" unless string.respond_to? :to_str 165 | 166 | string = string.to_str 167 | 168 | raise EncodingError, "strings must use BINARY encoding (got #{string.encoding})" if string.encoding != Encoding::BINARY 169 | end 170 | 171 | # Compare two 64 byte strings in constant time 172 | # 173 | # This should help to avoid timing attacks for string comparisons in your 174 | # application. Note that many of the functions (such as HmacSha512#verify) 175 | # use this method under the hood already. 176 | # 177 | # @param [String] one String #1 178 | # @param [String] two String #2 179 | # 180 | # @return [Boolean] Well, are they equal? 181 | def verify64(one, two) 182 | return false unless two.bytesize == 64 && one.bytesize == 64 183 | 184 | c_verify64(one, two) 185 | end 186 | 187 | # Compare two 64 byte strings in constant time 188 | # 189 | # This should help to avoid timing attacks for string comparisons in your 190 | # application. Note that many of the functions (such as HmacSha512#verify) 191 | # use this method under the hood already. 192 | # 193 | # @param [String] one String #1 194 | # @param [String] two String #2 195 | # 196 | # @raise [ArgumentError] If the strings are not equal in length 197 | # 198 | # @return [Boolean] Well, are they equal? 199 | def verify64!(one, two) 200 | check_length(one, 64, "First message") 201 | check_length(two, 64, "Second message") 202 | c_verify64(one, two) 203 | end 204 | 205 | # Compare two 32 byte strings in constant time 206 | # 207 | # This should help to avoid timing attacks for string comparisons in your 208 | # application. Note that many of the functions (such as HmacSha256#verify) 209 | # use this method under the hood already. 210 | # 211 | # @param [String] one String #1 212 | # @param [String] two String #2 213 | # 214 | # @return [Boolean] Well, are they equal? 215 | def verify32(one, two) 216 | return false unless two.bytesize == 32 && one.bytesize == 32 217 | 218 | c_verify32(one, two) 219 | end 220 | 221 | # Compare two 32 byte strings in constant time 222 | # 223 | # This should help to avoid timing attacks for string comparisons in your 224 | # application. Note that many of the functions (such as HmacSha256#verify) 225 | # use this method under the hood already. 226 | # 227 | # @param [String] one String #1 228 | # @param [String] two String #2 229 | # 230 | # @raise [ArgumentError] If the strings are not equal in length 231 | # 232 | # @return [Boolean] Well, are they equal? 233 | def verify32!(one, two) 234 | check_length(one, 32, "First message") 235 | check_length(two, 32, "Second message") 236 | c_verify32(one, two) 237 | end 238 | 239 | # Compare two 16 byte strings in constant time 240 | # 241 | # This should help to avoid timing attacks for string comparisons in your 242 | # application. Note that many of the functions (such as OneTime#verify) 243 | # use this method under the hood already. 244 | # 245 | # @param [String] one String #1 246 | # @param [String] two String #2 247 | # 248 | # @return [Boolean] Well, are they equal? 249 | def verify16(one, two) 250 | return false unless two.bytesize == 16 && one.bytesize == 16 251 | 252 | c_verify16(one, two) 253 | end 254 | 255 | # Compare two 16 byte strings in constant time 256 | # 257 | # This should help to avoid timing attacks for string comparisons in your 258 | # application. Note that many of the functions (such as OneTime#verify) 259 | # use this method under the hood already. 260 | # 261 | # @param [String] one String #1 262 | # @param [String] two String #2 263 | # 264 | # @raise [ArgumentError] If the strings are not equal in length 265 | # 266 | # @return [Boolean] Well, are they equal? 267 | def verify16!(one, two) 268 | check_length(one, 16, "First message") 269 | check_length(two, 16, "Second message") 270 | c_verify16(one, two) 271 | end 272 | 273 | # Hex encodes a message 274 | # 275 | # @param [String] bytes The bytes to encode 276 | # 277 | # @return [String] Tasty, tasty hexadecimal 278 | def bin2hex(bytes) 279 | bytes.to_s.unpack1("H*") 280 | end 281 | 282 | # Hex decodes a message 283 | # 284 | # @param [String] hex hex to decode. 285 | # 286 | # @return [String] crisp and clean bytes 287 | def hex2bin(hex) 288 | [hex.to_s].pack("H*") 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /lib/rbnacl/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | # NaCl/libsodium for Ruby 5 | module RbNaCl 6 | # The library's version 7 | VERSION = "7.1.2" 8 | end 9 | -------------------------------------------------------------------------------- /rbnacl.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "rbnacl/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "rbnacl" 9 | spec.version = RbNaCl::VERSION 10 | spec.authors = ["Tony Arcieri", "Jonathan Stott"] 11 | spec.email = ["bascule@gmail.com", "jonathan.stott@gmail.com"] 12 | spec.homepage = "https://github.com/RubyCrypto/rbnacl" 13 | spec.licenses = ["MIT"] 14 | spec.summary = "Ruby binding to the libsodium/NaCl cryptography library" 15 | spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ") 16 | The Networking and Cryptography (NaCl) library provides a high-level toolkit for building 17 | cryptographic systems and protocols 18 | DESCRIPTION 19 | spec.metadata = { 20 | "bug_tracker_uri" => "#{spec.homepage}/issues", 21 | "changelog_uri" => "#{spec.homepage}/blob/master/CHANGES.md", 22 | "documentation_uri" => "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}", 23 | "source_code_uri" => "#{spec.homepage}/tree/v#{spec.version}", 24 | "wiki_uri" => "#{spec.homepage}/wiki" 25 | } 26 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject do |f| 27 | f.start_with?("images/", "spec/", "tasks/", "Rakefile") 28 | end 29 | spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) } 30 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 31 | spec.require_paths = ["lib"] 32 | spec.required_ruby_version = ">= 2.6.0" 33 | spec.add_runtime_dependency "ffi", "~> 1" 34 | spec.add_development_dependency "bundler", "~> 2" 35 | end 36 | -------------------------------------------------------------------------------- /spec/rbnacl/aead/chacha20poly1305_ietf_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::AEAD::ChaCha20Poly1305IETF do 5 | if RbNaCl::Sodium::Version.supported_version?("1.0.9") 6 | include_examples "aead" do 7 | let(:key) { vector :aead_chacha20poly1305_ietf_key } 8 | let(:message) { vector :aead_chacha20poly1305_ietf_message } 9 | let(:nonce) { vector :aead_chacha20poly1305_ietf_nonce } 10 | let(:ad) { vector :aead_chacha20poly1305_ietf_ad } 11 | let(:ciphertext) { vector :aead_chacha20poly1305_ietf_ciphertext } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rbnacl/aead/chacha20poly1305_legacy_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::AEAD::ChaCha20Poly1305Legacy do 5 | include_examples "aead" do 6 | let(:key) { vector :aead_chacha20poly1305_orig_key } 7 | let(:message) { vector :aead_chacha20poly1305_orig_message } 8 | let(:nonce) { vector :aead_chacha20poly1305_orig_nonce } 9 | let(:ad) { vector :aead_chacha20poly1305_orig_ad } 10 | let(:ciphertext) { vector :aead_chacha20poly1305_orig_ciphertext } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/rbnacl/aead/xchacha20poly1305_ietf_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::AEAD::XChaCha20Poly1305IETF do 5 | if RbNaCl::Sodium::Version.supported_version?("1.0.12") 6 | include_examples "aead" do 7 | let(:key) { vector :aead_xchacha20poly1305_ietf_key } 8 | let(:message) { vector :aead_xchacha20poly1305_ietf_message } 9 | let(:nonce) { vector :aead_xchacha20poly1305_ietf_nonce } 10 | let(:ad) { vector :aead_xchacha20poly1305_ietf_ad } 11 | let(:ciphertext) { vector :aead_xchacha20poly1305_ietf_ciphertext } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rbnacl/authenticators/poly1305_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::OneTimeAuth do 5 | let(:key) { vector "auth_key_#{described_class.key_bytes}".to_sym } 6 | let(:message) { vector :auth_message } 7 | let(:tag) { vector :auth_onetime } 8 | 9 | context ".new" do 10 | it "raises ArgumentError on a key which is too long" do 11 | expect { described_class.new("\0" * described_class.key_bytes.succ) }.to raise_error(ArgumentError) 12 | end 13 | end 14 | 15 | context ".auth" do 16 | it "raises ArgumentError on a key which is too long" do 17 | expect { described_class.auth("\0" * described_class.key_bytes.succ, message) }.to raise_error(ArgumentError) 18 | end 19 | end 20 | 21 | context ".verify" do 22 | it "raises ArgumentError on a key which is too long" do 23 | expect { described_class.verify("\0" * described_class.key_bytes.succ, tag, message) }.to raise_error(ArgumentError) 24 | end 25 | end 26 | 27 | include_examples "authenticator" 28 | end 29 | -------------------------------------------------------------------------------- /spec/rbnacl/boxes/curve25519xsalsa20poly1305/private_key_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::PrivateKey do 5 | let(:bobsk) { vector :bob_private } 6 | let(:bobpk) { vector :bob_public } 7 | 8 | subject { RbNaCl::PrivateKey.new(bobsk) } 9 | 10 | context "generate" do 11 | let(:secret_key) { RbNaCl::PrivateKey.generate } 12 | 13 | it "returns a secret key" do 14 | expect(secret_key).to be_a RbNaCl::PrivateKey 15 | end 16 | 17 | it "has the public key also set" do 18 | expect(secret_key.public_key).to be_a RbNaCl::PublicKey 19 | end 20 | end 21 | 22 | context "new" do 23 | it "accepts a valid key" do 24 | expect { RbNaCl::PrivateKey.new(bobsk) }.not_to raise_error 25 | end 26 | 27 | it "raises TypeError when given a nil key" do 28 | expect { RbNaCl::PrivateKey.new(nil) }.to raise_error(TypeError) 29 | end 30 | 31 | it "raises ArgumentError when given a short key" do 32 | expect { RbNaCl::PrivateKey.new("short") }.to raise_error(ArgumentError) 33 | end 34 | end 35 | 36 | context "public_key" do 37 | it "returns a public key" do 38 | expect(subject.public_key).to be_a RbNaCl::PublicKey 39 | end 40 | 41 | it "returns the correct public key" do 42 | expect(subject.public_key.to_s).to eql bobpk 43 | end 44 | end 45 | 46 | context "#to_bytes" do 47 | it "returns the bytes of the key" do 48 | expect(subject.to_bytes).to eq bobsk 49 | end 50 | end 51 | 52 | context "#to_s" do 53 | it "returns the raw bytes of the key" do 54 | expect(subject.to_s).to eq bobsk 55 | end 56 | end 57 | 58 | include_examples "key equality" do 59 | let(:key) { subject } 60 | let(:key_bytes) { subject.to_bytes } 61 | let(:other_key) { described_class.new(bobpk) } 62 | end 63 | 64 | include_examples "serializable" 65 | end 66 | -------------------------------------------------------------------------------- /spec/rbnacl/boxes/curve25519xsalsa20poly1305/public_key_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::PublicKey do 5 | let(:alicepk) { vector :alice_public } 6 | 7 | subject { RbNaCl::PublicKey.new(alicepk) } 8 | 9 | context "new" do 10 | it "accepts a valid key" do 11 | expect { RbNaCl::PublicKey.new(alicepk) }.not_to raise_error 12 | end 13 | 14 | it "rejects a nil key" do 15 | expect { RbNaCl::PublicKey.new(nil) }.to raise_error(TypeError) 16 | end 17 | 18 | it "rejects a short key" do 19 | expect { RbNaCl::PublicKey.new("short") }.to raise_error(ArgumentError) 20 | end 21 | end 22 | 23 | context "#to_bytes" do 24 | it "returns the bytes of the key" do 25 | expect(subject.to_bytes).to eq alicepk 26 | end 27 | end 28 | 29 | context "#to_s" do 30 | it "returns the bytes of the key" do 31 | expect(subject.to_s).to eq alicepk 32 | end 33 | end 34 | 35 | include_examples "key equality" do 36 | let(:key) { subject } 37 | let(:key_bytes) { subject.to_bytes } 38 | let(:other_key) { described_class.new(alicepk.succ) } 39 | end 40 | 41 | include_examples "serializable" 42 | end 43 | -------------------------------------------------------------------------------- /spec/rbnacl/boxes/curve25519xsalsa20poly1305_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::Box do 5 | let(:alicepk) { vector :alice_public } 6 | let(:bobsk) { vector :bob_private } 7 | let(:alice_key) { RbNaCl::PublicKey.new(alicepk) } 8 | let(:bob_key) { RbNaCl::PrivateKey.new(bobsk) } 9 | 10 | context "new" do 11 | it "accepts strings" do 12 | expect do 13 | RbNaCl::Box.new(alicepk, bobsk) 14 | end.to_not raise_error 15 | end 16 | 17 | it "accepts KeyPairs" do 18 | expect do 19 | RbNaCl::Box.new(alice_key, bob_key) 20 | end.to_not raise_error 21 | end 22 | 23 | it "raises TypeError on a nil public key" do 24 | expect do 25 | RbNaCl::Box.new(nil, bobsk) 26 | end.to raise_error(TypeError) 27 | end 28 | 29 | it "raises RbNaCl::LengthError on an invalid public key" do 30 | expect do 31 | RbNaCl::Box.new("hello", bobsk) 32 | end.to raise_error(RbNaCl::LengthError, /Public key was 5 bytes \(Expected 32\)/) 33 | end 34 | 35 | it "raises TypeError on a nil secret key" do 36 | expect do 37 | RbNaCl::Box.new(alicepk, nil) 38 | end.to raise_error(TypeError) 39 | end 40 | 41 | it "raises RbNaCl::LengthError on an invalid secret key" do 42 | expect do 43 | RbNaCl::Box.new(alicepk, "hello") 44 | end.to raise_error(RbNaCl::LengthError, /Private key was 5 bytes \(Expected 32\)/) 45 | end 46 | end 47 | 48 | include_examples "box" do 49 | let(:box) { RbNaCl::Box.new(alicepk, bobsk) } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/rbnacl/boxes/sealed_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::SealedBox do 5 | let(:alicepk) { vector :alice_public } 6 | let(:alicesk) { vector :alice_private } 7 | let(:alice_pubkey) { RbNaCl::PublicKey.new(alicepk) } 8 | let(:alice_privkey) { RbNaCl::PrivateKey.new(alicesk) } 9 | 10 | context "new" do 11 | it "accepts public key strings" do 12 | expect do 13 | RbNaCl::SealedBox.from_public_key(alicepk) 14 | end.to_not raise_error 15 | end 16 | 17 | it "accepts public KeyPairs" do 18 | expect do 19 | RbNaCl::SealedBox.from_public_key(alice_pubkey) 20 | end.to_not raise_error 21 | end 22 | 23 | it "accepts private key strings" do 24 | expect do 25 | RbNaCl::SealedBox.from_private_key(alicepk) 26 | end.to_not raise_error 27 | end 28 | 29 | it "accepts private KeyPairs" do 30 | expect do 31 | RbNaCl::SealedBox.from_private_key(alice_privkey) 32 | end.to_not raise_error 33 | end 34 | 35 | it "raises TypeError on a nil public key" do 36 | expect do 37 | RbNaCl::SealedBox.from_public_key(nil) 38 | end.to raise_error(TypeError) 39 | end 40 | 41 | it "raises RbNaCl::LengthError on an invalid public key" do 42 | expect do 43 | RbNaCl::SealedBox.from_public_key("hello") 44 | end.to raise_error(RbNaCl::LengthError, /Public key was 5 bytes \(Expected 32\)/) 45 | end 46 | 47 | it "raises TypeError on a nil private key" do 48 | expect do 49 | RbNaCl::SealedBox.from_private_key(nil) 50 | end.to raise_error(TypeError) 51 | end 52 | 53 | it "raises RbNaCl::LengthError on an invalid private key" do 54 | expect do 55 | RbNaCl::SealedBox.from_private_key("hello") 56 | end.to raise_error(RbNaCl::LengthError, /Private key was 5 bytes \(Expected 32\)/) 57 | end 58 | end 59 | 60 | include_examples "sealed_box" do 61 | let(:box) { RbNaCl::SealedBox.new(alicepk, alicesk) } 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/rbnacl/group_element_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::GroupElement do 5 | let(:alice_private) { vector :alice_private } 6 | let(:alice_public) { vector :alice_public } 7 | 8 | let(:bob_public) { vector :bob_public } 9 | 10 | let(:alice_mult_bob) { vector :alice_mult_bob } 11 | 12 | let(:degenerate_key) { RbNaCl::GroupElements::Curve25519::DEGENERATE_KEY } 13 | 14 | subject { described_class.new(bob_public) } 15 | 16 | it "multiplies integers with the base point" do 17 | expect(described_class.base.mult(alice_private).to_s).to eq alice_public 18 | end 19 | 20 | it "multiplies integers with arbitrary points" do 21 | expect(described_class.new(bob_public).mult(alice_private).to_s).to eq alice_mult_bob 22 | end 23 | 24 | it "serializes to bytes" do 25 | expect(subject.to_bytes).to eq bob_public 26 | end 27 | 28 | it "detects degenerate keys" do 29 | expect { described_class.new(degenerate_key).mult(alice_private) }.to raise_error RbNaCl::CryptoError 30 | end 31 | 32 | include_examples "serializable" 33 | end 34 | -------------------------------------------------------------------------------- /spec/rbnacl/hash/blake2b_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::Hash::Blake2b do 5 | let(:reference_string) { vector :blake2b_message } 6 | let(:reference_string_hash) { vector :blake2b_digest } 7 | let(:empty_string_hash) { vector :blake2b_empty } 8 | 9 | it "calculates the correct hash for a reference string" do 10 | expect(RbNaCl::Hash.blake2b(reference_string)).to eq reference_string_hash 11 | end 12 | 13 | it "calculates the correct hash for an empty string" do 14 | expect(RbNaCl::Hash.blake2b("")).to eq empty_string_hash 15 | end 16 | 17 | context "arbitrary length message API" do 18 | let(:blake2b) { RbNaCl::Hash::Blake2b.new } 19 | 20 | it "calculates the correct hash for a reference string" do 21 | blake2b << reference_string 22 | expect(blake2b.digest).to eq reference_string_hash 23 | end 24 | 25 | it "calculates the correct hash for an empty string" do 26 | blake2b << "" 27 | expect(blake2b.digest).to eq empty_string_hash 28 | end 29 | 30 | it "raise CryptoError when digest called without reset / message" do 31 | expect { blake2b.digest }.to raise_error(RbNaCl::CryptoError) 32 | end 33 | 34 | it "calculates hash for empty string when digest called directly after reset" do 35 | blake2b.reset 36 | expect(blake2b.digest).to eq empty_string_hash 37 | end 38 | end 39 | 40 | context "keyed" do 41 | let(:reference_string) { vector :blake2b_keyed_message } 42 | let(:reference_key) { vector :blake2b_key } 43 | let(:reference_string_hash) { vector :blake2b_keyed_digest } 44 | 45 | it "calculates keyed hashes correctly" do 46 | expect(RbNaCl::Hash.blake2b(reference_string, key: reference_key)).to eq reference_string_hash 47 | end 48 | 49 | it "doesn't accept empty strings as a key" do 50 | expect { RbNaCl::Hash.blake2b(reference_string, key: "") }.to raise_error(RbNaCl::LengthError) 51 | end 52 | 53 | context "arbitrary length message API" do 54 | let(:blake2b) { RbNaCl::Hash::Blake2b.new(key: "") } 55 | let(:blake2b_wk) { RbNaCl::Hash::Blake2b.new(key: reference_key) } 56 | 57 | it "calculates keyed hashes correctly" do 58 | blake2b_wk << reference_string 59 | expect(blake2b_wk.digest).to eq reference_string_hash 60 | end 61 | 62 | it "doesn't accept empty strings as a key" do 63 | expect do 64 | blake2b << reference_string 65 | blake2b.digest 66 | end.to raise_error(RbNaCl::LengthError) 67 | end 68 | end 69 | end 70 | 71 | context "personalized" do 72 | let(:reference_string) { vector :blake2b_message } 73 | let(:reference_personal) { vector :blake2b_personal } 74 | let(:reference_personal_hash) { vector :blake2b_personal_digest } 75 | let(:reference_personal_short) { vector :blake2b_personal_short } 76 | let(:reference_personal_short_hash) { vector :blake2b_personal_short_digest } 77 | 78 | it "calculates personalised hashes correctly" do 79 | expect(RbNaCl::Hash.blake2b(reference_string, personal: reference_personal)).to eq reference_personal_hash 80 | end 81 | 82 | it "calculates personalised hashes correctly with a short personal" do 83 | expect(RbNaCl::Hash.blake2b(reference_string, personal: reference_personal_short)).to eq reference_personal_short_hash 84 | end 85 | 86 | context "arbitrary length message API" do 87 | let(:blake2b) { RbNaCl::Hash::Blake2b.new(personal: reference_personal) } 88 | let(:blake2b_sh) { RbNaCl::Hash::Blake2b.new(personal: reference_personal_short) } 89 | 90 | it "calculates personalised hashes correctly" do 91 | blake2b << reference_string 92 | expect(blake2b.digest).to eq reference_personal_hash 93 | end 94 | 95 | it "calculates personalised hashes correctly with a short personal" do 96 | blake2b_sh << reference_string 97 | expect(blake2b_sh.digest).to eq reference_personal_short_hash 98 | end 99 | end 100 | end 101 | 102 | context "salted" do 103 | let(:reference_string) { vector :blake2b_message } 104 | let(:reference_salt) { vector :blake2b_salt } 105 | let(:reference_salt_hash) { vector :blake2b_salt_digest } 106 | let(:reference_salt_short) { vector :blake2b_salt_short } 107 | let(:reference_salt_short_hash) { vector :blake2b_salt_short_digest } 108 | 109 | it "calculates saltised hashes correctly" do 110 | expect(RbNaCl::Hash.blake2b(reference_string, salt: reference_salt)).to eq reference_salt_hash 111 | end 112 | 113 | it "calculates saltised hashes correctly with a short salt" do 114 | expect(RbNaCl::Hash.blake2b(reference_string, salt: reference_salt_short)).to eq reference_salt_short_hash 115 | end 116 | 117 | context "arbitrary length message API" do 118 | let(:blake2b) { RbNaCl::Hash::Blake2b.new(salt: reference_salt) } 119 | let(:blake2b_sh) { RbNaCl::Hash::Blake2b.new(salt: reference_salt_short) } 120 | 121 | it "calculates saltised hashes correctly" do 122 | blake2b << reference_string 123 | expect(blake2b.digest).to eq reference_salt_hash 124 | end 125 | 126 | it "calculates saltised hashes correctly with a short salt" do 127 | blake2b_sh << reference_string 128 | expect(blake2b_sh.digest).to eq reference_salt_short_hash 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/rbnacl/hash_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::Hash do 5 | context "sha256" do 6 | let(:reference_string) { vector :sha256_message } 7 | let(:reference_string_hash) { vector :sha256_digest } 8 | let(:empty_string_hash) { vector :sha256_empty } 9 | 10 | it "calculates the correct hash for a reference string" do 11 | expect(RbNaCl::Hash.sha256(reference_string)).to eq reference_string_hash 12 | end 13 | 14 | it "calculates the correct hash for an empty string" do 15 | expect(RbNaCl::Hash.sha256("")).to eq empty_string_hash 16 | end 17 | 18 | it "doesn't raise on a null byte" do 19 | expect { RbNaCl::Hash.sha256("\0") }.to_not raise_error 20 | end 21 | end 22 | 23 | context "sha512" do 24 | let(:reference_string) { vector :sha512_message } 25 | let(:reference_string_hash) { vector :sha512_digest } 26 | let(:empty_string_hash) { vector :sha512_empty } 27 | 28 | it "calculates the correct hash for a reference string" do 29 | expect(RbNaCl::Hash.sha512(reference_string)).to eq reference_string_hash 30 | end 31 | 32 | it "calculates the correct hash for an empty string" do 33 | expect(RbNaCl::Hash.sha512("")).to eq empty_string_hash 34 | end 35 | 36 | it "doesn't raise on a null byte" do 37 | expect { RbNaCl::Hash.sha512("\0") }.to_not raise_error 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rbnacl/hmac/sha256_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::HMAC::SHA256 do 5 | let(:key) { vector :auth_hmac_key } 6 | let(:message) { vector :auth_hmac_data } 7 | let(:tag) { vector :auth_hmacsha256_tag } 8 | let(:mult_tag) { vector :auth_hmacsha256_mult_tag } 9 | let(:wrong_key) { "key".encode("utf-8") } 10 | 11 | include_examples "HMAC" 12 | include_examples "authenticator" 13 | end 14 | -------------------------------------------------------------------------------- /spec/rbnacl/hmac/sha512256_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::HMAC::SHA512256 do 5 | let(:key) { vector :auth_hmac_key } 6 | let(:message) { vector :auth_message } 7 | let(:tag) { vector :auth_hmacsha512256_tag } 8 | let(:mult_tag) { vector :auth_hmacsha512256_mult_tag } 9 | let(:wrong_key) { "key".encode("utf-8") } 10 | 11 | include_examples "HMAC" 12 | include_examples "authenticator" 13 | end 14 | -------------------------------------------------------------------------------- /spec/rbnacl/hmac/sha512_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::HMAC::SHA512 do 5 | let(:key) { vector :auth_hmac_key } 6 | let(:message) { vector :auth_hmac_data } 7 | let(:tag) { vector :auth_hmacsha512_tag } 8 | let(:mult_tag) { vector :auth_hmacsha512_mult_tag } 9 | let(:wrong_key) { "key".encode("utf-8") } 10 | 11 | include_examples "HMAC" 12 | include_examples "authenticator" 13 | end 14 | -------------------------------------------------------------------------------- /spec/rbnacl/password_hash/argon2_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | if RbNaCl::Sodium::Version::ARGON2_SUPPORTED 5 | RSpec.describe RbNaCl::PasswordHash::Argon2 do 6 | let(:argon2i_password) { vector :argon2i_password } 7 | let(:argon2i_salt) { vector :argon2i_salt } 8 | let(:argon2i_opslimit) { RbNaCl::TEST_VECTORS[:argon2i_opslimit] } 9 | let(:argon2i_memlimit) { RbNaCl::TEST_VECTORS[:argon2i_memlimit] } 10 | let(:argon2i_digest) { vector :argon2i_digest } 11 | let(:argon2i_outlen) { RbNaCl::TEST_VECTORS[:argon2i_outlen] } 12 | 13 | let(:argon2id_password) { vector :argon2id_password } 14 | let(:argon2id_salt) { vector :argon2id_salt } 15 | let(:argon2id_opslimit) { RbNaCl::TEST_VECTORS[:argon2id_opslimit] } 16 | let(:argon2id_memlimit) { RbNaCl::TEST_VECTORS[:argon2id_memlimit] } 17 | let(:argon2id_digest) { vector :argon2id_digest } 18 | let(:argon2id_outlen) { RbNaCl::TEST_VECTORS[:argon2id_outlen] } 19 | 20 | let(:str_ref_password) { RbNaCl::TEST_VECTORS[:argon2_str_passwd] } 21 | let(:str_ref_digest) { RbNaCl::TEST_VECTORS[:argon2_str_digest] } 22 | 23 | it "calculates the correct argon2i digest for a reference password/salt" do 24 | digest = RbNaCl::PasswordHash.argon2i( 25 | argon2i_password, 26 | argon2i_salt, 27 | argon2i_opslimit, 28 | argon2i_memlimit, 29 | argon2i_outlen 30 | ) 31 | expect(digest).to eq argon2i_digest 32 | end 33 | 34 | if RbNaCl::Sodium::Version::ARGON2_SUPPORTED 35 | it "calculates the correct argon2id digest for a reference password/salt" do 36 | digest = RbNaCl::PasswordHash.argon2id( 37 | argon2id_password, 38 | argon2id_salt, 39 | argon2id_opslimit, 40 | argon2id_memlimit, 41 | argon2id_outlen 42 | ) 43 | expect(digest).to eq argon2id_digest 44 | end 45 | 46 | it "calculates the correct argon2 default digest" do 47 | if RbNaCl::Sodium::Version.supported_version?("1.0.15") 48 | digest = RbNaCl::PasswordHash.argon2( 49 | argon2id_password, 50 | argon2id_salt, 51 | argon2id_opslimit, 52 | argon2id_memlimit, 53 | argon2id_outlen 54 | ) 55 | expect(digest).to eq argon2id_digest 56 | else 57 | digest = RbNaCl::PasswordHash.argon2( 58 | argon2i_password, 59 | argon2i_salt, 60 | argon2i_opslimit, 61 | argon2i_memlimit, 62 | argon2i_outlen 63 | ) 64 | expect(digest).to eq argon2i_digest 65 | end 66 | end 67 | 68 | end 69 | 70 | it "verifies password" do 71 | valid = RbNaCl::PasswordHash.argon2_valid?(str_ref_password, str_ref_digest) 72 | expect(valid).to eq true 73 | end 74 | 75 | it "creates digest string" do 76 | digest = RbNaCl::PasswordHash.argon2_str(str_ref_password) 77 | valid = RbNaCl::PasswordHash.argon2_valid?(str_ref_password, digest) 78 | expect(valid).to eq true 79 | end 80 | 81 | it "fails on invalid passwords" do 82 | valid = RbNaCl::PasswordHash.argon2_valid?("wrongpassword", str_ref_digest) 83 | expect(valid).to eq false 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/rbnacl/password_hash/scrypt_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::PasswordHash::SCrypt do 5 | let(:reference_password) { vector :scrypt_password } 6 | let(:reference_salt) { vector :scrypt_salt } 7 | let(:reference_opslimit) { RbNaCl::TEST_VECTORS[:scrypt_opslimit] } 8 | let(:reference_memlimit) { RbNaCl::TEST_VECTORS[:scrypt_memlimit] } 9 | let(:reference_digest) { vector :scrypt_digest } 10 | 11 | it "calculates the correct digest for a reference password/salt" do 12 | digest = RbNaCl::PasswordHash.scrypt( 13 | reference_password, 14 | reference_salt, 15 | reference_opslimit, 16 | reference_memlimit 17 | ) 18 | 19 | expect(digest).to eq reference_digest 20 | end 21 | 22 | it "calculates the correct digest using libsodium primitives" do 23 | digest = RbNaCl::PasswordHash.scrypt( 24 | reference_password, 25 | reference_salt 26 | ) 27 | 28 | expect(digest).to eq reference_digest 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/rbnacl/random_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::Random do 5 | it "produces random bytes" do 6 | expect(RbNaCl::Random.random_bytes(16).bytesize).to eq(16) 7 | end 8 | it "produces different random bytes" do 9 | expect(RbNaCl::Random.random_bytes(16)).not_to eq(RbNaCl::Random.random_bytes(16)) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/rbnacl/secret_box_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::SecretBox do 5 | let(:key) { vector :secret_key } 6 | 7 | context "new" do 8 | it "accepts strings" do 9 | expect { RbNaCl::SecretBox.new(key) }.to_not raise_error 10 | end 11 | 12 | it "raises on a nil key" do 13 | expect { RbNaCl::SecretBox.new(nil) }.to raise_error(TypeError) 14 | end 15 | 16 | it "raises on a short key" do 17 | expect { RbNaCl::SecretBox.new("hello") }.to raise_error RbNaCl::LengthError 18 | end 19 | end 20 | 21 | include_examples "box" do 22 | let(:box) { RbNaCl::SecretBox.new(key) } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/rbnacl/signatures/ed25519/signing_key_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::SigningKey do 5 | let(:signing_key) { vector :sign_private } 6 | let(:signing_keypair) { vector :sign_keypair } 7 | let(:message) { vector :sign_message } 8 | let(:signature) { vector :sign_signature } 9 | let(:curve25519_private_key) { vector :sign_curve25519_private } 10 | 11 | subject { described_class.new(signing_key) } 12 | 13 | it "generates keys" do 14 | expect(described_class.generate).to be_a described_class 15 | end 16 | 17 | it "signs messages as bytes" do 18 | expect(subject.sign(message)).to eq signature 19 | end 20 | 21 | it "signs messages, full version" do 22 | expect(subject.sign_attached(message)[0, RbNaCl::SigningKey.signature_bytes]).to eq signature 23 | expect(subject.sign_attached(message)[RbNaCl::SigningKey.signature_bytes, message.length]).to eq message 24 | end 25 | 26 | it "serializes to bytes" do 27 | expect(subject.to_bytes).to eq signing_key 28 | end 29 | 30 | it "serializes the internal signing key to bytes" do 31 | expect(subject.keypair_bytes.length).to eq 64 32 | expect(subject.keypair_bytes).to eq signing_keypair 33 | end 34 | 35 | it "can be converted to curve25519 private key" do 36 | sk = subject.to_curve25519_private_key 37 | expect(sk).to be_a_kind_of RbNaCl::PrivateKey 38 | expect(sk.to_s).to eq curve25519_private_key 39 | end 40 | 41 | include_examples "key equality" do 42 | let(:key_bytes) { signing_key } 43 | let(:key) { described_class.new(key_bytes) } 44 | let(:other_key) { described_class.new("B" * 32) } 45 | end 46 | 47 | include_examples "serializable" 48 | end 49 | -------------------------------------------------------------------------------- /spec/rbnacl/signatures/ed25519/verify_key_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::VerifyKey do 5 | let(:signing_key) { vector :sign_private } 6 | let(:verify_key) { vector :sign_public } 7 | 8 | let(:message) { vector :sign_message } 9 | let(:signature) { vector :sign_signature } 10 | let(:curve25519_public_key) { vector :sign_curve25519_public } 11 | 12 | let(:bad_signature) do 13 | sig = signature.dup 14 | sig[0] = (sig[0].ord + 1).chr 15 | sig 16 | end 17 | 18 | subject { RbNaCl::SigningKey.new(signing_key).verify_key } 19 | 20 | it "verifies correct signatures" do 21 | expect(subject.verify(signature, message)).to eq true 22 | end 23 | 24 | it "verifies correct signatures, full version" do 25 | expect(subject.verify_attached(signature + message)).to eq true 26 | end 27 | 28 | it "raises when asked to verify a bad signature" do 29 | expect { subject.verify(bad_signature, message) }.to raise_exception RbNaCl::BadSignatureError 30 | end 31 | 32 | it "raises when asked to verify a bad signature, full version" do 33 | expect { subject.verify_attached(bad_signature + message) }.to raise_exception RbNaCl::BadSignatureError 34 | end 35 | 36 | it "raises when asked to verify a short signature" do 37 | expect { subject.verify(bad_signature[0, 63], message) }.to raise_exception RbNaCl::LengthError 38 | end 39 | 40 | it "raises when asked to verify a nil signed message" do 41 | expect { subject.verify_attached(nil) }.to raise_exception RbNaCl::LengthError 42 | end 43 | 44 | it "raises when asked to verify too short signed message" do 45 | expect { subject.verify_attached(signature) }.to raise_exception RbNaCl::LengthError 46 | end 47 | 48 | it "serializes to bytes" do 49 | expect(subject.to_bytes).to eq verify_key 50 | end 51 | 52 | it "initializes from bytes" do 53 | expect(described_class.new(verify_key).to_s).to eq verify_key 54 | end 55 | 56 | it "can be converted to curve25519 public key" do 57 | pk = subject.to_curve25519_public_key 58 | expect(pk).to be_a_kind_of RbNaCl::PublicKey 59 | expect(pk.to_s).to eq curve25519_public_key 60 | end 61 | 62 | include_examples "key equality" do 63 | let(:key_bytes) { verify_key } 64 | let(:key) { described_class.new(verify_key) } 65 | let(:other_key) { described_class.new("B" * 32) } 66 | end 67 | 68 | include_examples "serializable" 69 | end 70 | -------------------------------------------------------------------------------- /spec/rbnacl/simple_box_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::SimpleBox do 5 | let(:secret_key) { vector :secret_key } 6 | let(:secret_box) { RbNaCl::SecretBox.new(secret_key) } 7 | let(:alicepk) { vector :alice_public } 8 | let(:alicesk) { vector :alice_private } 9 | let(:bobpk) { vector :bob_public } 10 | let(:bobsk) { vector :bob_private } 11 | 12 | context "instantiation" do 13 | it "can be instantiated from an already existing box" do 14 | expect { described_class.new(secret_box) }.not_to raise_error 15 | end 16 | 17 | it "can be instantiated from a secret key" do 18 | expect(described_class.from_secret_key(secret_key)).to be_a described_class 19 | end 20 | 21 | it "raises TypeError when given a nil secret key" do 22 | expect { described_class.from_secret_key(nil) }.to raise_error(TypeError) 23 | end 24 | 25 | it "can be instantiated from a key-pair" do 26 | expect(described_class.from_keypair(alicepk, bobsk)).to be_a described_class 27 | end 28 | 29 | it "raises TypeError when given nil secret keys in the pair" do 30 | expect { described_class.from_keypair(nil, bobsk) }.to raise_error(TypeError) 31 | expect { described_class.from_keypair(alicepk, nil) }.to raise_error(TypeError) 32 | end 33 | end 34 | 35 | context "cryptography" do 36 | let(:nonce) { vector :box_nonce } 37 | let(:message) { vector :box_message } 38 | let(:ciphertext) { vector :box_ciphertext } 39 | let(:alice) { described_class.from_keypair(bobpk, alicesk) } 40 | let(:bob) { described_class.from_keypair(alicepk, bobsk) } 41 | 42 | describe "bob" do 43 | it "decrypts a message from alice" do 44 | alices_ciphertext = alice.encrypt(message) 45 | expect(bob.decrypt(alices_ciphertext)).to eql message 46 | end 47 | 48 | it "decrypts own message" do 49 | bobs_ciphertext = bob.encrypt(message) 50 | expect(bob.decrypt(bobs_ciphertext)).to eql message 51 | end 52 | 53 | it "decrypts a message with a 'random' nonce" do 54 | expect(bob.decrypt(nonce + ciphertext)).to eql message 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/rbnacl/sodium_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RbNaCl::Sodium do 4 | subject(:sodium_class) do 5 | class SodiumExtendedClass 6 | extend RbNaCl::Sodium 7 | 8 | sodium_type :auth 9 | sodium_primitive :hmacsha512 10 | # sodium_constant :BYTES 11 | # sodium_constant :KEYBYTES 12 | end 13 | SodiumExtendedClass 14 | end 15 | 16 | context ".sodium_constant" do 17 | it "retrieves the libsodium constant" do 18 | sodium_class.sodium_constant :BYTES 19 | expect(sodium_class::BYTES).to eq(64) 20 | end 21 | 22 | context "with alternate constant name" do 23 | it "sets the alternate constant name" do 24 | sodium_class.sodium_constant :BYTES, name: :COOL_BYTES 25 | expect(sodium_class::COOL_BYTES).to eq(64) 26 | end 27 | end 28 | 29 | context "when libsodium does not define the constant" do 30 | it "raises an exception" do 31 | expect do 32 | sodium_class.sodium_constant :MIN_DANCING_PARTNERS 33 | end.to raise_error(FFI::NotFoundError) 34 | end 35 | end 36 | 37 | context "with fallback" do 38 | context "when libsodium defines the constant" do 39 | it "return the libsodium value" do 40 | sodium_class.sodium_constant :BYTES, fallback: 888 41 | expect(sodium_class::BYTES).to eq(64) 42 | end 43 | end 44 | 45 | context "when libsodium does not define the constant" do 46 | it "uses the fallback" do 47 | sodium_class.sodium_constant :MAX_PANDAS, fallback: 24 48 | expect(sodium_class::MAX_PANDAS).to eq(24) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/rbnacl/util_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.describe RbNaCl::Util do 5 | context ".verify64" do 6 | let(:msg) { RbNaCl::Util.zeros(64) } 7 | let(:identical_msg) { RbNaCl::Util.zeros(64) } 8 | let(:other_msg) { RbNaCl::Util.zeros(63) + "\001" } 9 | let(:short_msg) { RbNaCl::Util.zeros(63) } 10 | let(:long_msg) { RbNaCl::Util.zeros(65) } 11 | 12 | it "confirms identical messages are identical" do 13 | expect(RbNaCl::Util.verify64(msg, identical_msg)).to be true 14 | end 15 | 16 | it "confirms non-identical messages are non-identical" do 17 | expect(RbNaCl::Util.verify64(msg, other_msg)).to be false 18 | expect(RbNaCl::Util.verify64(other_msg, msg)).to be false 19 | expect(RbNaCl::Util.verify64(short_msg, msg)).to be false 20 | expect(RbNaCl::Util.verify64(msg, short_msg)).to be false 21 | expect(RbNaCl::Util.verify64(long_msg, msg)).to be false 22 | expect(RbNaCl::Util.verify64(msg, long_msg)).to be false 23 | end 24 | end 25 | 26 | context ".verify64!" do 27 | let(:msg) { RbNaCl::Util.zeros(64) } 28 | let(:identical_msg) { RbNaCl::Util.zeros(64) } 29 | let(:other_msg) { RbNaCl::Util.zeros(63) + "\001" } 30 | let(:short_msg) { RbNaCl::Util.zeros(63) } 31 | let(:long_msg) { RbNaCl::Util.zeros(65) } 32 | 33 | it "confirms identical messages are identical" do 34 | expect(RbNaCl::Util.verify64!(msg, identical_msg)).to be true 35 | end 36 | 37 | it "confirms non-identical messages are non-identical" do 38 | expect(RbNaCl::Util.verify64!(msg, other_msg)).to be false 39 | expect(RbNaCl::Util.verify64!(other_msg, msg)).to be false 40 | end 41 | 42 | it "raises descriptively on a short message in position 1" do 43 | expect { RbNaCl::Util.verify64!(short_msg, msg) }.to raise_error(RbNaCl::LengthError) 44 | end 45 | it "raises descriptively on a short message in position 2" do 46 | expect { RbNaCl::Util.verify64!(msg, short_msg) }.to raise_error(RbNaCl::LengthError) 47 | end 48 | it "raises descriptively on a long message in position 1" do 49 | expect { RbNaCl::Util.verify64!(long_msg, msg) }.to raise_error(RbNaCl::LengthError) 50 | end 51 | it "raises descriptively on a long message in position 2" do 52 | expect { RbNaCl::Util.verify64!(msg, long_msg) }.to raise_error(RbNaCl::LengthError) 53 | end 54 | end 55 | 56 | context ".verify32!" do 57 | let(:msg) { RbNaCl::Util.zeros(32) } 58 | let(:identical_msg) { RbNaCl::Util.zeros(32) } 59 | let(:other_msg) { RbNaCl::Util.zeros(31) + "\001" } 60 | let(:short_msg) { RbNaCl::Util.zeros(31) } 61 | let(:long_msg) { RbNaCl::Util.zeros(33) } 62 | 63 | it "confirms identical messages are identical" do 64 | expect(RbNaCl::Util.verify32!(msg, identical_msg)).to be true 65 | end 66 | 67 | it "confirms non-identical messages are non-identical" do 68 | expect(RbNaCl::Util.verify32!(msg, other_msg)).to be false 69 | expect(RbNaCl::Util.verify32!(other_msg, msg)).to be false 70 | end 71 | 72 | it "raises descriptively on a short message in position 1" do 73 | expect { RbNaCl::Util.verify32!(short_msg, msg) }.to raise_error(RbNaCl::LengthError) 74 | end 75 | it "raises descriptively on a short message in position 2" do 76 | expect { RbNaCl::Util.verify32!(msg, short_msg) }.to raise_error(RbNaCl::LengthError) 77 | end 78 | it "raises descriptively on a long message in position 1" do 79 | expect { RbNaCl::Util.verify32!(long_msg, msg) }.to raise_error(RbNaCl::LengthError) 80 | end 81 | it "raises descriptively on a long message in position 2" do 82 | expect { RbNaCl::Util.verify32!(msg, long_msg) }.to raise_error(RbNaCl::LengthError) 83 | end 84 | end 85 | 86 | context ".verify32" do 87 | let(:msg) { RbNaCl::Util.zeros(32) } 88 | let(:identical_msg) { RbNaCl::Util.zeros(32) } 89 | let(:other_msg) { RbNaCl::Util.zeros(31) + "\001" } 90 | let(:short_msg) { RbNaCl::Util.zeros(31) } 91 | let(:long_msg) { RbNaCl::Util.zeros(33) } 92 | 93 | it "confirms identical messages are identical" do 94 | expect(RbNaCl::Util.verify32(msg, identical_msg)).to be true 95 | end 96 | 97 | it "confirms non-identical messages are non-identical" do 98 | expect(RbNaCl::Util.verify32(msg, other_msg)).to be false 99 | expect(RbNaCl::Util.verify32(other_msg, msg)).to be false 100 | expect(RbNaCl::Util.verify32(short_msg, msg)).to be false 101 | expect(RbNaCl::Util.verify32(msg, short_msg)).to be false 102 | expect(RbNaCl::Util.verify32(long_msg, msg)).to be false 103 | expect(RbNaCl::Util.verify32(msg, long_msg)).to be false 104 | end 105 | end 106 | 107 | context ".verify16!" do 108 | let(:msg) { RbNaCl::Util.zeros(16) } 109 | let(:identical_msg) { RbNaCl::Util.zeros(16) } 110 | let(:other_msg) { RbNaCl::Util.zeros(15) + "\001" } 111 | let(:short_msg) { RbNaCl::Util.zeros(15) } 112 | let(:long_msg) { RbNaCl::Util.zeros(17) } 113 | 114 | it "confirms identical messages are identical" do 115 | expect(RbNaCl::Util.verify16!(msg, identical_msg)).to be true 116 | end 117 | 118 | it "confirms non-identical messages are non-identical" do 119 | expect(RbNaCl::Util.verify16!(msg, other_msg)).to be false 120 | expect(RbNaCl::Util.verify16!(other_msg, msg)).to be false 121 | end 122 | 123 | it "raises descriptively on a short message in position 1" do 124 | expect { RbNaCl::Util.verify16!(short_msg, msg) }.to raise_error(RbNaCl::LengthError) 125 | end 126 | it "raises descriptively on a short message in position 2" do 127 | expect { RbNaCl::Util.verify16!(msg, short_msg) }.to raise_error(RbNaCl::LengthError) 128 | end 129 | it "raises descriptively on a long message in position 1" do 130 | expect { RbNaCl::Util.verify16!(long_msg, msg) }.to raise_error(RbNaCl::LengthError) 131 | end 132 | it "raises descriptively on a long message in position 2" do 133 | expect { RbNaCl::Util.verify16!(msg, long_msg) }.to raise_error(RbNaCl::LengthError) 134 | end 135 | end 136 | 137 | context ".verify16" do 138 | let(:msg) { RbNaCl::Util.zeros(16) } 139 | let(:identical_msg) { RbNaCl::Util.zeros(16) } 140 | let(:other_msg) { RbNaCl::Util.zeros(15) + "\001" } 141 | let(:short_msg) { RbNaCl::Util.zeros(15) } 142 | let(:long_msg) { RbNaCl::Util.zeros(17) } 143 | 144 | it "confirms identical messages are identical" do 145 | expect(RbNaCl::Util.verify16(msg, identical_msg)).to be true 146 | end 147 | 148 | it "confirms non-identical messages are non-identical" do 149 | expect(RbNaCl::Util.verify16(msg, other_msg)).to be false 150 | expect(RbNaCl::Util.verify16(other_msg, msg)).to be false 151 | expect(RbNaCl::Util.verify16(short_msg, msg)).to be false 152 | expect(RbNaCl::Util.verify16(msg, short_msg)).to be false 153 | expect(RbNaCl::Util.verify16(long_msg, msg)).to be false 154 | expect(RbNaCl::Util.verify16(msg, long_msg)).to be false 155 | end 156 | end 157 | 158 | context "check_length" do 159 | it "accepts strings of the correct length" do 160 | expect do 161 | RbNaCl::Util.check_length("A" * 4, 4, "Test String") 162 | end.not_to raise_error 163 | end 164 | it "rejects strings which are too short" do 165 | expect do 166 | RbNaCl::Util.check_length("A" * 3, 4, "Test String") 167 | end.to raise_error(RbNaCl::LengthError, "Test String was 3 bytes (Expected 4)") 168 | end 169 | it "rejects strings which are too long" do 170 | expect do 171 | RbNaCl::Util.check_length("A" * 5, 4, "Test String") 172 | end.to raise_error(RbNaCl::LengthError, "Test String was 5 bytes (Expected 4)") 173 | end 174 | it "rejects nil strings" do 175 | expect do 176 | RbNaCl::Util.check_length(nil, 4, "Test String") 177 | end.to raise_error(RbNaCl::LengthError, "Test String was nil (Expected 4)") 178 | end 179 | end 180 | 181 | context "check_string" do 182 | let(:example_string) { "foobar".dup.force_encoding("UTF-8") } 183 | 184 | it "raises EncodingError when given strings with non-BINARY encoding" do 185 | expect do 186 | RbNaCl::Util.check_string(example_string, example_string.bytesize, "encoding test") 187 | end.to raise_error(EncodingError) 188 | end 189 | end 190 | 191 | context "hex encoding" do 192 | let(:bytes) { [0xDE, 0xAD, 0xBE, 0xEF].pack("c*") } 193 | let(:hex) { "deadbeef" } 194 | 195 | it "encodes to hex with bin2hex" do 196 | expect(RbNaCl::Util.bin2hex(bytes)).to eq hex 197 | end 198 | 199 | it "decodes from hex with hex2bin" do 200 | expect(RbNaCl::Util.hex2bin(hex)).to eq bytes 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /spec/shared/aead.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "aead" do 5 | let(:corrupt_ciphertext) { ciphertext.succ } 6 | let(:trunc_ciphertext) { ciphertext[0, 20] } 7 | let(:invalid_nonce) { nonce[0, nonce.bytesize / 2] } # too short! 8 | let(:invalid_nonce_long) { nonce + nonce } # too long! 9 | let(:nonce_error_regex) { /Nonce.*(Expected #{aead.nonce_bytes})/ } 10 | let(:corrupt_ad) { ad.succ } 11 | let(:trunc_ad) { ad[0, ad.bytesize / 2] } 12 | 13 | let(:aead) { described_class.new(key) } 14 | 15 | context "new" do 16 | it "accepts strings" do 17 | expect { described_class.new(key) }.to_not raise_error 18 | end 19 | 20 | it "raises on a nil key" do 21 | expect { described_class.new(nil) }.to raise_error(TypeError) 22 | end 23 | 24 | it "raises on a short key" do 25 | expect { described_class.new("hello") }.to raise_error RbNaCl::LengthError 26 | end 27 | 28 | it "raises on a long key" do 29 | expect { described_class.new("hello" + key) }.to raise_error RbNaCl::LengthError 30 | end 31 | end 32 | 33 | context "encrypt" do 34 | it "encrypts a message" do 35 | expect(aead.encrypt(nonce, message, ad)).to eq ciphertext 36 | end 37 | 38 | it "raises on a short nonce" do 39 | expect do 40 | aead.encrypt(invalid_nonce, message, ad) 41 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 42 | end 43 | 44 | it "raises on a long nonce" do 45 | expect do 46 | aead.encrypt(invalid_nonce_long, message, ad) 47 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 48 | end 49 | 50 | it "works with an empty message" do 51 | expect do 52 | aead.encrypt(nonce, nil, ad) 53 | end.to_not raise_error 54 | end 55 | 56 | it "works with an empty additional data" do 57 | expect do 58 | aead.encrypt(nonce, message, nil) 59 | end.to_not raise_error 60 | end 61 | end 62 | 63 | context "decrypt" do 64 | it "decrypts a message" do 65 | expect(aead.decrypt(nonce, ciphertext, ad)).to eq message 66 | end 67 | 68 | it "raises on a truncated message to decrypt" do 69 | expect do 70 | aead.decrypt(nonce, trunc_ciphertext, ad) 71 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 72 | end 73 | 74 | it "raises on a corrupt ciphertext" do 75 | expect do 76 | aead.decrypt(nonce, corrupt_ciphertext, ad) 77 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 78 | end 79 | 80 | it "raises when the additional data is truncated" do 81 | expect do 82 | aead.decrypt(nonce, ciphertext, corrupt_ad) 83 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 84 | end 85 | 86 | it "raises when the additional data is corrupt " do 87 | expect do 88 | aead.decrypt(nonce, ciphertext, trunc_ad) 89 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 90 | end 91 | 92 | it "raises on a short nonce" do 93 | expect do 94 | aead.decrypt(invalid_nonce, message, ad) 95 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 96 | end 97 | 98 | it "raises on a long nonce" do 99 | expect do 100 | aead.decrypt(invalid_nonce_long, message, ad) 101 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/shared/authenticator.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "authenticator" do 5 | context ".new" do 6 | it "accepts a key" do 7 | expect { described_class.new(key) }.to_not raise_error 8 | end 9 | 10 | it "requires a key" do 11 | expect { described_class.new }.to raise_error(ArgumentError) 12 | end 13 | 14 | it "raises TypeError on a nil key" do 15 | expect { described_class.new(nil) }.to raise_error(TypeError) 16 | end 17 | end 18 | 19 | context ".auth" do 20 | it "produces an authenticator" do 21 | expect(described_class.auth(key, message)).to eq tag 22 | end 23 | 24 | it "raises TypeError on a nil key" do 25 | expect { described_class.auth(nil, message) }.to raise_error(TypeError) 26 | end 27 | end 28 | 29 | context ".verify" do 30 | it "verify an authenticator" do 31 | expect(described_class.verify(key, tag, message)).to eq true 32 | end 33 | 34 | it "raises TypeError on a nil key" do 35 | expect { described_class.verify(nil, tag, message) }.to raise_error(TypeError) 36 | end 37 | 38 | it "fails to validate an invalid authenticator" do 39 | expect { described_class.verify(key, tag, message + "\0") }.to raise_error(RbNaCl::BadAuthenticatorError) 40 | end 41 | 42 | it "fails to validate a short authenticator" do 43 | expect { described_class.verify(key, tag[0, tag.bytesize - 2], message) }.to raise_error(RbNaCl::LengthError) 44 | end 45 | 46 | it "fails to validate a long authenticator" do 47 | expect { described_class.verify(key, tag + "\0", message) }.to raise_error(RbNaCl::LengthError) 48 | end 49 | end 50 | 51 | context "Instance methods" do 52 | let(:authenticator) { described_class.new(key) } 53 | 54 | context "#auth" do 55 | it "produces an authenticator" do 56 | expect(authenticator.auth(message)).to eq tag 57 | end 58 | end 59 | 60 | context "#verify" do 61 | it "verifies an authenticator" do 62 | expect(authenticator.verify(tag, message)).to be true 63 | end 64 | 65 | it "fails to validate an invalid authenticator" do 66 | expect { authenticator.verify(tag, message + "\0") }.to raise_error(RbNaCl::BadAuthenticatorError) 67 | end 68 | 69 | it "fails to validate a short authenticator" do 70 | expect { authenticator.verify(tag[0, tag.bytesize - 2], message) }.to raise_error(RbNaCl::LengthError) 71 | end 72 | 73 | it "fails to validate a long authenticator" do 74 | expect { authenticator.verify(tag + "\0", message) }.to raise_error(RbNaCl::LengthError) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/shared/box.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "box" do 5 | let(:nonce) { vector :box_nonce } 6 | let(:invalid_nonce) { nonce[0, 12] } # too short! 7 | let(:invalid_nonce_long) { nonce + nonce } # too long! 8 | let(:message) { vector :box_message } 9 | let(:ciphertext) { vector :box_ciphertext } 10 | let(:nonce_error_regex) { /Nonce.*(Expected #{box.nonce_bytes})/ } 11 | let(:corrupt_ciphertext) { ciphertext[80] = " " } # picked at random by fair diceroll 12 | 13 | context "box" do 14 | it "encrypts a message" do 15 | expect(box.box(nonce, message)).to eq ciphertext 16 | end 17 | 18 | it "raises on a short nonce" do 19 | expect do 20 | box.box(invalid_nonce, message) 21 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 22 | end 23 | 24 | it "raises on a long nonce" do 25 | expect do 26 | box.box(invalid_nonce_long, message) 27 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 28 | end 29 | end 30 | 31 | context "open" do 32 | it "decrypts a message" do 33 | expect(box.open(nonce, ciphertext)).to eq message 34 | end 35 | 36 | it "raises on a truncated message to decrypt" do 37 | expect do 38 | box.open(nonce, ciphertext[0, 64]) 39 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 40 | end 41 | 42 | it "raises on a corrupt ciphertext" do 43 | expect do 44 | box.open(nonce, corrupt_ciphertext) 45 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 46 | end 47 | 48 | it "raises on a short nonce" do 49 | expect do 50 | box.open(invalid_nonce, message) 51 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 52 | end 53 | 54 | it "raises on a long nonce" do 55 | expect do 56 | box.open(invalid_nonce_long, message) 57 | end.to raise_error(RbNaCl::LengthError, nonce_error_regex) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/shared/hmac.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "HMAC" do 5 | context ".new" do 6 | it "raises EncodingError on a key with wrong encoding" do 7 | expect { described_class.new(wrong_key) }.to raise_error(EncodingError) 8 | end 9 | 10 | it "raises LengthError when key is zero bytes" do 11 | expect { described_class.new("") }.to raise_error(::RbNaCl::LengthError) 12 | end 13 | end 14 | 15 | context ".auth" do 16 | it "raises EncodingError on a key with wrong encoding " do 17 | expect { described_class.auth(wrong_key, message) }.to raise_error(EncodingError) 18 | end 19 | end 20 | 21 | context ".verify" do 22 | it "raises EncodingError on a key with wrong encoding" do 23 | expect { described_class.verify(wrong_key, tag, message) }.to raise_error(EncodingError) 24 | end 25 | end 26 | 27 | context "Instance methods" do 28 | let(:authenticator) { described_class.new(key) } 29 | 30 | before(:each) { authenticator.update(message) } 31 | 32 | context "#update" do 33 | it "returns hexdigest when produces an authenticator" do 34 | expect(authenticator.update(message)).to eq mult_tag.unpack1("H*") 35 | end 36 | end 37 | 38 | context "#digest" do 39 | it "returns an authenticator" do 40 | expect(authenticator.digest).to eq tag 41 | end 42 | end 43 | 44 | context "#hexdigest" do 45 | it "returns hex authenticator" do 46 | expect(authenticator.hexdigest).to eq tag.unpack1("H*") 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/shared/key_equality.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "key equality" do 5 | context "equality" do 6 | it "equal keys are equal" do 7 | expect(described_class.new(key_bytes) == key).to be true 8 | end 9 | it "equal keys are equal to the string" do 10 | expect(key == key_bytes).to be true 11 | end 12 | it "keys are not equal to zero" do 13 | expect(key == RbNaCl::Util.zeros(32)).to be false 14 | end 15 | it "keys are not equal to another key" do 16 | expect(key == other_key).to be false 17 | end 18 | end 19 | 20 | context "lexicographic sorting" do 21 | it "can be compared lexicographically to a key smaller than it" do 22 | expect(key > RbNaCl::Util.zeros(32)).to be true 23 | end 24 | it "can be compared lexicographically to a key larger than it" do 25 | expect(described_class.new(RbNaCl::Util.zeros(32)) < key).to be true 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/shared/sealed_box.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "sealed_box" do 5 | let(:message) { vector :box_message } 6 | let(:ciphertext) { vector :box_ciphertext } 7 | let(:corrupt_ciphertext) { ciphertext[80] = " " } # picked at random by fair diceroll 8 | 9 | context "box" do 10 | it "roundtrips" do 11 | ciphertext = box.box(message) 12 | expect(box.open(ciphertext)).to eq message 13 | end 14 | end 15 | 16 | context "open" do 17 | it "raises on a truncated message to decrypt" do 18 | expect do 19 | box.open(ciphertext[0, 64]) 20 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 21 | end 22 | 23 | it "raises on a corrupt ciphertext" do 24 | expect do 25 | box.open(corrupt_ciphertext) 26 | end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/shared/serializable.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | RSpec.shared_examples "serializable" do 5 | context "serialization" do 6 | it "supports #to_s" do 7 | expect(subject.to_s).to be_a String 8 | end 9 | 10 | it "supports #to_str" do 11 | expect(subject.to_str).to be_a String 12 | end 13 | 14 | it "supports #inspect" do 15 | expect(subject.inspect).to be_a String 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | require "json" 5 | require "coveralls" 6 | Coveralls.wear! 7 | 8 | # Run the specs prior to running the self-test 9 | $RBNACL_SELF_TEST = false 10 | 11 | require "bundler/setup" 12 | require "rbnacl" 13 | 14 | require "shared/box" 15 | require "shared/sealed_box" 16 | require "shared/authenticator" 17 | require "shared/key_equality" 18 | require "shared/serializable" 19 | require "shared/aead" 20 | require "shared/hmac" 21 | 22 | def vector(name) 23 | [RbNaCl::TEST_VECTORS[name]].pack("H*") 24 | end 25 | 26 | RSpec.configure do |config| 27 | config.after :all do 28 | # Run the self-test after all the specs have passed 29 | require "rbnacl/self_test" 30 | end 31 | 32 | config.disable_monkey_patching! 33 | end 34 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | gem "rspec" 5 | require "rspec/core/rake_task" 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | RSpec::Core::RakeTask.new(:rcov) do |task| 10 | task.rcov = true 11 | end 12 | -------------------------------------------------------------------------------- /tasks/rubocop.rake: -------------------------------------------------------------------------------- 1 | # encoding: binary 2 | # frozen_string_literal: true 3 | 4 | require "rubocop/rake_task" 5 | 6 | RuboCop::RakeTask.new 7 | --------------------------------------------------------------------------------