├── .ruby-version ├── lib ├── tss │ ├── version.rb │ ├── cli_version.rb │ ├── cli_common.rb │ ├── hasher.rb │ ├── cli_combine.rb │ ├── cli_split.rb │ ├── tss.rb │ ├── splitter.rb │ ├── util.rb │ └── combiner.rb ├── tss.rb └── custom_contracts.rb ├── .coco.yml ├── Gemfile ├── .yardopts ├── .gitignore ├── .travis.yml ├── bin ├── setup ├── tss └── console ├── .hound.yml ├── .editorconfig ├── test ├── test_helper.rb ├── tss_burn_brute.rb ├── tss_benchmark.rb ├── tss_test.rb ├── tss_combiner_validation_test.rb ├── tss_hasher_test.rb ├── tss_python_interop_test.rb ├── tss_splitter_validation_test.rb └── tss_util_test.rb ├── .codeclimate.yml ├── Rakefile ├── LICENSE.txt ├── certs ├── gem-public_cert_grempe.pem └── gem-public_cert_grempe_2026.pem ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── RELEASE.md ├── tss.gemspec ├── README.md └── .rubocop.yml /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | -------------------------------------------------------------------------------- /lib/tss/version.rb: -------------------------------------------------------------------------------- 1 | module TSS 2 | VERSION = '0.5.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/tss.rb: -------------------------------------------------------------------------------- 1 | require 'contracts' 2 | require 'custom_contracts' 3 | require 'tss/tss' 4 | -------------------------------------------------------------------------------- /.coco.yml: -------------------------------------------------------------------------------- 1 | :include: 2 | - lib 3 | :exclude: 4 | - test 5 | :theme: dark 6 | :show_link_in_terminal: true 7 | 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tss.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin contracts -e lib/tss/custom_contracts.rb --no-private lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.5 4 | - 2.3.1 5 | - 2.4.0 6 | - jruby-9.1.5.0 7 | before_install: gem install bundler -v 1.13.2 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | scss: 2 | enabled: false 3 | 4 | ruby: 5 | config_file: .rubocop.yml 6 | 7 | javascript: 8 | ignore_file: .javascript_ignore 9 | 10 | fail_on_violations: true 11 | -------------------------------------------------------------------------------- /bin/tss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require 'tss' 5 | require 'tss/cli_version' 6 | require 'tss/cli_common' 7 | require 'tss/cli_split' 8 | require 'tss/cli_combine' 9 | 10 | TSS::CLI.start 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /lib/tss/cli_version.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module TSS 4 | class CLI < Thor 5 | desc 'version', 'tss version' 6 | 7 | long_desc <<-LONGDESC 8 | Display the current version of TSS 9 | LONGDESC 10 | 11 | def version 12 | say("TSS #{TSS::VERSION}") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'tss' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | require 'pry' 11 | Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # coveralls.io and coco are incompatible. Run each in their own env. 2 | if ENV['TRAVIS'] || ENV['CI'] || ENV['JENKINS_URL'] || ENV['TDDIUM'] || ENV['COVERALLS_RUN_LOCALLY'] 3 | # coveralls.io : web based code coverage 4 | require 'coveralls' 5 | Coveralls.wear! 6 | else 7 | # coco : local code coverage 8 | require 'coco' 9 | end 10 | 11 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 12 | require 'tss' 13 | 14 | require 'minitest/autorun' 15 | require 'minitest/pride' 16 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | exclude_fingerprints: 12 | - ff3b0ae9caf19067c4e103fe1456a126 13 | - 8ec23fad3fad979a5d1de264823ba8fa 14 | - cd954cababda47d20480edce9f605ac2 15 | - 22f73502e4c082c4f78cf483f0150bef 16 | fixme: 17 | enabled: true 18 | rubocop: 19 | enabled: true 20 | ratings: 21 | paths: 22 | - "**.inc" 23 | - "**.js" 24 | - "**.jsx" 25 | - "**.module" 26 | - "**.php" 27 | - "**.py" 28 | - "**.rb" 29 | exclude_paths: 30 | - test/ 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'wwtd/tasks' 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << 'test' 7 | t.libs << 'lib' 8 | t.test_files = FileList['test/**/*_test.rb'] 9 | t.verbose = false 10 | t.warning = false 11 | end 12 | 13 | Rake::TestTask.new(:bench) do |t| 14 | t.libs << 'test' 15 | t.libs << 'lib' 16 | t.test_files = FileList['test/**/*_benchmark.rb'] 17 | t.verbose = false 18 | t.warning = false 19 | end 20 | 21 | # a long running brute force burn-in test 22 | Rake::TestTask.new(:burn) do |t| 23 | t.libs << 'test' 24 | t.libs << 'lib' 25 | t.test_files = FileList['test/**/*_brute.rb'] 26 | t.verbose = false 27 | t.warning = false 28 | end 29 | 30 | task :test_all => [:test, :bench, :burn] 31 | 32 | task default: :test 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Glenn Rempe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/tss_burn_brute.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS do 4 | describe 'end-to-end burn-in test' do 5 | it 'must split and combine the secret properly using many combinations of options' do 6 | ['HUMAN', 'BINARY'].each do |f| 7 | ['NONE', 'SHA1', 'SHA256'].each do |h| 8 | (1..10).each do |m| 9 | (m..10).each do |n| 10 | id = SecureRandom.hex(rand(1..8)) 11 | s = SecureRandom.hex(rand(1..8)) 12 | shares = TSS.split(secret: s, identifier: id, threshold: m, num_shares: n, hash_alg: h, format: f) 13 | shares.first.encoding.name.must_equal f == 'HUMAN' ? 'UTF-8' : 'ASCII-8BIT' 14 | 15 | ['FIRST', 'SAMPLE', 'COMBINATIONS'].each do |sb| 16 | # can't use combinations with NONE 17 | sb = (h == 'NONE') ? 'FIRST' : sb 18 | sec = TSS.combine(shares: shares, select_by: sb) 19 | sec[:secret].must_equal s 20 | sec[:secret].encoding.name.must_equal 'UTF-8' 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/tss/cli_common.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module TSS 4 | class CLI < Thor 5 | 6 | class_option :verbose, :type => :boolean, :aliases => '-v', :desc => 'Display additional logging output' 7 | 8 | no_commands do 9 | # rubocop:disable CyclomaticComplexity 10 | def exit_if_binary!(str) 11 | str.each_byte { |c| 12 | # OK, 9 (TAB), 10 (CR), 13 (LF), >=32 for normal ASCII 13 | # Usage of anything other than 10, 13, and 32-126 ASCII decimal codes 14 | # looks as though contents are binary and not standard text. 15 | if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) || c == 127 16 | err('STDIN secret appears to contain binary data.') 17 | exit(1) 18 | end 19 | } 20 | 21 | unless ['UTF-8', 'US-ASCII'].include?(str.encoding.name) 22 | err('STDIN secret has a non UTF-8 or US-ASCII encoding.') 23 | exit(1) 24 | end 25 | end 26 | # rubocop:enable CyclomaticComplexity 27 | 28 | def log(str) 29 | say_status(:log, "#{Time.now.utc.iso8601} : #{str}", :white) if options[:verbose] 30 | end 31 | 32 | def err(str) 33 | say_status(:error, "#{Time.now.utc.iso8601} : #{str}", :red) 34 | end 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /certs/gem-public_cert_grempe.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYDCCAkigAwIBAgIBATANBgkqhkiG9w0BAQUFADA7MQ4wDAYDVQQDDAVnbGVu 3 | bjEVMBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwHhcN 4 | MTYwNDExMDI0NTU0WhcNMTcwNDExMDI0NTU0WjA7MQ4wDAYDVQQDDAVnbGVubjEV 5 | MBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwggEiMA0G 6 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZqTH5Jf+D/W2B4BIiL49CpHa86rK/ 7 | oT+v3xZwuEE92lJea+ygn3IAsidVTW47AKE6Lt3UqUkGQGKxsqH/Dhir08BqjLlD 8 | gBUozGZpM3B6uWZnD6QXLbOmZeGVDnwB/QDfzaawN1i3smlYxYT+KNLjl80aN3we 9 | /cHAWG7JG47AF/S91mYcg1WgZnDgZt9+RyVR1AsfYbM+SidOSoXEOHPCbuUxLKJb 10 | gj5ieCFhm5GNWEugvgiX/ruas+VHV0fF3fzjYlU2fZPTuQyB4UD5FWX4UqdsBf3w 11 | jB94TDBsJ3FVGPbggEhLGKd8pbQmBIOqXolGaqhs7dnuf5imu5mAXHC1AgMBAAGj 12 | bzBtMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBRfxEyosUbKjfFa 13 | j+gae2CcT3aFCTAZBgNVHREEEjAQgQ5nbGVubkByZW1wZS51czAZBgNVHRIEEjAQ 14 | gQ5nbGVubkByZW1wZS51czANBgkqhkiG9w0BAQUFAAOCAQEAzgK20+MNOknR9Kx6 15 | RisI3DsioCADjGldxY+INrwoTfPDVmNm4GdTYC+V+/BvxJw1RqHjEbuXSg0iibQC 16 | 4vN+th0Km7dnas/td1i+EKfGencfyQyecIaG9l3kbCkCWnldRtZ+BS5EfP2ML2u8 17 | fyCtze/Piovu8IwXL1W5kGZMnvzLmWxdqI3VPUou40n8F+EiMMLgd53kpzjtNOau 18 | 4W+mqVGOwlEGVSgI5+0SIsD8pvc62PlPWTv0kn1bcufKKCZmoVmpfbe3j4JpBInq 19 | zieXiXZSAojfFx9g91fKdIrlPbInHU/BaCxXSLBwvOM0drE+c2ue9X8gB55XAhzX 20 | 37oBiw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /certs/gem-public_cert_grempe_2026.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYDCCAkigAwIBAgIBATANBgkqhkiG9w0BAQUFADA7MQ4wDAYDVQQDDAVnbGVu 3 | bjEVMBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwHhcN 4 | MTYxMDEzMDEzMjM5WhcNMjYxMDExMDEzMjM5WjA7MQ4wDAYDVQQDDAVnbGVubjEV 5 | MBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwggEiMA0G 6 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrEuLEy11cjgMC4+ldcgLzBrGcfWWg 7 | nUhdCRn3Arzo2EV1d4V4h6VOHmk4o7kumBeajUMMZ0+xKtu8euRCnbDnlxowfJvT 8 | S0nzsOt1dm++INeKMpZU84LuH7BbAlyL+B//l1YkI33gsbA8wm06+vV8tUEBuQch 9 | vBU2xrCyS2+0LQTCaCS+VvHbV97hzIwSIgUFJuFjrcnnpV8Qt1R0Bi8pzDk+2jyN 10 | AgxaWa41UHn70O0gFRRDGXacRpvy3HRSJrvlHPPAC02CjhKjsOLjZowaHxCv9XIJ 11 | tCQnVEOUUo9+owG2Gna4k4DMLIjiGChHNFXtO8WyuksukVqcsdc9kvdzAgMBAAGj 12 | bzBtMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBR68/Ook0uwfe6t 13 | FbLHXIReYQ2VpzAZBgNVHREEEjAQgQ5nbGVubkByZW1wZS51czAZBgNVHRIEEjAQ 14 | gQ5nbGVubkByZW1wZS51czANBgkqhkiG9w0BAQUFAAOCAQEAI27KUzTE9BoD2irI 15 | CkMVPC0YS6iANrzQy3zIJI4yLKEZmI1jDE+W2APL11Woo5+sttgqY7148W84ZWdK 16 | mD9ueqH5hPC8NOd3wYXVMNwmyLhnyh80cOzGeurW1SJ0VV3BqSKEE8q4EFjCzUK9 17 | Oq8dW9i9Bxn8qgcOSFTYITJZ/mNyy2shHs5gg0MIz0uOsKaHqrrMseVfG7ZoTgV1 18 | kkyRaYAHI1MSDNGFNwgURPQsgnxQrX8YG48q0ypFC1gOl/l6D0e/oF4SKMS156uc 19 | vprF5QiDz8HshVP9DjJT2I1wyGyvxEdU3cTRo0upMP/VZLcgyBVFy90N2XYWWk2D 20 | GIxGSw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/tss_benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'minitest/benchmark' 3 | 4 | # SEE : http://chriskottom.com/blog/2015/04/minitest-benchmark-an-introduction/ 5 | # SEE : http://chriskottom.com/blog/2015/05/minitest-benchmark-a-practical-example/ 6 | 7 | # vary the number of shares created while keeping the secret constant 8 | class SplitBenchmarkNumShares < Minitest::Benchmark 9 | def self.bench_range 10 | bench_linear(1, 255, 8) 11 | end 12 | 13 | def bench_tss_split_num_shares 14 | assert_performance_linear(0.900) do |n| 15 | TSS.split(secret: 'secret', threshold: n, num_shares: n, hash_alg: 'SHA256') 16 | end 17 | end 18 | end 19 | 20 | # vary the size of the secret while keeping the number of shares constant 21 | class SplitBenchmarkSecretLength < Minitest::Benchmark 22 | def self.bench_range 23 | bench_linear(1, 65_534, 4096) 24 | end 25 | 26 | def bench_tss_split_secret_size 27 | assert_performance_linear(0.900) do |n| 28 | @secret = 'a' * n 29 | TSS.split(secret: @secret, threshold: 3, num_shares: 3, hash_alg: 'SHA256') 30 | end 31 | end 32 | end 33 | 34 | # when combining shares, vary the number of shares passed in to be processed 35 | class CombineBenchmarkNumShares < Minitest::Benchmark 36 | def self.bench_range 37 | bench_linear(8, 255, 8) 38 | end 39 | 40 | def setup 41 | @s = TSS.split(secret: SecureRandom.hex(32), threshold: 8, num_shares: 255, hash_alg: 'SHA256') 42 | end 43 | 44 | def bench_tss_combine_num_shares 45 | assert_performance_constant(0.900) do |n| 46 | TSS.combine(shares: @s.sample(n)) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.5.0 (1/28/2017) 4 | 5 | * Breaking refactor to use automatic PKCS#7 padding on secrets w/ 16 byte block size. 6 | * Update Copyright year 7 | * Ruby 2.4.0 testing 8 | * Update version of sysrandom gem 9 | * Fixed minitest spec warning 10 | 11 | ## v0.4.2 (10/12/2016) 12 | 13 | * Sign the gem with a new cert that expires in 10 years. 14 | Both old and new public certs are in the certs dir. 15 | 16 | ## v0.4.1 (9/28/2016) 17 | 18 | * Use activesupport for blank support. Remove the extraction. 19 | * Update sysrandom and remove the workaround needed for earlier version 20 | 21 | ## v0.4.0 (9/24/2016) 22 | 23 | * Breaking change to force upcasing of some addition string args 24 | * Use yard-contracts 25 | * yardoc cleanups 26 | * Remove int_commas utility method 27 | * Hash w/ sha256 a,b strings in secure_compare 28 | * Deeper Contracts integration 29 | 30 | ## v0.3.0 (9/24/2016) 31 | 32 | * Breaking change, identifier cannot be an empty string 33 | * Much greater coverage of functions with Contracts 34 | * Related documentation updates 35 | 36 | ## v0.2.0 (9/23/2016) 37 | 38 | * clone the shares object passed to TSS.combine to prevent modification 39 | * test enhancements 40 | * use cryptosphere/sysrandom in place of native securerandom 41 | * integer args are no longer coercible from Strings 42 | * Remove dry-* in favor of contracts (http://egonschiele.github.io/contracts.ruby/) 43 | * readme fixes 44 | 45 | ## v0.1.1 (4/14/2016) 46 | 47 | * documentation enhancements 48 | * added two additional custom exception classes to allow rescuing from no secret recovery or invalid hash during recovery 49 | * specify Rubies >= 2.1.0 in gemspec 50 | 51 | ## v0.1.0 (4/12/2016) 52 | 53 | This is the initial ALPHA quality release of the tss gem. 54 | 55 | It is for review only and should not yet be used in production. 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at glenn@rempe.us. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Gem Release Process 2 | 3 | Don't use the `bundle exec rake release` task. It is more convenient, 4 | but it skips the process of signing the version release task. 5 | 6 | ## Run Tests 7 | 8 | ```sh 9 | $ rake test_all 10 | ``` 11 | 12 | ## Git Push 13 | 14 | ```sh 15 | $ git push 16 | ``` 17 | 18 | Check for regressions in automated tests: 19 | 20 | * [https://travis-ci.org/grempe/tss-rb](https://travis-ci.org/grempe/tss-rb) 21 | * [https://coveralls.io/github/grempe/tss-rb?branch=master](https://coveralls.io/github/grempe/tss-rb?branch=master) 22 | * [https://codeclimate.com/github/grempe/tss-rb](https://codeclimate.com/github/grempe/tss-rb) 23 | 24 | ## Bump Version Number and edit CHANGELOG.md 25 | 26 | ```sh 27 | $ vi lib/tss/version.rb 28 | $ git add lib/tss/version.rb 29 | $ vi CHANGELOG.md 30 | $ git add CHANGELOG.md 31 | ``` 32 | 33 | ## Local Build and Install w/ Signed Gem 34 | 35 | The `build` step should ask for PEM passphrase to sign gem. If it does 36 | not ask it means that the signing cert is not present. 37 | 38 | Build: 39 | 40 | ```sh 41 | $ rake build 42 | Enter PEM pass phrase: 43 | tss 0.5.0 built to pkg/tss-0.5.0.gem. 44 | ``` 45 | 46 | Install locally w/ Cert: 47 | 48 | ```sh 49 | $ gem uninstall tss 50 | $ rbenv rehash 51 | $ gem install pkg/tss-0.5.0.gem -P MediumSecurity 52 | Successfully installed tss-0.5.0 53 | 1 gem installed 54 | ``` 55 | 56 | ## Git Commit Version and CHANGELOG Changes, Tag and push to Github 57 | 58 | ```sh 59 | $ git commit -m 'Bump version v0.5.0' 60 | $ git tag -s v0.5.0 -m "v0.5.0" SHA1_OF_COMMIT 61 | ``` 62 | 63 | Verify last commit and last tag are GPG signed: 64 | 65 | ``` 66 | $ git tag -v v0.5.0 67 | ... 68 | gpg: Good signature from "Glenn Rempe (Code Signing Key) " [ultimate] 69 | ... 70 | ``` 71 | 72 | ``` 73 | $ git log --show-signature 74 | ... 75 | gpg: Good signature from "Glenn Rempe (Code Signing Key) " [ultimate] 76 | ... 77 | ``` 78 | 79 | Push code and tags to GitHub: 80 | 81 | ``` 82 | $ git push 83 | $ git push --tags 84 | ``` 85 | 86 | ## Push gem to Rubygems.org 87 | 88 | ```sh 89 | $ gem push pkg/tss-0.1.1.gem 90 | ``` 91 | 92 | Verify Gem Push at [https://rubygems.org/gems/tss](https://rubygems.org/gems/tss) 93 | 94 | ## Create a GitHub Release 95 | 96 | Specify the tag we just pushed to attach release to. Copy notes from CHANGELOG.md 97 | 98 | [https://github.com/grempe/tss-rb/releases](https://github.com/grempe/tss-rb/releases) 99 | 100 | ## Announce Release on Twitter 101 | 102 | The normal blah, blah, blah. 103 | -------------------------------------------------------------------------------- /tss.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tss/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'tss' 8 | spec.version = TSS::VERSION 9 | spec.authors = ['Glenn Rempe'] 10 | spec.email = ['glenn@rempe.us'] 11 | 12 | spec.required_ruby_version = '>= 2.2.2' 13 | 14 | cert = File.expand_path('~/.gem-certs/gem-private_key_grempe_2026.pem') 15 | if cert && File.exist?(cert) 16 | spec.signing_key = cert 17 | spec.cert_chain = ['certs/gem-public_cert_grempe_2026.pem'] 18 | end 19 | 20 | spec.summary = <<-EOF 21 | A Ruby gem implementing Threshold Secret Sharing. This code can be 22 | used in your Ruby applications or as a simple command line (CLI) 23 | executable for splitting and reconstructing secrets. 24 | EOF 25 | 26 | spec.description = <<-EOF 27 | Threshold Secret Sharing (TSS) provides a way to generate N shares 28 | from a value, so that any M of those shares can be used to 29 | reconstruct the original value, but any M-1 shares provide no 30 | information about that value. This method can provide shared access 31 | control on key material and other secrets that must be strongly 32 | protected. 33 | 34 | This gem implements a Threshold Secret Sharing method based on 35 | polynomial interpolation in GF(256) and a format for the storage and 36 | transmission of shares. 37 | 38 | This implementation follows the specification in the document: 39 | 40 | http://tools.ietf.org/html/draft-mcgrew-tss-03 41 | EOF 42 | 43 | spec.homepage = 'https://github.com/grempe/tss-rb' 44 | spec.license = 'MIT' 45 | 46 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 47 | spec.bindir = 'bin' 48 | spec.executables << 'tss' 49 | spec.require_paths = ['lib'] 50 | 51 | spec.add_dependency 'activesupport', '>= 4.0.0' 52 | spec.add_dependency 'sysrandom', '>= 1.0.3', '~> 1.0.4' 53 | spec.add_dependency 'contracts', '~> 0.14' 54 | spec.add_dependency 'binary_struct', '~> 2.1' 55 | spec.add_dependency 'thor', '~> 0.19' 56 | 57 | spec.add_development_dependency 'bundler', '~> 2.0' 58 | spec.add_development_dependency 'rake', '~> 11.1' 59 | spec.add_development_dependency 'minitest', '~> 5.0' 60 | spec.add_development_dependency 'pry', '~> 0.10' 61 | spec.add_development_dependency 'coveralls', '~> 0.8' 62 | spec.add_development_dependency 'coco', '~> 0.14' 63 | spec.add_development_dependency 'wwtd', '~> 1.3' 64 | spec.add_development_dependency 'yard-contracts', '~> 0.1' 65 | end 66 | -------------------------------------------------------------------------------- /lib/custom_contracts.rb: -------------------------------------------------------------------------------- 1 | module Contracts 2 | 3 | # Custom Contracts 4 | # See : https://egonschiele.github.io/contracts.ruby/ 5 | 6 | class ArrayOfShares 7 | def self.valid? val 8 | val.is_a?(Array) && 9 | val.length.between?(1,255) && 10 | Contracts::ArrayOf[String].valid?(val) 11 | end 12 | 13 | def self.to_s 14 | 'An Array of split secret shares' 15 | end 16 | end 17 | 18 | class SecretArg 19 | def self.valid? val 20 | val.is_a?(String) && 21 | val.length.between?(1,TSS::MAX_UNPADDED_SECRET_SIZE) && 22 | ['UTF-8', 'US-ASCII'].include?(val.encoding.name) 23 | end 24 | 25 | def self.to_s 26 | "must be a UTF-8 or US-ASCII String between 1 and #{TSS::MAX_UNPADDED_SECRET_SIZE} characters in length" 27 | end 28 | end 29 | 30 | class ThresholdArg 31 | def self.valid? val 32 | val.is_a?(Integer) && 33 | val.between?(1,255) 34 | end 35 | 36 | def self.to_s 37 | 'must be an Integer between 1 and 255' 38 | end 39 | end 40 | 41 | class NumSharesArg 42 | def self.valid? val 43 | val.is_a?(Integer) && 44 | val.between?(1,255) 45 | end 46 | 47 | def self.to_s 48 | 'must be an Integer between 1 and 255' 49 | end 50 | end 51 | 52 | class IdentifierArg 53 | def self.valid? val 54 | val.is_a?(String) && 55 | val.length.between?(1,16) && 56 | val =~ /^[a-zA-Z0-9\-\_\.]*$/i 57 | end 58 | 59 | def self.to_s 60 | 'must be a String between 1 and 16 characters in length limited to [a-z, A-Z, -, _, .]' 61 | end 62 | end 63 | 64 | class HashAlgArg 65 | def self.valid? val 66 | Contracts::Enum['NONE', 'SHA1', 'SHA256'].valid?(val) 67 | end 68 | 69 | def self.to_s 70 | 'must be a uppercase String specifying the hash algorithm to use [NONE, SHA1, SHA256].' 71 | end 72 | end 73 | 74 | class FormatArg 75 | def self.valid? val 76 | Contracts::Enum['BINARY', 'HUMAN'].valid?(val) 77 | end 78 | 79 | def self.to_s 80 | 'must be a uppercase String specifying the desired String share format [BINARY, HUMAN].' 81 | end 82 | end 83 | 84 | class SelectByArg 85 | def self.valid? val 86 | Contracts::Enum['FIRST', 'SAMPLE', 'COMBINATIONS'].valid?(val) 87 | end 88 | 89 | def self.to_s 90 | 'must be a uppercase String specifying the desired way to sample shares provided [FIRST, SAMPLE, COMBINATIONS].' 91 | end 92 | end 93 | 94 | class PadBlocksizeArg 95 | def self.valid? val 96 | val.is_a?(Integer) && 97 | val.between?(0,255) 98 | end 99 | 100 | def self.to_s 101 | 'must be an Integer between 0 and 255' 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/tss_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS do 4 | 5 | describe 'errors' do 6 | describe 'split' do 7 | it 'must raise an error if called without a Hash arg with secret key' do 8 | assert_raises(ParamContractError) { TSS.split([]) } 9 | assert_raises(ParamContractError) { TSS.split({}) } 10 | end 11 | end 12 | 13 | describe 'combine' do 14 | it 'must raise an error if called without a Hash arg with shares key' do 15 | assert_raises(ParamContractError) { TSS.combine([]) } 16 | assert_raises(ParamContractError) { TSS.combine({}) } 17 | end 18 | end 19 | end 20 | 21 | describe 'with common args' do 22 | it 'must split and combine the secret properly' do 23 | ['HUMAN', 'BINARY'].each do |f| 24 | ['NONE', 'SHA1', 'SHA256'].each do |h| 25 | ['a', 'unicode ½ ♥ 💩', SecureRandom.hex(32).force_encoding('US-ASCII')].each do |s| 26 | shares = TSS.split(secret: s, hash_alg: h, format: f) 27 | shares.first.encoding.name.must_equal f == 'HUMAN' ? 'UTF-8' : 'ASCII-8BIT' 28 | sec = TSS.combine(shares: shares) 29 | if h == 'NONE' 30 | sec[:hash].must_be_nil 31 | else 32 | sec[:hash].must_equal TSS::Hasher.hex_string(h, s) 33 | end 34 | sec[:hash_alg].must_equal h 35 | sec[:identifier].length.must_equal 16 36 | unless sec[:process_time] == 0.0 37 | sec[:process_time].must_be :>, 0.01 38 | end 39 | sec[:secret].must_equal s 40 | sec[:secret].encoding.name.must_equal 'UTF-8' 41 | sec[:threshold].must_equal 3 42 | end 43 | end 44 | end 45 | end 46 | end 47 | 48 | describe 'end-to-end test with *Rule of 64* max settings' do 49 | it 'must split and combine the secret properly' do 50 | # Note : .force_encoding('US-ASCII') only because SecureRandom.hex(32) 51 | # returns ASCII-8BIT encoding, not US-ASCII as on MRI. Lets tests pass 52 | # on jRuby. 53 | secret = SecureRandom.hex(32).force_encoding('US-ASCII') 54 | shares = TSS.split(secret: secret, threshold: 64, num_shares: 64, identifier: SecureRandom.hex(8), hash_alg: 'SHA256', select_by: 'first') 55 | shares.first.encoding.name.must_equal 'UTF-8' 56 | recovered_secret = TSS.combine(shares: shares) 57 | recovered_secret[:hash].must_equal Digest::SHA256.hexdigest(secret) 58 | recovered_secret[:hash_alg].must_equal 'SHA256' 59 | recovered_secret[:identifier].length.must_equal 16 60 | unless recovered_secret[:process_time] == 0.0 61 | recovered_secret[:process_time].must_be :>, 0.01 62 | end 63 | recovered_secret[:secret].must_equal secret 64 | recovered_secret[:secret].encoding.name.must_equal 'UTF-8' 65 | recovered_secret[:threshold].must_equal 64 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/tss/hasher.rb: -------------------------------------------------------------------------------- 1 | module TSS 2 | # Hasher is responsible for managing access to the various one-way hash 3 | # functions that can be used to validate a secret. 4 | class Hasher 5 | include Contracts::Core 6 | C = Contracts 7 | 8 | HASHES = { 'NONE' => { code: 0, bytesize: 0, hasher: nil }, 9 | 'SHA1' => { code: 1, bytesize: 20, hasher: Digest::SHA1 }, 10 | 'SHA256' => { code: 2, bytesize: 32, hasher: Digest::SHA256 }}.freeze 11 | 12 | # Lookup the Symbol key for a Hash with the code. 13 | # 14 | # @param code the hash code to convert to a Symbol key 15 | # @return the hash key String or nil if not found 16 | Contract C::Int => C::Maybe[C::HashAlgArg] 17 | def self.key_from_code(code) 18 | return nil unless Hasher.codes.include?(code) 19 | HASHES.each do |k, v| 20 | return k if v[:code] == code 21 | end 22 | end 23 | 24 | # Lookup the hash code for the hash matching hash_key. 25 | # 26 | # @param hash_key the hash key to convert to an Integer code 27 | # @return the hash key code 28 | Contract C::HashAlgArg => C::Maybe[C::Int] 29 | def self.code(hash_key) 30 | HASHES[hash_key][:code] 31 | end 32 | 33 | # Lookup all valid hash codes, including NONE. 34 | # 35 | # @return all hash codes including NONE 36 | Contract C::None => C::ArrayOf[C::Int] 37 | def self.codes 38 | HASHES.map do |_k, v| 39 | v[:code] 40 | end 41 | end 42 | 43 | # All valid hash codes that actually do hashing, excluding NONE. 44 | # 45 | # @return all hash codes excluding NONE 46 | Contract C::None => C::ArrayOf[C::Int] 47 | def self.codes_without_none 48 | HASHES.map do |_k, v| 49 | v[:code] if v[:code] > 0 50 | end.compact 51 | end 52 | 53 | # Lookup the size in Bytes for a specific hash_key. 54 | # 55 | # @param hash_key the hash key to lookup 56 | # @return the size in Bytes for a specific hash_key 57 | Contract C::HashAlgArg => C::Int 58 | def self.bytesize(hash_key) 59 | HASHES[hash_key][:bytesize] 60 | end 61 | 62 | # Return a hexdigest hash for a String using hash_key hash algorithm. 63 | # Returns '' if hash_key == 'NONE' 64 | # 65 | # @param hash_key the hash key to use to hash a String 66 | # @param str the String to hash 67 | # @return the hex digest for str 68 | Contract C::HashAlgArg, String => String 69 | def self.hex_string(hash_key, str) 70 | return '' if hash_key == 'NONE' 71 | HASHES[hash_key][:hasher].send(:hexdigest, str) 72 | end 73 | 74 | # Return a Byte String hash for a String using hash_key hash algorithm. 75 | # Returns '' if hash_key == 'NONE' 76 | # 77 | # @param hash_key the hash key to use to hash a String 78 | # @param str the String to hash 79 | # @return the Byte String digest for str 80 | Contract C::HashAlgArg, String => String 81 | def self.byte_string(hash_key, str) 82 | return '' if hash_key == 'NONE' 83 | HASHES[hash_key][:hasher].send(:digest, str) 84 | end 85 | 86 | # Return a Byte Array hash for a String using hash_key hash algorithm. 87 | # Returns [] if hash_key == 'NONE' 88 | # 89 | # @param hash_key the hash key to use to hash a String 90 | # @param str the String to hash 91 | # @return the Byte Array digest for str 92 | Contract C::HashAlgArg, String => C::ArrayOf[C::Int] 93 | def self.byte_array(hash_key, str) 94 | return [] if hash_key == 'NONE' 95 | HASHES[hash_key][:hasher].send(:digest, str).unpack('C*') 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/tss_combiner_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS::Combiner do 4 | before do 5 | @secret = 'I love secrets with multi-byte unicode characters ½ ♥ 💩' 6 | @threshold = 3 7 | @num_shares = 5 8 | @shares = TSS::Splitter.new(secret: @secret, threshold: @threshold, num_shares: @num_shares, identifier: SecureRandom.hex(8), hash_alg: 'SHA256').split 9 | end 10 | 11 | describe 'shares argument' do 12 | it 'must raise an error if a nil is passed' do 13 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: nil).combine } 14 | end 15 | 16 | it 'must raise an error if a non-Array is passed' do 17 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: 'foo').combine } 18 | end 19 | 20 | it 'must raise an error if any nils are passed in the shares array' do 21 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: @shares << nil).combine } 22 | end 23 | 24 | it 'must raise an error if Array with members that are not Strings is passed' do 25 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: ['foo', :bar]).combine } 26 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: ['foo', 123]).combine } 27 | end 28 | 29 | it 'must raise an error if an too small empty Array is passed' do 30 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: []).combine } 31 | end 32 | 33 | it 'must raise an error if a too large Array is passed' do 34 | arr = [] 35 | 256.times { arr << 'foo' } 36 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: arr).combine } 37 | end 38 | 39 | it 'must raise an error if an invalid share is passed' do 40 | assert_raises(TSS::ArgumentError) { TSS::Combiner.new(shares: ['foo']).combine } 41 | end 42 | 43 | it 'must raise an error if too few shares are passed' do 44 | assert_raises(TSS::ArgumentError) { TSS::Combiner.new(shares: @shares.sample(@threshold - 1)).combine } 45 | end 46 | 47 | it 'must return the right secret with exactly the right amount of valid shares' do 48 | secret = TSS::Combiner.new(shares: @shares.sample(@threshold)).combine 49 | assert_kind_of String, secret[:secret] 50 | secret[:secret].encoding.name.must_equal 'UTF-8' 51 | secret[:secret].must_equal @secret 52 | end 53 | 54 | it 'must return the right secret with all of the valid shares' do 55 | secret = TSS::Combiner.new(shares: @shares).combine 56 | assert_kind_of String, secret[:secret] 57 | secret[:secret].encoding.name.must_equal 'UTF-8' 58 | secret[:secret].must_equal @secret 59 | end 60 | end 61 | 62 | describe 'share selection args' do 63 | it 'must raise an error if a an invalid share_selection: value is passed' do 64 | assert_raises(ParamContractError) { TSS::Combiner.new(shares: @shares, select_by: 'foo').combine } 65 | end 66 | 67 | describe 'when share_selection arg is unset' do 68 | it 'must return a secret and default to first' do 69 | secret = TSS::Combiner.new(shares: @shares).combine 70 | secret[:secret].must_equal @secret 71 | end 72 | end 73 | 74 | describe 'when share_selection arg is set to first' do 75 | it 'must return a secret' do 76 | secret = TSS::Combiner.new(shares: @shares, select_by: 'FIRST').combine 77 | secret[:secret].must_equal @secret 78 | end 79 | end 80 | 81 | describe 'when share_selection arg is set to sample' do 82 | it 'must return a secret' do 83 | secret = TSS::Combiner.new(shares: @shares, select_by: 'SAMPLE').combine 84 | secret[:secret].must_equal @secret 85 | end 86 | end 87 | 88 | describe 'when share_selection arg is set to combinations' do 89 | it 'must return a secret' do 90 | secret = TSS::Combiner.new(shares: @shares, select_by: 'COMBINATIONS').combine 91 | secret[:secret].must_equal @secret 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/tss_hasher_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS::Hasher do 4 | describe 'HASHES' do 5 | it 'must return a correct result' do 6 | TSS::Hasher::HASHES.must_equal({ 'NONE' => { code: 0, bytesize: 0, hasher: nil }, 7 | 'SHA1' => { code: 1, bytesize: 20, hasher: Digest::SHA1 }, 8 | 'SHA256' => { code: 2, bytesize: 32, hasher: Digest::SHA256 } }) 9 | end 10 | end 11 | 12 | describe 'key_from_code for 0' do 13 | it 'must return NONE' do 14 | TSS::Hasher.key_from_code(0).must_equal 'NONE' 15 | end 16 | end 17 | 18 | describe 'key_from_code for 1' do 19 | it 'must return SHA1' do 20 | TSS::Hasher.key_from_code(1).must_equal 'SHA1' 21 | end 22 | end 23 | 24 | describe 'key_from_code for 2' do 25 | it 'must return SHA256' do 26 | TSS::Hasher.key_from_code(2).must_equal 'SHA256' 27 | end 28 | end 29 | 30 | describe 'key_from_code for unknown code' do 31 | it 'must return nil' do 32 | TSS::Hasher.key_from_code(99).must_be_nil 33 | end 34 | end 35 | 36 | describe 'code for NONE' do 37 | it 'must return 0' do 38 | TSS::Hasher.code('NONE').must_equal 0 39 | end 40 | end 41 | 42 | describe 'code for SHA1' do 43 | it 'must return 1' do 44 | TSS::Hasher.code('SHA1').must_equal 1 45 | end 46 | end 47 | 48 | describe 'code for SHA256' do 49 | it 'must return 2' do 50 | TSS::Hasher.code('SHA256').must_equal 2 51 | end 52 | end 53 | 54 | describe 'codes' do 55 | it 'must return a correct result' do 56 | TSS::Hasher.codes.must_equal [0, 1, 2] 57 | end 58 | end 59 | 60 | describe 'codes_without_none' do 61 | it 'must return a correct result' do 62 | TSS::Hasher.codes_without_none.must_equal [1, 2] 63 | end 64 | end 65 | 66 | describe 'bytesize for NONE' do 67 | it 'must return 0' do 68 | TSS::Hasher.bytesize('NONE').must_equal 0 69 | end 70 | end 71 | 72 | describe 'bytesize for SHA1' do 73 | it 'must return 20' do 74 | TSS::Hasher.bytesize('SHA1').must_equal 20 75 | end 76 | end 77 | 78 | describe 'bytesize for SHA256' do 79 | it 'must return 32' do 80 | TSS::Hasher.bytesize('SHA256').must_equal 32 81 | end 82 | end 83 | 84 | describe 'hash NONE to hex_string' do 85 | it 'must return an empty string' do 86 | TSS::Hasher.hex_string('NONE', 'a string to hash').must_equal '' 87 | end 88 | end 89 | 90 | describe 'hash SHA1 to hex_string' do 91 | it 'must return a SHA1 hash' do 92 | TSS::Hasher.hex_string('SHA1', 'a string to hash').must_equal '8632b08226eb79cf5c827bb4708a2615b059a201' 93 | end 94 | end 95 | 96 | describe 'hash SHA256 to hex_string' do 97 | it 'must return a SHA256 hash' do 98 | TSS::Hasher.hex_string('SHA256', 'a string to hash').must_equal '187c7a6cd902bc520f03015550d735a8e24f00f888c0328c9b6bcbd2d7c90cf7' 99 | end 100 | end 101 | 102 | describe 'hash NONE to byte_string' do 103 | it 'must return an empty string' do 104 | TSS::Hasher.byte_string('NONE', 'a string to hash').must_equal '' 105 | end 106 | end 107 | 108 | describe 'hash SHA1 to byte_string' do 109 | it 'must return a SHA1 bytestring' do 110 | # use .unpack('H*') to convert from bytestring to Hex since tests 111 | # sometimes return different formatted bytestrings 112 | TSS::Hasher.byte_string('SHA1', 'a string to hash').unpack('H*').must_equal ['8632b08226eb79cf5c827bb4708a2615b059a201'] 113 | end 114 | end 115 | 116 | describe 'hash SHA256 to byte_string' do 117 | it 'must return a SHA256 bytestring' do 118 | # use .unpack('H*') to convert from bytestring to Hex since tests 119 | # sometimes return different formatted bytestrings 120 | TSS::Hasher.byte_string('SHA256', 'a string to hash').unpack('H*').must_equal ['187c7a6cd902bc520f03015550d735a8e24f00f888c0328c9b6bcbd2d7c90cf7'] 121 | end 122 | end 123 | 124 | describe 'hash NONE to byte_array' do 125 | it 'must return an empty array' do 126 | TSS::Hasher.byte_array('NONE', 'a string to hash').must_equal [] 127 | end 128 | end 129 | 130 | describe 'hash SHA1 to byte_array' do 131 | it 'must return an array of bytes' do 132 | TSS::Hasher.byte_array('SHA1', 'a string to hash').must_equal [134, 50, 176, 130, 38, 235, 121, 207, 92, 130, 123, 180, 112, 138, 38, 21, 176, 89, 162, 1] 133 | end 134 | end 135 | 136 | describe 'hash SHA256 to byte_array' do 137 | it 'must return an array of bytes' do 138 | TSS::Hasher.byte_array('SHA256', 'a string to hash').must_equal [24, 124, 122, 108, 217, 2, 188, 82, 15, 3, 1, 85, 80, 215, 53, 168, 226, 79, 0, 248, 136, 192, 50, 140, 155, 107, 203, 210, 215, 201, 12, 247] 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/tss/cli_combine.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module TSS 4 | class CLI < Thor 5 | include Thor::Actions 6 | 7 | method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read shares from' 8 | method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the recovered secret to' 9 | method_option :padding, :type => :boolean, :default => true, :desc => 'Whether PKCS#7 padding is expected in the secret and should be removed' 10 | 11 | desc 'combine', 'Enter shares to recover a split secret' 12 | 13 | long_desc <<-LONGDESC 14 | `tss combine` will take as input a number of shares that were generated 15 | using the `tss split` command. Shares can be provided 16 | using one of three different input methods; STDIN, a path to a file, 17 | or when prompted for them interactively. 18 | 19 | You can enter shares one by one, or from a text file of shares. If the 20 | shares are successfully combined to recover a secret, the secret and 21 | some metadata will be written to STDOUT or a file. 22 | 23 | Optional Params: 24 | 25 | input_file : 26 | Provide the path to a file containing shares. Any lines in the file not 27 | beginning with `tss~` and matching the pattern expected for shares will be 28 | ignored. Leading and trailing whitespace or any other text will be ignored 29 | as long as the shares are each on a line by themselves. 30 | 31 | output_file : 32 | Provide the path to a file where you would like to write any recovered 33 | secret, instead of to STDOUT. When this option is provided the output file 34 | will contain only the secret itself. Some metadata, including the hash digest 35 | of the secret, will be written to STDOUT. Running `sha1sum` or `sha256sum` 36 | on the output file should provide a digest matching that of the secret 37 | when it was originally split. 38 | 39 | padding/no-padding : 40 | Whether or not PKCS#7 padding should be removed from secret data. By 41 | default padding is applied to shared secrets when created. Turning this 42 | off may be helpful if you need to combine shares created with a third-party 43 | library. 44 | 45 | Example w/ options: 46 | 47 | $ tss combine -I shares.txt -O secret.txt 48 | LONGDESC 49 | 50 | # rubocop:disable CyclomaticComplexity 51 | def combine 52 | log('Starting combine') 53 | log("options : #{options.inspect}") 54 | shares = [] 55 | 56 | # There are three ways to pass in shares. STDIN, by specifying 57 | # `--input-file`, and in response to being prompted and entering shares 58 | # line by line. 59 | 60 | # STDIN 61 | # Usage : echo 'foo bar baz' | bundle exec bin/tss split | bundle exec bin/tss combine 62 | unless STDIN.tty? 63 | $stdin.each_line do |line| 64 | line = line.strip 65 | exit_if_binary!(line) 66 | 67 | if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE) 68 | shares << line 69 | else 70 | log("Skipping invalid share file line : #{line}") 71 | end 72 | end 73 | end 74 | 75 | # Read from an Input File 76 | if STDIN.tty? && options[:input_file] 77 | log("Input file specified : #{options[:input_file]}") 78 | 79 | if File.exist?(options[:input_file]) 80 | log("Input file found : #{options[:input_file]}") 81 | 82 | file = File.open(options[:input_file], 'r') 83 | while !file.eof? 84 | line = file.readline.strip 85 | exit_if_binary!(line) 86 | 87 | if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE) 88 | shares << line 89 | else 90 | log("Skipping invalid share file line : #{line}") 91 | end 92 | end 93 | else 94 | err("Filename '#{options[:input_file]}' does not exist.") 95 | exit(1) 96 | end 97 | end 98 | 99 | # Enter shares in response to a prompt. 100 | if STDIN.tty? && options[:input_file].blank? 101 | say('Enter shares, one per line, and a dot (.) on a line by itself to finish :') 102 | last_ans = nil 103 | until last_ans == '.' 104 | last_ans = ask('share> ').strip 105 | exit_if_binary!(last_ans) 106 | 107 | if last_ans != '.' && last_ans.start_with?('tss~') && last_ans.match(Util::HUMAN_SHARE_RE) 108 | shares << last_ans 109 | end 110 | end 111 | end 112 | 113 | begin 114 | sec = TSS.combine(shares: shares, padding: options[:padding]) 115 | 116 | say('') 117 | say('RECOVERED SECRET METADATA') 118 | say('*************************') 119 | say("hash : #{sec[:hash]}") 120 | say("hash_alg : #{sec[:hash_alg]}") 121 | say("identifier : #{sec[:identifier]}") 122 | say("process_time : #{sec[:process_time]}ms") 123 | say("threshold : #{sec[:threshold]}") 124 | 125 | # Write the secret to a file or STDOUT. The hash of the file checked 126 | # using sha1sum or sha256sum should match the hash of the original 127 | # secret when it was split. 128 | if options[:output_file].present? 129 | say("secret file : [#{options[:output_file]}]") 130 | File.open(options[:output_file], 'w'){ |somefile| somefile.puts sec[:secret] } 131 | else 132 | say('secret :') 133 | say(sec[:secret]) 134 | end 135 | rescue TSS::Error => e 136 | err("#{e.class} : #{e.message}") 137 | end 138 | end 139 | # rubocop:enable CyclomaticComplexity 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /test/tss_python_interop_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # Test interoperability with Python TSS library shares: 4 | # See : https://github.com/seb-m/tss 5 | describe TSS do 6 | 7 | # Python Code: 8 | # >>> secret = 'I love Python secrets too!' 9 | # >>> shares = tss.share_secret(2, 3, secret, 'id', tss.Hash.NONE) 10 | # 11 | describe 'decoding the tss Python module shares with Hash NONE' do 12 | it 'must return the expected secret' do 13 | secret = 'I love Python secrets too!' 14 | 15 | # Python tss shares. Only thing changed was to replace single quotes with double quotes. 16 | shares = ["id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x1b\x01\xe15\xaf:\xe1L\xcc\xa7U$\xaf&\x9b\xe47\x89\x10\xfb\x1aY\xa2hj\x17\xfd\xbf", 17 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x1b\x02\x02\n\xf1\xc5C7\xe3\xa5!\xd4\xfd\xfd\x9f\xb3\xfb\xa6\x85{\x9b.\xca\xb0H\x9fP\x06", 18 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x1b\x03\xaa\x1f2\x90\xd4\x1e\x0fR\r\x84:\xb4jw\xbfJ\xf6\xf2\xe4\x03\x1b\xf8V\xe7\xc2\x98"] 19 | 20 | header = TSS::Util.extract_share_header(shares.first) 21 | header[:identifier].must_equal 'id' 22 | header[:hash_id].must_equal 0 23 | header[:threshold].must_equal 2 24 | header[:share_len].must_equal 27 25 | recovered_secret = TSS::Combiner.new(shares: shares, padding: false).combine 26 | recovered_secret[:secret].must_equal secret 27 | recovered_secret[:secret].encoding.name.must_equal 'UTF-8' 28 | end 29 | end 30 | 31 | # Python Code: 32 | # >>> secret = 'I love Python secrets too!' 33 | # >>> shares = tss.share_secret(2, 3, secret, 'id', tss.Hash.SHA1) 34 | # 35 | describe 'decoding the tss Python module shares with Hash SHA1' do 36 | it 'must return the expected secret' do 37 | secret = 'I love Python secrets too!' 38 | 39 | # Python tss shares. Only thing changed was to replace single quotes with double quotes. 40 | shares = ["id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00/\x01\xb9w.d\xbf\x8d\xfa\x16\xa1\xdd~`%7Km\rN\x0b\xbd)\\\xea\x1c\xc6|\x9c\\\xb1\xff\xea@\x07d\x8f\xe06\xf9B5\xf6\xed\x93\x11k\xe0", 41 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00/\x02\xb2\x8e\xe8y\xff\xae\x8f\xdc\xd2=Dq\xf8\x0e\x03u\xbf\n\xb9\xfd\xc7\xd8S\x89&\x9b\xc1!\x82\xeb\xcfZ\x18\x1bS\xd0<\xb9]\xfc\x85\x9b\xe1;\xc2\xc1", 42 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00/\x03B\xd9\xaar6FU\x9a\n\x94R~\xb3\x19;}\xd16\xd74\x9d\xa4\xcd\xfa\x8f\xc6\x03\n\x93\xe7%\xa5\xe4\xc7\xee\xc0:p\xa1\xbb]@\xcf\xd4\xa5\xde"] 43 | 44 | header = TSS::Util.extract_share_header(shares.first) 45 | header[:identifier].must_equal 'id' 46 | header[:hash_id].must_equal 1 47 | header[:threshold].must_equal 2 48 | header[:share_len].must_equal 47 49 | recovered_secret = TSS::Combiner.new(shares: shares, padding: false).combine 50 | recovered_secret[:secret].must_equal secret 51 | recovered_secret[:secret].encoding.name.must_equal 'UTF-8' 52 | end 53 | end 54 | 55 | # Python Code: 56 | # >>> secret = 'I love Python secrets too!' 57 | # >>> shares = tss.share_secret(2, 3, secret, 'id', tss.Hash.SHA256) 58 | # 59 | describe 'decoding the tss Python module shares with Hash SHA256' do 60 | it 'must return the expected secret' do 61 | secret = 'I love Python secrets too!' 62 | 63 | # Python tss shares. Only thing changed was to replace single quotes with double quotes. 64 | # had to \ escape a double-quote in the first share 65 | shares = ["id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00;\x01\xd5\xefx=\x1fw\xf0\xe1\x82B\xc4\x00\xc1\"&\xd8\xf8\x0b\xbc\xa0\xae\x14\xea\xab\x8cI\x90vR\xea\xc1-p\x87<\xb5q\x87<>\xe2\xd9\xee\'\xbb\xf7\xbb\xe9\x9b\'\xb9C1\x01HuK\xad", 66 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00;\x02j\xa5D\xcb\xa4A\x9b)\x94\x18+\xb1+$\xd9\x04N\x80\xcc\xc7\xd2HS\xfc\xb2\xf10\x0e\x85\xec\x9c\xf5\xc7L\x81\xe1\xe4\xae=G\xc4G\xe5\xd5\x9f\xe1n\xbeo\xd3\xda\xfan\xf2\xb2\xdd>\xd0", 67 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00;\x03\xf6jP\x99\xcdSK\x98o.\x87\xde\x84&\x8c\xb9\xd5\xf9\x15\x13\x0f|\xcd8Q\x99P&\xc8\xee^\xbd\xaa\xfc\xea$\x97@\xcb\x99/\xc4\x15r\x83\x1a\xd4z\xcav\xfbd[\xa3\xe4L\xe4\xfb"] 68 | header = TSS::Util.extract_share_header(shares.first) 69 | header[:identifier].must_equal 'id' 70 | header[:hash_id].must_equal 2 71 | header[:threshold].must_equal 2 72 | header[:share_len].must_equal 59 73 | recovered_secret = TSS::Combiner.new(shares: shares, padding: false).combine 74 | recovered_secret[:secret].must_equal secret 75 | recovered_secret[:secret].encoding.name.must_equal 'UTF-8' 76 | end 77 | end 78 | 79 | # Python Code: 80 | # >>> secret = 'I love Python secrets too!' 81 | # >>> shares = tss.share_secret(2, 3, secret, 'id', tss.Hash.SHA256) 82 | # 83 | describe 'decoding the tss Python module shares with multi-byte unicode secret and Hash SHA256' do 84 | it 'must return the expected secret' do 85 | secret = 'I love secrets with multi-byte unicode characters ½ ♥ 💩' 86 | 87 | # Python tss shares. Only thing changed was to replace single quotes with double quotes. 88 | # had to \ escape a double-quote in the some shares 89 | shares = [ "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00^\x01\x82q\xb4\x83\xa1\xdf\x9e$\xf53\xfaQ\x92\x8c#UbV\x11>\xbbo\x9c\x8c\xd7\xc9K\xfb\xab\xf3\xd4\xbav[l\xd5A\x94&?\xab\xca\x1d2\xa2\x94f\xb8\x1a\xd9\xc8;\xba\xe6\xdc\xeb\xb6\xa7\x85\xca\xce6f5Jx\xf4\xb3&\rt\x9dr\xbd$\x82c\xa0\x9cS\xa4\x12\x91XH\xdd\x15r\xd5\x11\x8b\x8cs", 90 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00^\x02\xc4\x82\xc7\xac\xc3\nG\xdd^\xc3y\r\xa3\x96&3\x7f0\x9a\x1c\xdaA\x97\x9f\x0e\xfe0f\xd1R\xd3\xf0^\r}\x00.\x9c,\xdb\xf5,\xac\xc7\xfa\xafc\xfd\xa1\xc9\xd6\xaa\x0f\xea\x139\x17^\xab\"g\x0e\xd9e\x16\xcf\x93\xf3\xe0f\xacc\xda\x82\xfd7M\xa7\x18:\xa1\x19\x82\x93\xd3\xf1#\x91:v\xc6\\?", 91 | "id\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00^\x03\x0f\xd3\x1f@\x14\xb0\xf9\x8a\xce\x93\xf19Ei%\x11t\x12\xe3\x02\x0c[gg\xb0\x1a\x19\xe4\x0e\xc4\'?F?r\xba\x0bm*\x876\x87\xc3\x94;O`7\xc80\xdc,\x95\xeeVw\x81\t\xb1z\x00\xefEU\"\xa2G:\xa2\xb6\xe4\xc0K\x97C\xadWSd\x1d\xa2\xe9z#S\x1c19\x96\xa2\xfd\xe5\xf2"] 92 | 93 | header = TSS::Util.extract_share_header(shares.first) 94 | header[:identifier].must_equal 'id' 95 | header[:hash_id].must_equal 2 96 | header[:threshold].must_equal 2 97 | header[:share_len].must_equal 94 98 | recovered_secret = TSS::Combiner.new(shares: shares, padding: false).combine 99 | recovered_secret[:secret].must_equal secret 100 | recovered_secret[:secret].encoding.name.must_equal 'UTF-8' 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/tss/cli_split.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | 3 | module TSS 4 | class CLI < Thor 5 | include Thor::Actions 6 | 7 | method_option :threshold, :aliases => '-t', :banner => 'threshold', :type => :numeric, :desc => '# of shares, of total, required to reconstruct a secret' 8 | method_option :num_shares, :aliases => '-n', :banner => 'num_shares', :type => :numeric, :desc => '# of shares total that will be generated' 9 | method_option :identifier, :aliases => '-i', :banner => 'identifier', :type => :string, :desc => 'A unique identifier string, 0-16 Bytes, [a-zA-Z0-9.-_]' 10 | method_option :hash_alg, :aliases => '-h', :banner => 'hash_alg', :type => :string, :desc => 'A hash type for verification, NONE, SHA1, SHA256' 11 | method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'HUMAN', :desc => 'Share output format, BINARY or HUMAN' 12 | method_option :padding, :type => :boolean, :default => true, :desc => 'Whether to apply PKCS#7 padding to the secret' 13 | method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read the secret from' 14 | method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the shares to' 15 | 16 | desc 'split', 'Split a secret into shares that can be used to re-create the secret' 17 | 18 | long_desc <<-LONGDESC 19 | `tss split` will generate a set of Threshold Secret Sharing shares from 20 | a SECRET provided. A secret to be split can be provided using one of three 21 | different input methods; STDIN, a path to a file, or when prompted 22 | for it interactively. In all cases the secret should be UTF-8 or 23 | US-ASCII encoded text and be no larger than #{TSS::MAX_UNPADDED_SECRET_SIZE} 24 | bytes. 25 | 26 | Optional Params: 27 | 28 | num_shares : 29 | The number of total shares that will be generated. 30 | 31 | threshold : 32 | The threshold is the number of shares required to 33 | recreate a secret. This is always a subset of the total 34 | shares. 35 | 36 | identifier : 37 | A unique identifier string that will be attached 38 | to each share. It can be 0-16 Bytes long and use the 39 | characters [a-zA-Z0-9.-_] 40 | 41 | hash_alg : 42 | One of NONE, SHA1, SHA256. The algorithm to use for a one-way hash of the secret that will be split along with the secret. 43 | 44 | padding/no-padding : 45 | Whether to apply PKCS#7 padding to secret. By default padding is applied. Turning this off may be helpful if you need to interoperate with a third party library. 46 | 47 | format : 48 | Whether to output the shares as a binary octet string (RTSS), or as more human friendly URL safe Base 64 encoded text with some metadata. 49 | 50 | input_file : 51 | Provide the path to a file containing UTF-8 or US-ASCII text, the contents of which will be used as the secret. 52 | 53 | output_file : 54 | Provide the path to a file where you would like to write the shares, one per line, instead of to STDOUT. 55 | 56 | Example w/ options: 57 | 58 | $ tss split -t 3 -n 6 -i abc123 -h SHA256 -f HUMAN 59 | 60 | Enter your secret: 61 | 62 | secret > my secret 63 | 64 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEB113xpF37jGHm5QGhXKD8mgK2897MIQkSWri6ksNnAODn0efXznuBsSUnhlDIqQFU 65 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEC4tZegQrC3z6-02er3FZaWMadtlvxPb1EI_FNjG0dFrcdEDj4V7Cmcw___SesJHHP 66 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEDWPKPVjJaITosPGAMhvCgxCBB9uptl2h5UPngnw71V7Z9T-pnxiLKIfgUbRqyBrv- 67 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEExY3ti8ckAIQC02OKCrpEVVnUmyg3NXO9oG3PNw3PlgbbKdFRi9gBCNN_tjkhT3An 68 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEFf6k8XP-8_oCQPGQtUBy-yb8I25mrn6aA02ViJG4n1we7dgPOGkptWiSUJgQ_bboW 69 | tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEGSiKTeaiFrd_ICgIn0OoYC3sjnhyWgxLWqiyVOsBdwVBBt9zhg4FKmA5MXXNb4MqN 70 | 71 | LONGDESC 72 | 73 | # rubocop:disable CyclomaticComplexity 74 | def split 75 | log('Starting split') 76 | log('options : ' + options.inspect) 77 | args = {} 78 | 79 | # There are three ways to pass in the secret. STDIN, by specifying 80 | # `--input-file`, and after being prompted and entering your secret 81 | # line by line. 82 | 83 | # STDIN 84 | # Usage : echo 'foo bar baz' | bundle exec bin/tss split 85 | unless STDIN.tty? 86 | secret = $stdin.read 87 | exit_if_binary!(secret) 88 | end 89 | 90 | # Read from an Input File 91 | if STDIN.tty? && options[:input_file].present? 92 | log("Input file specified : #{options[:input_file]}") 93 | 94 | if File.exist?(options[:input_file]) 95 | log("Input file found : #{options[:input_file]}") 96 | secret = File.open(options[:input_file], 'r'){ |file| file.read } 97 | exit_if_binary!(secret) 98 | else 99 | err("Filename '#{options[:input_file]}' does not exist.") 100 | exit(1) 101 | end 102 | end 103 | 104 | # Enter a secret in response to a prompt. 105 | if STDIN.tty? && options[:input_file].blank? 106 | say('Enter your secret, enter a dot (.) on a line by itself to finish :') 107 | last_ans = nil 108 | secret = [] 109 | 110 | while last_ans != '.' 111 | last_ans = ask('secret > ') 112 | secret << last_ans unless last_ans == '.' 113 | end 114 | 115 | # Strip whitespace from the leading and trailing edge of the secret. 116 | # Separate each line of the secret with newline, and add a trailing 117 | # newline so the hash of a secret when it is created will match 118 | # the hash of a file output when recombinging shares. 119 | secret = secret.join("\n").strip + "\n" 120 | exit_if_binary!(secret) 121 | end 122 | 123 | args[:secret] = secret 124 | args[:threshold] = options[:threshold] if options[:threshold] 125 | args[:num_shares] = options[:num_shares] if options[:num_shares] 126 | args[:identifier] = options[:identifier] if options[:identifier] 127 | args[:hash_alg] = options[:hash_alg] if options[:hash_alg] 128 | args[:format] = options[:format] if options[:format] 129 | args[:padding] = options[:padding] 130 | 131 | begin 132 | log("Calling : TSS.split(#{args.inspect})") 133 | shares = TSS.split(args) 134 | 135 | if options[:output_file].present? 136 | file_header = "# THRESHOLD SECRET SHARING SHARES\n" 137 | file_header << "# #{Time.now.utc.iso8601}\n" 138 | file_header << "# https://github.com/grempe/tss-rb\n" 139 | file_header << "\n\n" 140 | 141 | File.open(options[:output_file], 'w') do |somefile| 142 | somefile.puts file_header + shares.join("\n") 143 | end 144 | log("Process complete : Output file written : #{options[:output_file]}") 145 | else 146 | $stdout.puts shares.join("\n") 147 | log('Process complete') 148 | end 149 | rescue TSS::Error => e 150 | err("#{e.class} : #{e.message}") 151 | end 152 | end 153 | # rubocop:enable CyclomaticComplexity 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/tss/tss.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/object/blank' 2 | require 'digest' 3 | require 'base64' 4 | require 'sysrandom/securerandom' 5 | require 'binary_struct' 6 | require 'tss/version' 7 | require 'tss/util' 8 | require 'tss/hasher' 9 | require 'tss/splitter' 10 | require 'tss/combiner' 11 | 12 | # Threshold Secret Sharing 13 | # 14 | # @author Glenn Rempe 15 | module TSS 16 | include Contracts::Core 17 | C = Contracts 18 | 19 | # Defined in TSS spec, two less than 2^16 20 | MAX_SECRET_SIZE = 64_534 21 | 22 | # Max size minus up to 16 bytes PKCS#7 padding 23 | # and 32 bytes of cryptographic hash 24 | MAX_UNPADDED_SECRET_SIZE = MAX_SECRET_SIZE - 48 25 | 26 | # When applying PKCS#7 padding, what block size in bytes should be used 27 | PADDING_BLOCK_SIZE_BYTES = 16 28 | 29 | # An unexpected error has occurred. 30 | class Error < StandardError; end 31 | 32 | # An argument provided is of the wrong type, or has an invalid value. 33 | class ArgumentError < TSS::Error; end 34 | 35 | # A secret was attmepted to be recovered, but failed due to invalid shares. 36 | class NoSecretError < TSS::Error; end 37 | 38 | # A secret was attempted to be recovered, but failed due to an invalid verifier hash. 39 | class InvalidSecretHashError < TSS::Error; end 40 | 41 | # Threshold Secret Sharing (TSS) provides a way to generate N shares 42 | # from a value, so that any M of those shares can be used to 43 | # reconstruct the original value, but any M-1 shares provide no 44 | # information about that value. This method can provide shared access 45 | # control on key material and other secrets that must be strongly 46 | # protected. 47 | # 48 | # @param [Hash] opts the options to create a message with. 49 | # @option opts [String] :secret takes a String (UTF-8 or US-ASCII encoding) with a length between 1..65_534 50 | # @option opts [Integer] :threshold (3) The number of shares (M) that will be required to recombine the 51 | # secret. Must be a value between 1..255 inclusive. Defaults to a threshold of 3 shares. 52 | # @option opts [Integer] :num_shares (5) The total number of shares (N) that will be created. Must be 53 | # a value between the `threshold` value (M) and 255 inclusive. 54 | # The upper limit is particular to the TSS algorithm used. 55 | # @option opts [String] :identifier (SecureRandom.hex(8)) A 1-16 byte String limited to the characters 0-9, a-z, A-Z, 56 | # the dash (-), the underscore (_), and the period (.). The identifier will 57 | # be embedded in each the binary header of each share and should not reveal 58 | # anything about the secret. 59 | # 60 | # If the arg is nil it defaults to the value of `SecureRandom.hex(8)` 61 | # which returns a random 16 Byte string which represents a Base10 decimal 62 | # between 1 and 18446744073709552000. 63 | # @option opts [String] :hash_alg ('SHA256') The one-way hash algorithm that will be used to verify the 64 | # secret returned by a later recombine operation is identical to what was 65 | # split. This value will be concatenated with the secret prior to splitting. 66 | # 67 | # The valid hash algorithm values are `NONE`, `SHA1`, and `SHA256`. Defaults 68 | # to `SHA256`. The use of `NONE` is discouraged as it does not allow those 69 | # who are recombining the shares to verify if they have in fact recovered 70 | # the correct secret. 71 | # @option opts [String] :format ('BINARY') the format of the String share output, 'BINARY' or 'HUMAN' 72 | # @option opts [Boolean] :padding Whether to apply PKCS#7 padding to secret 73 | # 74 | # @return an Array of formatted String shares 75 | # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid 76 | Contract ({ :secret => C::SecretArg, :threshold => C::Maybe[C::ThresholdArg], :num_shares => C::Maybe[C::NumSharesArg], :identifier => C::Maybe[C::IdentifierArg], :hash_alg => C::Maybe[C::HashAlgArg], :format => C::Maybe[C::FormatArg], :padding => C::Maybe[C::Bool] }) => C::ArrayOfShares 77 | def self.split(opts) 78 | TSS::Splitter.new(opts).split 79 | end 80 | 81 | # The reconstruction, or combining, operation reconstructs the secret from a 82 | # set of valid shares where the number of shares is >= the threshold when the 83 | # secret was initially split. All options are provided in a single Hash: 84 | # 85 | # @param [Hash] opts the options to create a message with. 86 | # @option opts [Array] :shares an Array of String shares to try to recombine into a secret 87 | # @option opts [Boolean] :padding Whether PKCS#7 padding is expected in the secret and should be removed 88 | # @option opts [String] :select_by ('FIRST') the method to use for selecting 89 | # shares from the Array if more then threshold shares are provided. Can be 90 | # upper case 'FIRST', 'SAMPLE', or 'COMBINATIONS'. 91 | # 92 | # If the number of shares provided as input to the secret 93 | # reconstruction operation is greater than the threshold M, then M 94 | # of those shares are selected for use in the operation. The method 95 | # used to select the shares can be chosen using the following values: 96 | # 97 | # `FIRST` : If X shares are required by the threshold and more than X 98 | # shares are provided, then the first X shares in the Array of shares provided 99 | # will be used. All others will be discarded and the operation will fail if 100 | # those selected shares cannot recreate the secret. 101 | # 102 | # `SAMPLE` : If X shares are required by the threshold and more than X 103 | # shares are provided, then X shares will be randomly selected from the Array 104 | # of shares provided. All others will be discarded and the operation will 105 | # fail if those selected shares cannot recreate the secret. 106 | # 107 | # `COMBINATIONS` : If X shares are required, and more than X shares are 108 | # provided, then all possible combinations of the threshold number of shares 109 | # will be tried to see if the secret can be recreated. 110 | # This flexibility comes with a cost. All combinations of `threshold` shares 111 | # must be generated. Due to the math associated with combinations it is possible 112 | # that the system would try to generate a number of combinations that could never 113 | # be generated or processed in many times the life of the Universe. This option 114 | # can only be used if the possible combinations for the number of shares and the 115 | # threshold needed to reconstruct a secret result in a number of combinations 116 | # that is small enough to have a chance at being processed. If the number 117 | # of combinations will be too large then the an Exception will be raised before 118 | # processing has started. 119 | # 120 | # @return a Hash containing the now combined secret and other metadata 121 | # @raise [TSS::NoSecretError] if the secret cannot be re-created from the shares provided 122 | # @raise [TSS::InvalidSecretHashError] if the embedded hash of the secret does not match the hash of the recreated secret 123 | # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid 124 | Contract ({ :shares => C::ArrayOfShares, :padding => C::Maybe[C::Bool], :select_by => C::Maybe[C::SelectByArg] }) => ({ :hash => C::Maybe[String], :hash_alg => C::HashAlgArg, :identifier => C::IdentifierArg, :process_time => C::Num, :secret => C::SecretArg, :threshold => C::ThresholdArg }) 125 | def self.combine(opts) 126 | TSS::Combiner.new(opts).combine 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/tss_splitter_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS::Splitter do 4 | before do 5 | @s = TSS::Splitter.new(secret: 'my secret') 6 | end 7 | 8 | describe 'secret' do 9 | it 'must raise an error if nil' do 10 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: nil).split } 11 | end 12 | 13 | it 'must raise an error if not a string' do 14 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 123).split } 15 | end 16 | 17 | it 'must raise an error if size < 1' do 18 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: '').split } 19 | end 20 | 21 | it 'must raise an error if size > TSS::MAX_UNPADDED_SECRET_SIZE' do 22 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a' * (TSS::MAX_UNPADDED_SECRET_SIZE + 1)).split } 23 | end 24 | 25 | it 'must raise an error if String encoding is not UTF-8' do 26 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a'.force_encoding('ISO-8859-1')).split } 27 | end 28 | 29 | it 'must return an Array of default shares with US-ASCII encoded secret' do 30 | s = TSS::Splitter.new(secret: 'a'.force_encoding('US-ASCII')).split 31 | assert_kind_of Array, s 32 | assert s.size.must_equal 5 33 | assert_kind_of String, s.first 34 | end 35 | 36 | it 'must return an Array of default shares with a min size secret' do 37 | s = TSS::Splitter.new(secret: 'a').split 38 | assert_kind_of Array, s 39 | assert s.size.must_equal 5 40 | assert_kind_of String, s.first 41 | end 42 | 43 | it 'must return an Array of default shares with a max size secret' do 44 | s = TSS::Splitter.new(secret: 'a' * TSS::MAX_UNPADDED_SECRET_SIZE).split 45 | assert_kind_of Array, s 46 | assert s.size.must_equal 5 47 | assert_kind_of String, s.first 48 | end 49 | end 50 | 51 | describe 'threshold' do 52 | it 'must raise an error if size < 1' do 53 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', threshold: 0).split } 54 | end 55 | 56 | it 'must raise an error if size > 255' do 57 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', threshold: 256).split } 58 | end 59 | 60 | it 'must return an Array of default shares with a min size threshold' do 61 | s = TSS::Splitter.new(secret: 'a', threshold: 1).split 62 | assert_kind_of Array, s 63 | assert s.size.must_equal 5 64 | secret = TSS::Combiner.new(shares: s.sample(1)).combine 65 | assert_kind_of String, secret[:secret] 66 | secret[:threshold].must_equal 1 67 | end 68 | 69 | it 'must return an Array of default threshold (3) shares with no threshold' do 70 | s = TSS::Splitter.new(secret: 'a').split 71 | assert_kind_of Array, s 72 | assert s.size.must_equal 5 73 | secret = TSS::Combiner.new(shares: s.sample(3)).combine 74 | assert_kind_of String, secret[:secret] 75 | secret[:threshold].must_equal 3 76 | end 77 | 78 | it 'must return an Array of default shares with a max size threshold' do 79 | s = TSS::Splitter.new(secret: 'a', threshold: 255, num_shares: 255).split 80 | assert_kind_of Array, s 81 | assert s.size.must_equal 255 82 | secret = TSS::Combiner.new(shares: s.sample(255)).combine 83 | assert_kind_of String, secret[:secret] 84 | secret[:threshold].must_equal 255 85 | end 86 | end 87 | 88 | describe 'num_shares' do 89 | it 'must raise an error if size < 1' do 90 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', num_shares: 0).split } 91 | end 92 | 93 | it 'must raise an error if size > 255' do 94 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', num_shares: 256).split } 95 | end 96 | 97 | it 'must raise an error if num_shares < threshold' do 98 | assert_raises(TSS::ArgumentError) { TSS::Splitter.new(secret: 'a', threshold: 3, num_shares: 2).split } 99 | end 100 | 101 | it 'must return an Array of shares with a min size' do 102 | s = TSS::Splitter.new(secret: 'a', threshold: 1, num_shares: 1).split 103 | assert_kind_of Array, s 104 | assert s.size.must_equal 1 105 | secret = TSS::Combiner.new(shares: s.sample(1)).combine 106 | assert_kind_of String, secret[:secret] 107 | secret[:threshold].must_equal 1 108 | end 109 | 110 | it 'must return an Array of threshold (5) shares with no num_shares' do 111 | s = TSS::Splitter.new(secret: 'a').split 112 | assert_kind_of Array, s 113 | assert s.size.must_equal 5 114 | secret = TSS::Combiner.new(shares: s).combine 115 | assert_kind_of String, secret[:secret] 116 | secret[:threshold].must_equal 3 117 | end 118 | 119 | it 'must return an Array of shares with a max size' do 120 | s = TSS::Splitter.new(secret: 'a', threshold: 255, num_shares: 255).split 121 | assert_kind_of Array, s 122 | assert s.size.must_equal 255 123 | secret = TSS::Combiner.new(shares: s.sample(255)).combine 124 | assert_kind_of String, secret[:secret] 125 | secret[:threshold].must_equal 255 126 | end 127 | end 128 | 129 | describe 'identifier' do 130 | it 'must raise an error if size > 16' do 131 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', identifier: 'a'*17).split } 132 | end 133 | 134 | it 'must raise an error if non-whitelisted characters' do 135 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', identifier: '&').split } 136 | end 137 | 138 | it 'must raise an error if passed an empty string' do 139 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', identifier: '').split } 140 | end 141 | 142 | it 'must accept a String with all whitelisted characters' do 143 | id = 'abc-ABC_0.9' 144 | s = TSS::Splitter.new(secret: 'a', identifier: id).split 145 | secret = TSS::Combiner.new(shares: s).combine 146 | secret[:identifier].must_equal id 147 | end 148 | 149 | it 'must accept a 16 Byte String' do 150 | id = SecureRandom.hex(8) 151 | s = TSS::Splitter.new(secret: 'a', identifier: id).split 152 | secret = TSS::Combiner.new(shares: s).combine 153 | secret[:identifier].must_equal id 154 | end 155 | end 156 | 157 | describe 'hash_alg' do 158 | it 'must raise an error if empty' do 159 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', hash_alg: '').split } 160 | end 161 | 162 | it 'must raise an error if value is not in the Enum' do 163 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', hash_alg: 'foo').split } 164 | end 165 | 166 | it 'must accept an NONE String' do 167 | s = TSS::Splitter.new(secret: 'a', hash_alg: 'NONE').split 168 | secret = TSS::Combiner.new(shares: s).combine 169 | secret[:hash_alg].must_equal 'NONE' 170 | end 171 | 172 | it 'must accept an SHA1 String' do 173 | s = TSS::Splitter.new(secret: 'a', hash_alg: 'SHA1').split 174 | secret = TSS::Combiner.new(shares: s).combine 175 | secret[:hash_alg].must_equal 'SHA1' 176 | end 177 | 178 | it 'must accept an SHA256 String' do 179 | s = TSS::Splitter.new(secret: 'a', hash_alg: 'SHA256').split 180 | secret = TSS::Combiner.new(shares: s).combine 181 | secret[:hash_alg].must_equal 'SHA256' 182 | end 183 | end 184 | 185 | describe 'format' do 186 | it 'must raise an error if a an invalid value is passed' do 187 | assert_raises(ParamContractError) { TSS::Splitter.new(secret: 'a', format: 'alien').split } 188 | end 189 | 190 | it 'must default to HUMAN format output when no param is passed' do 191 | s = TSS::Splitter.new(secret: 'a').split 192 | s.first.must_match(/^tss~/) 193 | end 194 | 195 | it 'must default to HUMAN format output when nil is passed' do 196 | s = TSS::Splitter.new(secret: 'a', format: nil).split 197 | s.first.must_match(/^tss~/) 198 | end 199 | 200 | it 'must accept a HUMAN option' do 201 | s = TSS::Splitter.new(secret: 'a', format: 'HUMAN').split 202 | s.first.encoding.to_s.must_equal 'UTF-8' 203 | s.first.must_match(/^tss~/) 204 | secret = TSS::Combiner.new(shares: s).combine 205 | secret[:secret].must_equal 'a' 206 | end 207 | 208 | it 'must accept a BINARY option' do 209 | s = TSS::Splitter.new(secret: 'a', format: 'BINARY').split 210 | s.first.encoding.to_s.must_equal 'ASCII-8BIT' 211 | secret = TSS::Combiner.new(shares: s).combine 212 | secret[:secret].must_equal 'a' 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/tss/splitter.rb: -------------------------------------------------------------------------------- 1 | module TSS 2 | # Warning, you probably don't want to use this directly. Instead 3 | # see the TSS module. 4 | # 5 | # TSS::Splitter has responsibility for splitting a secret into an Array of String shares. 6 | class Splitter 7 | include Contracts::Core 8 | include Util 9 | 10 | C = Contracts 11 | 12 | attr_reader :secret, :threshold, :num_shares, :identifier, :hash_alg, :format, :padding 13 | 14 | Contract ({ :secret => C::SecretArg, :threshold => C::Maybe[C::ThresholdArg], :num_shares => C::Maybe[C::NumSharesArg], :identifier => C::Maybe[C::IdentifierArg], :hash_alg => C::Maybe[C::HashAlgArg], :format => C::Maybe[C::FormatArg], :padding => C::Maybe[C::Bool] }) => C::Any 15 | def initialize(opts = {}) 16 | @secret = opts.fetch(:secret) 17 | @threshold = opts.fetch(:threshold, 3) 18 | @num_shares = opts.fetch(:num_shares, 5) 19 | @identifier = opts.fetch(:identifier, SecureRandom.hex(8)) 20 | @hash_alg = opts.fetch(:hash_alg, 'SHA256') 21 | @format = opts.fetch(:format, 'HUMAN') 22 | @padding = opts.fetch(:padding, true) 23 | end 24 | 25 | SHARE_HEADER_STRUCT = BinaryStruct.new([ 26 | 'a16', :identifier, # String, 16 Bytes, arbitrary binary string (null padded, count is width) 27 | 'C', :hash_id, 28 | 'C', :threshold, 29 | 'n', :share_len 30 | ]) 31 | 32 | # Warning, you probably don't want to use this directly. Instead 33 | # see the TSS module. 34 | # 35 | # To split a secret into a set of shares, the following 36 | # procedure, or any equivalent method, is used: 37 | # 38 | # This operation takes an octet string S, whose length is L octets, and 39 | # a threshold parameter M, and generates a set of N shares, any M of 40 | # which can be used to reconstruct the secret. 41 | # 42 | # The secret S is treated as an unstructured sequence of octets. It is 43 | # not expected to be null-terminated. The number of octets in the 44 | # secret may be anywhere from zero up to 65,534 (that is, two less than 45 | # 2^16). 46 | # 47 | # The threshold parameter M is the number of shares that will be needed 48 | # to reconstruct the secret. This value may be any number between one 49 | # and 255, inclusive. 50 | # 51 | # The number of shares N that will be generated MUST be between the 52 | # threshold value M and 255, inclusive. The upper limit is particular 53 | # to the TSS algorithm specified in this document. 54 | # 55 | # If the operation can not be completed successfully, then an error 56 | # code should be returned. 57 | # 58 | # @return an Array of formatted String shares 59 | # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid 60 | Contract C::None => C::ArrayOfShares 61 | def split 62 | num_shares_not_less_than_threshold!(threshold, num_shares) 63 | 64 | # Append needed PKCS#7 padding to the string 65 | secret_padded = padding ? Util.pad(secret) : secret 66 | 67 | # Calculate the cryptographic hash of the secret string 68 | secret_hash = Hasher.byte_array(hash_alg, secret) 69 | 70 | # RTSS : Combine the secret with a hash digest before splitting. When 71 | # recombine the two will be separated again and the hash will be used 72 | # to validate the correct secret was returned. 73 | # secret || padding || hash(secret) 74 | secret_pad_hash_bytes = Util.utf8_to_bytes(secret_padded) + secret_hash 75 | 76 | secret_bytes_is_smaller_than_max_size!(secret_pad_hash_bytes) 77 | 78 | # For each share, a distinct Share Index is generated. Each Share 79 | # Index is an octet other than the all-zero octet. All of the Share 80 | # Indexes used during a share generation process MUST be distinct. 81 | # Each share is initialized to the Share Index associated with that 82 | # share. 83 | shares = [] 84 | (1..num_shares).each { |n| shares << [n] } 85 | 86 | # For each octet of the secret, the following steps are performed. 87 | # 88 | # An array A of M octets is created, in which the array element A[0] 89 | # contains the octet of the secret, and the array elements A[1], 90 | # ..., A[M-1] contain octets that are selected independently and 91 | # uniformly at random. 92 | # 93 | # For each share, the value of f(X,A) is 94 | # computed, where X is the Share Index of the share, and the 95 | # resulting octet is appended to the share. 96 | # 97 | # After the procedure is done, each share contains one more octet than 98 | # does the secret. The share format can be illustrated as 99 | # 100 | # +---------+---------+---------+---------+---------+ 101 | # | X | f(X,A) | f(X,B) | f(X,C) | ... | 102 | # +---------+---------+---------+---------+---------+ 103 | # 104 | # where X is the Share Index of the share, and A, B, and C are arrays 105 | # of M octets; A[0] is equal to the first octet of the secret, B[0] is 106 | # equal to the second octet of the secret, and so on. 107 | # 108 | secret_pad_hash_bytes.each do |byte| 109 | # Unpack random Byte String into Byte Array of 8 bit unsigned Integers 110 | r = SecureRandom.random_bytes(threshold - 1).unpack('C*') 111 | 112 | # Build each share one byte at a time for each byte of the secret. 113 | shares.map! { |s| s << Util.f(s[0], [byte] + r) } 114 | end 115 | 116 | # build up a common binary struct header for all shares 117 | header = share_header(identifier, hash_alg, threshold, shares.first.length) 118 | 119 | # create each binary or human share and return it. 120 | shares.map! do |s| 121 | binary = (header + s.pack('C*')).force_encoding('ASCII-8BIT') 122 | 123 | # Prefer unpadded Base64 output. The 'padding' option was 124 | # only added to Ruby later and Ruby <= 2.2.x barfs on it still. 125 | binary_b64 = begin 126 | Base64.urlsafe_encode64(binary, padding: false) 127 | rescue 128 | Base64.urlsafe_encode64(binary) 129 | end 130 | 131 | # join with URL safe '~' 132 | human = ['tss', 'v1', identifier, threshold, binary_b64].join('~') 133 | format == 'BINARY' ? binary : human 134 | end 135 | 136 | return shares 137 | end 138 | 139 | private 140 | 141 | # The num_shares must be greater than or equal to the threshold or it is invalid. 142 | # 143 | # @param threshold the threshold value 144 | # @param num_shares the num_shares value 145 | # @return returns true if num_shares is >= threshold 146 | # @raise [ParamContractError, TSS::ArgumentError] if invalid 147 | Contract C::ThresholdArg, C::NumSharesArg => C::Bool 148 | def num_shares_not_less_than_threshold!(threshold, num_shares) 149 | if num_shares < threshold 150 | raise TSS::ArgumentError, "invalid num_shares, must be >= threshold (#{threshold})" 151 | else 152 | return true 153 | end 154 | end 155 | 156 | # The total Byte size of the secret, including padding and hash, must be 157 | # less than the max allowed Byte size or it is invalid. 158 | # 159 | # @param secret_bytes the Byte Array containing the secret 160 | # @return returns true if num_shares is >= threshold 161 | # @raise [ParamContractError, TSS::ArgumentError] if invalid 162 | Contract C::ArrayOf[C::Int] => C::Bool 163 | def secret_bytes_is_smaller_than_max_size!(secret_bytes) 164 | if secret_bytes.size > TSS::MAX_SECRET_SIZE 165 | raise TSS::ArgumentError, 'invalid secret, combined padded and hashed secret is too large' 166 | else 167 | return true 168 | end 169 | end 170 | 171 | # Construct a binary share header from its constituent parts. 172 | # 173 | # @param identifier the unique identifier String 174 | # @param hash_alg the hash algorithm String 175 | # @param threshold the threshold value 176 | # @param share_len the length of the share in Bytes 177 | # @return returns an octet String of Bytes containing the binary header 178 | # @raise [ParamContractError] if invalid 179 | Contract C::IdentifierArg, C::HashAlgArg, C::ThresholdArg, C::Int => String 180 | def share_header(identifier, hash_alg, threshold, share_len) 181 | SHARE_HEADER_STRUCT.encode(identifier: identifier, 182 | hash_id: Hasher.code(hash_alg), 183 | threshold: threshold, 184 | share_len: share_len) 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /test/tss_util_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe TSS::Util do 4 | 5 | describe 'EXP constant' do 6 | it 'must match the documented table' do 7 | # copied from the spec docs directly 8 | DOC_EXP = [0x01, 0x03, 0x05, 0x0f, 0x11, 0x33, 0x55, 0xff, 9 | 0x1a, 0x2e, 0x72, 0x96, 0xa1, 0xf8, 0x13, 0x35, 10 | 0x5f, 0xe1, 0x38, 0x48, 0xd8, 0x73, 0x95, 0xa4, 11 | 0xf7, 0x02, 0x06, 0x0a, 0x1e, 0x22, 0x66, 0xaa, 12 | 0xe5, 0x34, 0x5c, 0xe4, 0x37, 0x59, 0xeb, 0x26, 13 | 0x6a, 0xbe, 0xd9, 0x70, 0x90, 0xab, 0xe6, 0x31, 14 | 0x53, 0xf5, 0x04, 0x0c, 0x14, 0x3c, 0x44, 0xcc, 15 | 0x4f, 0xd1, 0x68, 0xb8, 0xd3, 0x6e, 0xb2, 0xcd, 16 | 0x4c, 0xd4, 0x67, 0xa9, 0xe0, 0x3b, 0x4d, 0xd7, 17 | 0x62, 0xa6, 0xf1, 0x08, 0x18, 0x28, 0x78, 0x88, 18 | 0x83, 0x9e, 0xb9, 0xd0, 0x6b, 0xbd, 0xdc, 0x7f, 19 | 0x81, 0x98, 0xb3, 0xce, 0x49, 0xdb, 0x76, 0x9a, 20 | 0xb5, 0xc4, 0x57, 0xf9, 0x10, 0x30, 0x50, 0xf0, 21 | 0x0b, 0x1d, 0x27, 0x69, 0xbb, 0xd6, 0x61, 0xa3, 22 | 0xfe, 0x19, 0x2b, 0x7d, 0x87, 0x92, 0xad, 0xec, 23 | 0x2f, 0x71, 0x93, 0xae, 0xe9, 0x20, 0x60, 0xa0, 24 | 0xfb, 0x16, 0x3a, 0x4e, 0xd2, 0x6d, 0xb7, 0xc2, 25 | 0x5d, 0xe7, 0x32, 0x56, 0xfa, 0x15, 0x3f, 0x41, 26 | 0xc3, 0x5e, 0xe2, 0x3d, 0x47, 0xc9, 0x40, 0xc0, 27 | 0x5b, 0xed, 0x2c, 0x74, 0x9c, 0xbf, 0xda, 0x75, 28 | 0x9f, 0xba, 0xd5, 0x64, 0xac, 0xef, 0x2a, 0x7e, 29 | 0x82, 0x9d, 0xbc, 0xdf, 0x7a, 0x8e, 0x89, 0x80, 30 | 0x9b, 0xb6, 0xc1, 0x58, 0xe8, 0x23, 0x65, 0xaf, 31 | 0xea, 0x25, 0x6f, 0xb1, 0xc8, 0x43, 0xc5, 0x54, 32 | 0xfc, 0x1f, 0x21, 0x63, 0xa5, 0xf4, 0x07, 0x09, 33 | 0x1b, 0x2d, 0x77, 0x99, 0xb0, 0xcb, 0x46, 0xca, 34 | 0x45, 0xcf, 0x4a, 0xde, 0x79, 0x8b, 0x86, 0x91, 35 | 0xa8, 0xe3, 0x3e, 0x42, 0xc6, 0x51, 0xf3, 0x0e, 36 | 0x12, 0x36, 0x5a, 0xee, 0x29, 0x7b, 0x8d, 0x8c, 37 | 0x8f, 0x8a, 0x85, 0x94, 0xa7, 0xf2, 0x0d, 0x17, 38 | 0x39, 0x4b, 0xdd, 0x7c, 0x84, 0x97, 0xa2, 0xfd, 39 | 0x1c, 0x24, 0x6c, 0xb4, 0xc7, 0x52, 0xf6, 0x00].freeze 40 | 41 | TSS::Util::EXP.must_equal DOC_EXP 42 | end 43 | end 44 | 45 | describe 'LOG constant' do 46 | it 'must match the documented table' do 47 | # copied from the spec docs directly 48 | DOC_LOG = [0, 0, 25, 1, 50, 2, 26, 198, 49 | 75, 199, 27, 104, 51, 238, 223, 3, 50 | 100, 4, 224, 14, 52, 141, 129, 239, 51 | 76, 113, 8, 200, 248, 105, 28, 193, 52 | 125, 194, 29, 181, 249, 185, 39, 106, 53 | 77, 228, 166, 114, 154, 201, 9, 120, 54 | 101, 47, 138, 5, 33, 15, 225, 36, 55 | 18, 240, 130, 69, 53, 147, 218, 142, 56 | 150, 143, 219, 189, 54, 208, 206, 148, 57 | 19, 92, 210, 241, 64, 70, 131, 56, 58 | 102, 221, 253, 48, 191, 6, 139, 98, 59 | 179, 37, 226, 152, 34, 136, 145, 16, 60 | 126, 110, 72, 195, 163, 182, 30, 66, 61 | 58, 107, 40, 84, 250, 133, 61, 186, 62 | 43, 121, 10, 21, 155, 159, 94, 202, 63 | 78, 212, 172, 229, 243, 115, 167, 87, 64 | 175, 88, 168, 80, 244, 234, 214, 116, 65 | 79, 174, 233, 213, 231, 230, 173, 232, 66 | 44, 215, 117, 122, 235, 22, 11, 245, 67 | 89, 203, 95, 176, 156, 169, 81, 160, 68 | 127, 12, 246, 111, 23, 196, 73, 236, 69 | 216, 67, 31, 45, 164, 118, 123, 183, 70 | 204, 187, 62, 90, 251, 96, 177, 134, 71 | 59, 82, 161, 108, 170, 85, 41, 157, 72 | 151, 178, 135, 144, 97, 190, 220, 252, 73 | 188, 149, 207, 205, 55, 63, 91, 209, 74 | 83, 57, 132, 60, 65, 162, 109, 71, 75 | 20, 42, 158, 93, 86, 242, 211, 171, 76 | 68, 17, 146, 217, 35, 32, 46, 137, 77 | 180, 124, 184, 38, 119, 153, 227, 165, 78 | 103, 74, 237, 222, 197, 49, 254, 24, 79 | 13, 99, 140, 128, 192, 247, 112, 7].freeze 80 | 81 | TSS::Util::LOG.must_equal DOC_LOG 82 | end 83 | end 84 | 85 | describe 'gf256_add' do 86 | it 'must return a correct result' do 87 | # In GF256 Math A - B == A + B 88 | TSS::Util.gf256_add(15, 6).must_equal 9 89 | end 90 | end 91 | 92 | describe 'gf256_sub' do 93 | it 'must return a correct result' do 94 | # In GF256 Math A - B == A + B 95 | TSS::Util.gf256_sub(15, 6).must_equal 9 96 | end 97 | end 98 | 99 | describe 'gf256_mul' do 100 | it 'must return 0 if X == 0' do 101 | TSS::Util.gf256_mul(0, 6).must_equal 0 102 | end 103 | 104 | it 'must return 0 if Y == 0' do 105 | TSS::Util.gf256_mul(15, 0).must_equal 0 106 | end 107 | 108 | it 'must return a correct result' do 109 | TSS::Util.gf256_mul(5, 10).must_equal 34 110 | end 111 | end 112 | 113 | describe 'gf256_div' do 114 | it 'must return 0 if X == 0' do 115 | TSS::Util.gf256_div(0, 6).must_equal 0 116 | end 117 | 118 | it 'must raise an error if Y == 0 (divide by zero)' do 119 | assert_raises(TSS::Error) { TSS::Util.gf256_div(15, 0) } 120 | end 121 | 122 | it 'must return a correct result' do 123 | TSS::Util.gf256_div(5, 10).must_equal 141 124 | end 125 | end 126 | 127 | describe 'utf8_to_bytes' do 128 | it 'must present a correct result' do 129 | test_str = 'I ½ ♥ 💩' 130 | TSS::Util.utf8_to_bytes(test_str).must_equal test_str.bytes.to_a 131 | end 132 | end 133 | 134 | describe 'bytes_to_utf8' do 135 | it 'must present a correct result' do 136 | test_str = 'I ½ ♥ 💩' 137 | TSS::Util.bytes_to_utf8(test_str.bytes.to_a).must_equal test_str 138 | end 139 | end 140 | 141 | describe 'bytes_to_hex' do 142 | it 'must present a correct result' do 143 | test_str = 'I ½ ♥ 💩' 144 | bytes = test_str.bytes.to_a 145 | TSS::Util.bytes_to_hex(bytes).must_equal '4920c2bd20e299a520f09f92a9' 146 | end 147 | end 148 | 149 | describe 'hex_to_bytes' do 150 | it 'must present a correct result' do 151 | test_str = 'I ½ ♥ 💩' 152 | bytes = test_str.bytes.to_a 153 | TSS::Util.hex_to_bytes('4920c2bd20e299a520f09f92a9').must_equal bytes 154 | end 155 | end 156 | 157 | describe 'hex_to_utf8' do 158 | it 'must present a correct result' do 159 | test_str = 'I ½ ♥ 💩' 160 | bytes = test_str.bytes.to_a 161 | hex = TSS::Util.bytes_to_hex(bytes) 162 | TSS::Util.hex_to_utf8(hex).must_equal test_str 163 | end 164 | end 165 | 166 | describe 'utf8_to_hex' do 167 | it 'must present a correct result' do 168 | test_str = 'I ½ ♥ 💩' 169 | TSS::Util.utf8_to_hex(test_str).must_equal '4920c2bd20e299a520f09f92a9' 170 | end 171 | end 172 | 173 | describe 'pad' do 174 | it 'must raise an error if called with an Integer < 0' do 175 | assert_raises(ParamContractError) { TSS::Util.pad('a', -1) } 176 | end 177 | 178 | it 'must raise an error if called with an Integer > 255' do 179 | assert_raises(ParamContractError) { TSS::Util.pad('a', 256) } 180 | end 181 | 182 | it 'must pad a string to 16 bytes by default' do 183 | res = TSS::Util.pad('a') 184 | res.must_equal "a\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F\u000F" 185 | res[-1].must_equal "\u000F" # 15 186 | res.length.must_equal 16 187 | end 188 | 189 | it 'must pad an exact block length string with an extra full padding 16 byte block' do 190 | res = TSS::Util.pad('0123456789012345') 191 | res.must_equal "0123456789012345\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010\u0010" 192 | res[-1].must_equal "\u0010" # 16 193 | res.length.must_equal 32 194 | end 195 | 196 | it 'must pad a string with zero bytes' do 197 | res = TSS::Util.pad('foo', 0) 198 | res.must_equal 'foo' 199 | end 200 | 201 | it 'must pad a string to 0 < N < 255 bytes' do 202 | res = TSS::Util.pad('foo', 8) 203 | res.must_equal "foo\u0005\u0005\u0005\u0005\u0005" 204 | res[-1].must_equal "\u0005" # 5 205 | res.length.must_equal 8 206 | end 207 | 208 | it 'must pad a string to 255 bytes' do 209 | res = TSS::Util.pad('foo', 255) 210 | res.must_equal "foo\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC" 211 | res.length.must_equal 255 212 | end 213 | end 214 | 215 | describe 'unpad' do 216 | it 'must raise an error if called with an Integer < 0' do 217 | assert_raises(ParamContractError) { TSS::Util.unpad('a', -1) } 218 | end 219 | 220 | it 'must raise an error if called with an Integer > 255' do 221 | assert_raises(ParamContractError) { TSS::Util.unpad('a', 256) } 222 | end 223 | 224 | it 'must unpad a known padded string' do 225 | padded_str = "foo\u0005\u0005\u0005\u0005\u0005" 226 | TSS::Util.unpad(padded_str).must_equal 'foo' 227 | end 228 | 229 | it 'must unpad a complex padded string' do 230 | str = 'unicode ½ ♥ 💩' 231 | TSS::Util.unpad(TSS::Util.pad(str)).must_equal str 232 | end 233 | end 234 | 235 | describe 'secure_compare' do 236 | it 'must return true for same strings' do 237 | TSS::Util.secure_compare('a', 'a').must_equal true 238 | end 239 | 240 | it 'must return false for different strings' do 241 | TSS::Util.secure_compare('a', 'b').must_equal false 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /lib/tss/util.rb: -------------------------------------------------------------------------------- 1 | module TSS 2 | # Common utility, math, and conversion functions. 3 | module Util 4 | include Contracts::Core 5 | C = Contracts 6 | 7 | # The regex to match against human style shares 8 | HUMAN_SHARE_RE = /^tss~v1~*[a-zA-Z0-9\.\-\_]{0,16}~[0-9]{1,3}~([a-zA-Z0-9\-\_]+\={0,2})$/ 9 | 10 | # The EXP table. The elements are to be read from top to 11 | # bottom and left to right. For example, EXP[0] is 0x01, EXP[8] is 12 | # 0x1a, and so on. Note that the EXP[255] entry is present only as a 13 | # placeholder, and is not actually used in any computation. 14 | EXP = [0x01, 0x03, 0x05, 0x0f, 0x11, 0x33, 0x55, 0xff, 15 | 0x1a, 0x2e, 0x72, 0x96, 0xa1, 0xf8, 0x13, 0x35, 16 | 0x5f, 0xe1, 0x38, 0x48, 0xd8, 0x73, 0x95, 0xa4, 17 | 0xf7, 0x02, 0x06, 0x0a, 0x1e, 0x22, 0x66, 0xaa, 18 | 0xe5, 0x34, 0x5c, 0xe4, 0x37, 0x59, 0xeb, 0x26, 19 | 0x6a, 0xbe, 0xd9, 0x70, 0x90, 0xab, 0xe6, 0x31, 20 | 0x53, 0xf5, 0x04, 0x0c, 0x14, 0x3c, 0x44, 0xcc, 21 | 0x4f, 0xd1, 0x68, 0xb8, 0xd3, 0x6e, 0xb2, 0xcd, 22 | 0x4c, 0xd4, 0x67, 0xa9, 0xe0, 0x3b, 0x4d, 0xd7, 23 | 0x62, 0xa6, 0xf1, 0x08, 0x18, 0x28, 0x78, 0x88, 24 | 0x83, 0x9e, 0xb9, 0xd0, 0x6b, 0xbd, 0xdc, 0x7f, 25 | 0x81, 0x98, 0xb3, 0xce, 0x49, 0xdb, 0x76, 0x9a, 26 | 0xb5, 0xc4, 0x57, 0xf9, 0x10, 0x30, 0x50, 0xf0, 27 | 0x0b, 0x1d, 0x27, 0x69, 0xbb, 0xd6, 0x61, 0xa3, 28 | 0xfe, 0x19, 0x2b, 0x7d, 0x87, 0x92, 0xad, 0xec, 29 | 0x2f, 0x71, 0x93, 0xae, 0xe9, 0x20, 0x60, 0xa0, 30 | 0xfb, 0x16, 0x3a, 0x4e, 0xd2, 0x6d, 0xb7, 0xc2, 31 | 0x5d, 0xe7, 0x32, 0x56, 0xfa, 0x15, 0x3f, 0x41, 32 | 0xc3, 0x5e, 0xe2, 0x3d, 0x47, 0xc9, 0x40, 0xc0, 33 | 0x5b, 0xed, 0x2c, 0x74, 0x9c, 0xbf, 0xda, 0x75, 34 | 0x9f, 0xba, 0xd5, 0x64, 0xac, 0xef, 0x2a, 0x7e, 35 | 0x82, 0x9d, 0xbc, 0xdf, 0x7a, 0x8e, 0x89, 0x80, 36 | 0x9b, 0xb6, 0xc1, 0x58, 0xe8, 0x23, 0x65, 0xaf, 37 | 0xea, 0x25, 0x6f, 0xb1, 0xc8, 0x43, 0xc5, 0x54, 38 | 0xfc, 0x1f, 0x21, 0x63, 0xa5, 0xf4, 0x07, 0x09, 39 | 0x1b, 0x2d, 0x77, 0x99, 0xb0, 0xcb, 0x46, 0xca, 40 | 0x45, 0xcf, 0x4a, 0xde, 0x79, 0x8b, 0x86, 0x91, 41 | 0xa8, 0xe3, 0x3e, 0x42, 0xc6, 0x51, 0xf3, 0x0e, 42 | 0x12, 0x36, 0x5a, 0xee, 0x29, 0x7b, 0x8d, 0x8c, 43 | 0x8f, 0x8a, 0x85, 0x94, 0xa7, 0xf2, 0x0d, 0x17, 44 | 0x39, 0x4b, 0xdd, 0x7c, 0x84, 0x97, 0xa2, 0xfd, 45 | 0x1c, 0x24, 0x6c, 0xb4, 0xc7, 0x52, 0xf6, 0x00].freeze 46 | 47 | # The LOG table. The elements are to be read from top to 48 | # bottom and left to right. For example, LOG[1] is 0, LOG[8] is 75, 49 | # and so on. Note that the LOG[0] entry is present only as a 50 | # placeholder, and is not actually used in any computation. 51 | LOG = [0, 0, 25, 1, 50, 2, 26, 198, 52 | 75, 199, 27, 104, 51, 238, 223, 3, 53 | 100, 4, 224, 14, 52, 141, 129, 239, 54 | 76, 113, 8, 200, 248, 105, 28, 193, 55 | 125, 194, 29, 181, 249, 185, 39, 106, 56 | 77, 228, 166, 114, 154, 201, 9, 120, 57 | 101, 47, 138, 5, 33, 15, 225, 36, 58 | 18, 240, 130, 69, 53, 147, 218, 142, 59 | 150, 143, 219, 189, 54, 208, 206, 148, 60 | 19, 92, 210, 241, 64, 70, 131, 56, 61 | 102, 221, 253, 48, 191, 6, 139, 98, 62 | 179, 37, 226, 152, 34, 136, 145, 16, 63 | 126, 110, 72, 195, 163, 182, 30, 66, 64 | 58, 107, 40, 84, 250, 133, 61, 186, 65 | 43, 121, 10, 21, 155, 159, 94, 202, 66 | 78, 212, 172, 229, 243, 115, 167, 87, 67 | 175, 88, 168, 80, 244, 234, 214, 116, 68 | 79, 174, 233, 213, 231, 230, 173, 232, 69 | 44, 215, 117, 122, 235, 22, 11, 245, 70 | 89, 203, 95, 176, 156, 169, 81, 160, 71 | 127, 12, 246, 111, 23, 196, 73, 236, 72 | 216, 67, 31, 45, 164, 118, 123, 183, 73 | 204, 187, 62, 90, 251, 96, 177, 134, 74 | 59, 82, 161, 108, 170, 85, 41, 157, 75 | 151, 178, 135, 144, 97, 190, 220, 252, 76 | 188, 149, 207, 205, 55, 63, 91, 209, 77 | 83, 57, 132, 60, 65, 162, 109, 71, 78 | 20, 42, 158, 93, 86, 242, 211, 171, 79 | 68, 17, 146, 217, 35, 32, 46, 137, 80 | 180, 124, 184, 38, 119, 153, 227, 165, 81 | 103, 74, 237, 222, 197, 49, 254, 24, 82 | 13, 99, 140, 128, 192, 247, 112, 7].freeze 83 | 84 | # GF(256) Addition 85 | # The addition operation returns the Bitwise 86 | # Exclusive OR (XOR) of its operands. 87 | # 88 | # @param a [Integer] a single Integer 89 | # @param b [Integer] a single Integer 90 | # @return [Integer] a GF(256) SUM of a and b 91 | def self.gf256_add(a, b) 92 | a ^ b 93 | end 94 | 95 | # The subtraction operation is identical to GF(256) addition, because the 96 | # field has characteristic two. 97 | # 98 | # @param a [Integer] a single Integer 99 | # @param b [Integer] a single Integer 100 | # @return [Integer] a GF(256) subtraction of a and b 101 | def self.gf256_sub(a, b) 102 | gf256_add(a, b) 103 | end 104 | 105 | # The multiplication operation takes two elements X and Y as input and 106 | # proceeds as follows. If either X or Y is equal to 0x00, then the 107 | # operation returns 0x00. Otherwise, the value EXP[ (LOG[X] + LOG[Y]) 108 | # modulo 255] is returned. 109 | # 110 | # @param x [Integer] a single Integer 111 | # @param y [Integer] a single Integer 112 | # @return [Integer] a GF(256) multiplication of x and y 113 | def self.gf256_mul(x, y) 114 | return 0 if x == 0 || y == 0 115 | EXP[(LOG[x] + LOG[y]) % 255] 116 | end 117 | 118 | # The division operation takes a dividend X and a divisor Y as input 119 | # and computes X divided by Y as follows. If X is equal to 0x00, then 120 | # the operation returns 0x00. If Y is equal to 0x00, then the input is 121 | # invalid, and an error condition occurs. Otherwise, the value 122 | # EXP[(LOG[X] - LOG[Y]) modulo 255] is returned. 123 | # 124 | # @param x [Integer] a single Integer 125 | # @param y [Integer] a single Integer 126 | # @return [Integer] a GF(256) division of x divided by y 127 | # @raise [TSS::Error] if an attempt to divide by zero is tried 128 | def self.gf256_div(x, y) 129 | return 0 if x == 0 130 | raise TSS::Error, 'divide by zero' if y == 0 131 | EXP[(LOG[x] - LOG[y]) % 255] 132 | end 133 | 134 | # Share generation Function 135 | # 136 | # The function f takes as input a single octet X that is not equal to 137 | # 0x00, and an array A of M octets, and returns a single octet. It is 138 | # defined as: 139 | # 140 | # f(X, A) = GF_SUM A[i] (*) X^i 141 | # i=0,M-1 142 | # 143 | # Because the GF_SUM summation takes place over GF(256), each addition 144 | # uses the exclusive-or operation, and not integer addition. Note that 145 | # the successive values of X^i used in the computation of the function 146 | # f can be computed by multiplying a value by X once for each term in 147 | # the summation. 148 | # 149 | # @param x [Integer] a single Integer 150 | # @param bytes [Array] an Array of Integers 151 | # @return [Integer] a single Integer 152 | # @raise [TSS::Error] if the index value for the share is zero 153 | def self.f(x, bytes) 154 | raise TSS::Error, 'invalid share index value, cannot be 0' if x == 0 155 | y = 0 156 | x_i = 1 157 | 158 | bytes.each do |b| 159 | y = gf256_add(y, gf256_mul(b, x_i)) 160 | x_i = gf256_mul(x_i, x) 161 | end 162 | 163 | y 164 | end 165 | 166 | # Secret Reconstruction Function 167 | # 168 | # We define the function L_i (for i from 0 to M-1, inclusive) that 169 | # takes as input an array U of M pairwise distinct octets, and is 170 | # defined as 171 | # 172 | # U[j] 173 | # L_i(U) = GF_PRODUCT ------------- 174 | # j=0,M-1, j!=i U[j] (+) U[i] 175 | # 176 | # Here the product runs over all of the values of j from 0 to M-1, 177 | # excluding the value i. (This function is equal to ith Lagrange 178 | # function, evaluated at zero.) Note that the denominator in the above 179 | # expression is never equal to zero because U[i] is not equal to U[j] 180 | # whenever i is not equal to j. 181 | # 182 | # @param i [Integer] a single Integer 183 | # @param u [Array] an Array of Integers 184 | # @return [Integer] a single Integer 185 | def self.basis_poly(i, u) 186 | prod = 1 187 | 188 | (0..(u.length - 1)).each do |j| 189 | next if i == j 190 | prod = gf256_mul(prod, gf256_div(u[j], gf256_add(u[j], u[i]))) 191 | end 192 | 193 | prod 194 | end 195 | 196 | # Secret Reconstruction Function 197 | # 198 | # We denote the interpolation function as I. This function takes as 199 | # input two arrays U and V, each consisting of M octets, and returns a 200 | # single octet; it is defined as: 201 | # 202 | # I(U, V) = GF_SUM L_i(U) (*) V[i]. 203 | # i=0,M-1 204 | # 205 | # @param u [Array] an Array of Integers 206 | # @param v [Array] an Array of Integers 207 | # @return [Integer] a single Integer 208 | def self.lagrange_interpolation(u, v) 209 | sum = 0 210 | 211 | (0..(v.length - 1)).each do |i| 212 | sum = gf256_add(sum, gf256_mul(basis_poly(i, u), v[i])) 213 | end 214 | 215 | sum 216 | end 217 | 218 | # Convert a UTF-8 String to an Array of Bytes 219 | # 220 | # @param str a UTF-8 String to convert 221 | # @return an Array of Integer Bytes 222 | Contract String => C::ArrayOf[C::Int] 223 | def self.utf8_to_bytes(str) 224 | str.bytes.to_a 225 | end 226 | 227 | # Convert an Array of Bytes to a UTF-8 String 228 | # 229 | # @param bytes an Array of Bytes to convert 230 | # @return a UTF-8 String 231 | Contract C::ArrayOf[C::Int] => String 232 | def self.bytes_to_utf8(bytes) 233 | bytes.pack('C*').force_encoding('utf-8') 234 | end 235 | 236 | # Convert an Array of Bytes to a hex String 237 | # 238 | # @param bytes an Array of Bytes to convert 239 | # @return a hex String 240 | Contract C::ArrayOf[C::Int] => String 241 | def self.bytes_to_hex(bytes) 242 | hex = '' 243 | bytes.each { |b| hex += sprintf('%02x', b) } 244 | hex.downcase 245 | end 246 | 247 | # Convert a hex String to an Array of Bytes 248 | # 249 | # @param str a hex String to convert 250 | # @return an Array of Integer Bytes 251 | Contract String => C::ArrayOf[C::Int] 252 | def self.hex_to_bytes(str) 253 | [str].pack('H*').unpack('C*') 254 | end 255 | 256 | # Convert a hex String to a UTF-8 String 257 | # 258 | # @param hex a hex String to convert 259 | # @return a UTF-8 String 260 | Contract String => String 261 | def self.hex_to_utf8(hex) 262 | bytes_to_utf8(hex_to_bytes(hex)) 263 | end 264 | 265 | # Convert a UTF-8 String to a hex String 266 | # 267 | # @param str a UTF-8 String to convert 268 | # @return a hex String 269 | Contract String => String 270 | def self.utf8_to_hex(str) 271 | bytes_to_hex(utf8_to_bytes(str)) 272 | end 273 | 274 | # Pad a String with PKCS#7 (RFC5652) 275 | # See : https://tools.ietf.org/html/rfc5652#section-6.3 276 | # 277 | # @param str the String or Array of bytes to pad 278 | # @param k pad blocksize (0-255), default 16 279 | # @return a PKCS#7 padded String or Array of bytes 280 | Contract C::Or[Array, String], C::PadBlocksizeArg => C::Or[Array, String] 281 | def self.pad(str, k = TSS::PADDING_BLOCK_SIZE_BYTES) 282 | return str if k.zero? 283 | str_bytes = str.is_a?(Array) ? str : TSS::Util.utf8_to_bytes(str) 284 | l = str_bytes.length 285 | val = k - (l % k) 286 | pad_bytes = [val] * val 287 | padded_str_bytes = str_bytes + pad_bytes 288 | str.is_a?(Array) ? padded_str_bytes : TSS::Util.bytes_to_utf8(padded_str_bytes) 289 | end 290 | 291 | # Remove padding from a String previously padded with PKCS#7 (RFC5652) 292 | # 293 | # @param str the String to remove padding from 294 | # @param k pad blocksize (0-255) 295 | # @return an unpadded String or Array of bytes 296 | Contract C::Or[Array, String], C::PadBlocksizeArg => C::Or[Array, String] 297 | def self.unpad(str, k = TSS::PADDING_BLOCK_SIZE_BYTES) 298 | return str if k.zero? 299 | str_bytes = str.is_a?(Array) ? str : TSS::Util.utf8_to_bytes(str) 300 | val = str_bytes.last 301 | raise 'Input is not padded or padding is corrupt' if val > k 302 | # Verify that the proper number of PKCS#7 padding bytes are present 303 | # and match the last byte value in both value and number of bytes present. 304 | raise 'Padding bytes are invalid' unless str_bytes.last(val).all? {|b| b == val} 305 | l = str_bytes.length - val 306 | unpadded_str_bytes = str_bytes.take(l) 307 | str.is_a?(Array) ? unpadded_str_bytes : TSS::Util.bytes_to_utf8(unpadded_str_bytes) 308 | end 309 | 310 | # Constant time string comparison. 311 | # Extracted from Rack::Utils 312 | # https://github.com/rack/rack/blob/master/lib/rack/utils.rb 313 | # 314 | # NOTE: the values compared should be of fixed length, such as strings 315 | # that have already been hashed. This should not be used 316 | # on variable length plaintext strings because it could leak length info 317 | # via timing attacks. 318 | # 319 | # The user provided value should always be passed in as the second 320 | # parameter so as not to leak info about the secret. 321 | # 322 | # @param a the private value 323 | # @param b the user provided value 324 | # @return whether the strings match or not 325 | Contract String, String => C::Bool 326 | def self.secure_compare(a, b) 327 | return false unless a.bytesize == b.bytesize 328 | ah = Digest::SHA256.hexdigest(a) 329 | bh = Digest::SHA256.hexdigest(b) 330 | 331 | l = ah.unpack('C*') 332 | r, i = 0, -1 333 | bh.each_byte { |v| r |= v ^ l[i+=1] } 334 | r == 0 335 | end 336 | 337 | # Extract the header data from a binary share. 338 | # Extra "\x00" padding in the identifier will be removed. 339 | # 340 | # @param share a binary octet share 341 | # @return header attributes 342 | Contract String => Hash 343 | def self.extract_share_header(share) 344 | h = Splitter::SHARE_HEADER_STRUCT.decode(share) 345 | h[:identifier] = h[:identifier].delete("\x00") 346 | return h 347 | end 348 | 349 | # Calculate the factorial for an Integer. 350 | # 351 | # @param n the Integer to calculate for 352 | # @return the factorial of n 353 | Contract C::Int => C::Int 354 | def self.factorial(n) 355 | (1..n).reduce(:*) || 1 356 | end 357 | 358 | # Calculate the number of combinations possible 359 | # for a given number of shares and threshold. 360 | # 361 | # * http://www.wolframalpha.com/input/?i=20+choose+5 362 | # * http://www.mathsisfun.com/combinatorics/combinations-permutations-calculator.html (Set balls, 20, 5, no, no) == 15504 363 | # * http://www.mathsisfun.com/combinatorics/combinations-permutations.html 364 | # * https://jdanger.com/calculating-factorials-in-ruby.html 365 | # * http://chriscontinanza.com/2010/10/29/Array.html 366 | # * http://stackoverflow.com/questions/2434503/ruby-factorial-function 367 | # 368 | # @param n the total number of shares 369 | # @param r the threshold number of shares 370 | # @return the number of possible combinations 371 | Contract C::Int, C::Int => C::Int 372 | def self.calc_combinations(n, r) 373 | factorial(n) / (factorial(r) * factorial(n - r)) 374 | end 375 | end 376 | end 377 | -------------------------------------------------------------------------------- /lib/tss/combiner.rb: -------------------------------------------------------------------------------- 1 | module TSS 2 | # Warning, you probably don't want to use this directly. Instead 3 | # see the TSS module. 4 | # 5 | # TSS::Combiner has responsibility for combining an Array of String shares back 6 | # into the original secret the shares were split from. It is also responsible 7 | # for doing extensive validation of user provided shares and ensuring 8 | # that any recovered secret matches the hash of the original secret. 9 | class Combiner 10 | include Contracts::Core 11 | include Util 12 | 13 | C = Contracts 14 | 15 | attr_reader :shares, :select_by, :padding 16 | 17 | Contract ({ :shares => C::ArrayOfShares, :select_by => C::Maybe[C::SelectByArg], :padding => C::Maybe[C::Bool] }) => C::Any 18 | def initialize(opts = {}) 19 | # clone the incoming shares so the object passed to this 20 | # function doesn't get modified. 21 | @shares = opts.fetch(:shares).clone 22 | @select_by = opts.fetch(:select_by, 'FIRST') 23 | @padding = opts.fetch(:padding, true) 24 | end 25 | 26 | # Warning, you probably don't want to use this directly. Instead 27 | # see the TSS module. 28 | # 29 | # To reconstruct a secret from a set of shares, the following 30 | # procedure, or any equivalent method, is used: 31 | # 32 | # If the number of shares provided as input to the secret 33 | # reconstruction operation is greater than the threshold M, then M 34 | # of those shares are selected for use in the operation. The method 35 | # used to select the shares can be arbitrary. 36 | # 37 | # If the shares are not equal length, then the input is 38 | # inconsistent. An error should be reported, and processing must 39 | # halt. 40 | # 41 | # The output string is initialized to the empty (zero-length) octet 42 | # string. 43 | # 44 | # The octet array U is formed by setting U[i] equal to the first 45 | # octet of the ith share. (Note that the ordering of the shares is 46 | # arbitrary, but must be consistent throughout this algorithm.) 47 | # 48 | # The initial octet is stripped from each share. 49 | # 50 | # If any two elements of the array U have the same value, then an 51 | # error condition has occurred; this fact should be reported, then 52 | # the procedure must halt. 53 | # 54 | # For each octet of the shares, the following steps are performed. 55 | # An array V of M octets is created, in which the array element V[i] 56 | # contains the octet from the ith share. The value of I(U, V) is 57 | # computed, then appended to the output string. 58 | # 59 | # The output string is returned (along with some metadata). 60 | # 61 | # 62 | # @return an Hash of combined secret attributes 63 | # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid 64 | # rubocop:disable CyclomaticComplexity 65 | Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C::HashAlgArg, :identifier => C::IdentifierArg, :process_time => C::Num, :secret => C::SecretArg, :threshold => C::ThresholdArg}) 66 | def combine 67 | # unwrap 'human' shares into binary shares 68 | if all_shares_appear_human?(shares) 69 | @shares = convert_shares_human_to_binary(shares) 70 | end 71 | 72 | validate_all_shares(shares) 73 | start_processing_time = Time.now 74 | 75 | h = Util.extract_share_header(shares.sample) 76 | threshold = h[:threshold] 77 | identifier = h[:identifier] 78 | hash_id = h[:hash_id] 79 | 80 | # Select a subset of the shares provided using the chosen selection 81 | # method. If there are exactly the right amount of shares this is a no-op. 82 | if select_by == 'FIRST' 83 | @shares = shares.shift(threshold) 84 | elsif select_by == 'SAMPLE' 85 | @shares = shares.sample(threshold) 86 | end 87 | 88 | # slice out the data after the header bytes in each share 89 | # and unpack the byte string into an Array of Byte Arrays 90 | shares_bytes = shares.map do |s| 91 | bytestring = s.byteslice(Splitter::SHARE_HEADER_STRUCT.size..s.bytesize) 92 | bytestring.unpack('C*') unless bytestring.nil? 93 | end.compact 94 | 95 | shares_bytes_have_valid_indexes!(shares_bytes) 96 | 97 | if select_by == 'COMBINATIONS' 98 | share_combinations_mode_allowed!(hash_id) 99 | share_combinations_out_of_bounds!(shares, threshold) 100 | 101 | # Build an Array of all possible `threshold` size combinations. 102 | share_combos = shares_bytes.combination(threshold).to_a 103 | 104 | # Try each combination until one works. 105 | secret = nil 106 | while secret.nil? && share_combos.present? 107 | # Check a combination and shift it off the Array 108 | result = extract_secret_from_shares!(hash_id, share_combos.shift) 109 | next if result.nil? 110 | secret = result 111 | end 112 | else 113 | secret = extract_secret_from_shares!(hash_id, shares_bytes) 114 | end 115 | 116 | # Return a Hash with the secret and metadata 117 | { 118 | hash: secret[:hash], 119 | hash_alg: secret[:hash_alg], 120 | identifier: identifier, 121 | process_time: ((Time.now - start_processing_time)*1000).round(2), 122 | secret: Util.bytes_to_utf8(secret[:secret]), 123 | threshold: threshold 124 | } 125 | end 126 | # rubocop:enable CyclomaticComplexity 127 | 128 | private 129 | 130 | # Given a hash ID and an Array of Arrays of Share Bytes, extract a secret 131 | # and validate it against any one-way hash that was embedded in the shares 132 | # along with the secret. 133 | # 134 | # @param hash_id the ID of the one-way hash function to test with 135 | # @param shares_bytes the shares as Byte Arrays to be evaluated 136 | # @return returns the secret as an Array of Bytes if it was recovered from the shares and validated 137 | # @raise [TSS::NoSecretError] if the secret was not able to be recovered (with no hash) 138 | # @raise [TSS::InvalidSecretHashError] if the secret was able to be recovered but the hash test failed 139 | Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C::HashAlgArg }) 140 | def extract_secret_from_shares!(hash_id, shares_bytes) 141 | secret = [] 142 | 143 | # build up an Array of index values from each share 144 | # u[i] equal to the first octet of the ith share 145 | u = shares_bytes.map { |s| s[0] } 146 | 147 | # loop through each byte in all the shares 148 | # start at Array index 1 in each share's Byte Array to skip the index 149 | (1..(shares_bytes.first.length - 1)).each do |i| 150 | v = shares_bytes.map { |share| share[i] } 151 | secret << Util.lagrange_interpolation(u, v) 152 | end 153 | 154 | hash_alg = Hasher.key_from_code(hash_id) 155 | 156 | # Run the hash digest checks if the shares were created with a digest 157 | if Hasher.codes_without_none.include?(hash_id) 158 | # RTSS : pop off the hash digest bytes from the tail of the secret. This 159 | # leaves `secret` with only the secret bytes remaining. 160 | orig_hash_bytes = secret.pop(Hasher.bytesize(hash_alg)) 161 | orig_hash_hex = Util.bytes_to_hex(orig_hash_bytes) 162 | 163 | # Remove PKCS#7 padding from the secret now that the hash 164 | # has been extracted from the data 165 | secret = Util.unpad(secret) if padding 166 | 167 | # RTSS : verify that the recombined secret computes the same hash 168 | # digest now as when it was originally created. 169 | new_hash_bytes = Hasher.byte_array(hash_alg, Util.bytes_to_utf8(secret)) 170 | new_hash_hex = Util.bytes_to_hex(new_hash_bytes) 171 | 172 | unless Util.secure_compare(orig_hash_hex, new_hash_hex) 173 | raise TSS::InvalidSecretHashError, 'invalid shares, hash of secret does not equal embedded hash' 174 | end 175 | else 176 | secret = Util.unpad(secret) if padding 177 | end 178 | 179 | if secret.present? 180 | return { secret: secret, hash: orig_hash_hex, hash_alg: hash_alg } 181 | else 182 | raise TSS::NoSecretError, 'invalid shares, unable to recombine into a verifiable secret' 183 | end 184 | end 185 | 186 | # Do all of the shares match the pattern expected of human style shares? 187 | # 188 | # @param shares the shares to be evaluated 189 | # @return returns true if all shares match the patterns, false if not 190 | # @raise [ParamContractError] if shares appear invalid 191 | Contract C::ArrayOf[String] => C::Bool 192 | def all_shares_appear_human?(shares) 193 | shares.all? do |s| 194 | # test for starting with 'tss' since regex match against 195 | # binary data sometimes throws exceptions. 196 | s.start_with?('tss~') && s.match(Util::HUMAN_SHARE_RE) 197 | end 198 | end 199 | 200 | # Convert an Array of human style shares to binary style 201 | # 202 | # @param shares the shares to be converted 203 | # @return returns an Array of String shares in binary octet String format 204 | # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid 205 | Contract C::ArrayOf[String] => C::ArrayOf[String] 206 | def convert_shares_human_to_binary(shares) 207 | shares.map do |s| 208 | s_b64 = s.match(Util::HUMAN_SHARE_RE) 209 | if s_b64.present? && s_b64.to_a[1].present? 210 | begin 211 | # the [1] capture group contains the Base64 encoded bin share 212 | Base64.urlsafe_decode64(s_b64.to_a[1]) 213 | rescue ArgumentError 214 | raise TSS::ArgumentError, 'invalid shares, some human format shares have invalid Base64 data' 215 | end 216 | else 217 | raise TSS::ArgumentError, 'invalid shares, some human format shares do not match expected pattern' 218 | end 219 | end 220 | end 221 | 222 | # Do all shares have a common Byte size? They are invalid if not. 223 | # 224 | # @param shares the shares to be evaluated 225 | # @return returns true if all shares have the same Byte size 226 | # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid 227 | Contract C::ArrayOf[String] => C::Bool 228 | def shares_have_same_bytesize!(shares) 229 | shares.each do |s| 230 | unless s.bytesize == shares.first.bytesize 231 | raise TSS::ArgumentError, 'invalid shares, different byte lengths' 232 | end 233 | end 234 | return true 235 | end 236 | 237 | # Do all shares have a valid header and match each other? They are invalid if not. 238 | # 239 | # @param shares the shares to be evaluated 240 | # @return returns true if all shares have the same header 241 | # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid 242 | Contract C::ArrayOf[String] => C::Bool 243 | def shares_have_valid_headers!(shares) 244 | fh = Util.extract_share_header(shares.first) 245 | 246 | unless Contract.valid?(fh, ({ :identifier => String, :hash_id => C::Int, :threshold => C::Int, :share_len => C::Int })) 247 | raise TSS::ArgumentError, 'invalid shares, headers have invalid structure' 248 | end 249 | 250 | shares.each do |s| 251 | unless Util.extract_share_header(s) == fh 252 | raise TSS::ArgumentError, 'invalid shares, headers do not match' 253 | end 254 | end 255 | 256 | return true 257 | end 258 | 259 | # Do all shares have a the expected length? They are invalid if not. 260 | # 261 | # @param shares the shares to be evaluated 262 | # @return returns true if all shares have the same header 263 | # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid 264 | Contract C::ArrayOf[String] => C::Bool 265 | def shares_have_expected_length!(shares) 266 | shares.each do |s| 267 | unless s.bytesize > Splitter::SHARE_HEADER_STRUCT.size + 1 268 | raise TSS::ArgumentError, 'invalid shares, too short' 269 | end 270 | end 271 | return true 272 | end 273 | 274 | # Were enough shares provided to meet the threshold? They are invalid if not. 275 | # 276 | # @param shares the shares to be evaluated 277 | # @return returns true if there are enough shares 278 | # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid 279 | Contract C::ArrayOf[String] => C::Bool 280 | def shares_meet_threshold_min!(shares) 281 | fh = Util.extract_share_header(shares.first) 282 | unless shares.size >= fh[:threshold] 283 | raise TSS::ArgumentError, 'invalid shares, fewer than threshold' 284 | else 285 | return true 286 | end 287 | end 288 | 289 | # Were enough shares provided to meet the threshold? They are invalid if not. 290 | # 291 | # @param shares the shares to be evaluated 292 | # @return returns true if all tests pass 293 | # @raise [ParamContractError] if shares appear invalid 294 | Contract C::ArrayOf[String] => C::Bool 295 | def validate_all_shares(shares) 296 | if shares_have_valid_headers!(shares) && 297 | shares_have_same_bytesize!(shares) && 298 | shares_have_expected_length!(shares) && 299 | shares_meet_threshold_min!(shares) 300 | return true 301 | else 302 | return false 303 | end 304 | end 305 | 306 | # Do all the shares have a valid first-byte index? They are invalid if not. 307 | # 308 | # @param shares_bytes the shares as Byte Arrays to be evaluated 309 | # @return returns true if there are enough shares 310 | # @raise [ParamContractError, TSS::ArgumentError] if shares bytes appear invalid 311 | Contract C::ArrayOf[C::ArrayOf[C::Num]] => C::Bool 312 | def shares_bytes_have_valid_indexes!(shares_bytes) 313 | u = shares_bytes.map do |s| 314 | raise TSS::ArgumentError, 'invalid shares, no index' if s[0].blank? 315 | raise TSS::ArgumentError, 'invalid shares, zero index' if s[0] == 0 316 | s[0] 317 | end 318 | 319 | unless u.uniq.size == shares_bytes.size 320 | raise TSS::ArgumentError, 'invalid shares, duplicate indexes' 321 | else 322 | return true 323 | end 324 | end 325 | 326 | # Is it valid to use combinations mode? Only when there is an embedded non-zero 327 | # hash_id Integer to test the results against. Invalid if not. 328 | # 329 | # @param hash_id the shares as Byte Arrays to be evaluated 330 | # @return returns true if OK to use combinations mode 331 | # @raise [ParamContractError, TSS::ArgumentError] if hash_id represents a non hashing type 332 | Contract C::Int => C::Bool 333 | def share_combinations_mode_allowed!(hash_id) 334 | unless Hasher.codes_without_none.include?(hash_id) 335 | raise TSS::ArgumentError, 'invalid options, combinations mode can only be used with hashed shares.' 336 | else 337 | return true 338 | end 339 | end 340 | 341 | # Calculate the number of possible combinations when combinations mode is 342 | # selected. Raise an exception if the possible combinations are too large. 343 | # 344 | # If this is not tested, the number of combinations can quickly grow into 345 | # numbers that cannot be calculated before the end of the universe. 346 | # e.g. 255 total shares, with threshold of 128, results in # combinations of: 347 | # 2884329411724603169044874178931143443870105850987581016304218283632259375395 348 | # 349 | # @param shares the shares to be evaluated 350 | # @param threshold the threshold value set in the shares 351 | # @param max_combinations the max (1_000_000) number of combinations allowed 352 | # @return returns true if a reasonable number of combinations 353 | # @raise [ParamContractError, TSS::ArgumentError] if args are invalid or the number of possible combinations is unreasonably high 354 | Contract C::ArrayOf[String], C::Int, C::Int => C::Bool 355 | def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000) 356 | combinations = Util.calc_combinations(shares.size, threshold) 357 | if combinations > max_combinations 358 | raise TSS::ArgumentError, "invalid options, too many combinations (#{combinations})" 359 | else 360 | return true 361 | end 362 | end 363 | end 364 | end 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSS - Threshold Secret Sharing 2 | 3 | [![Gem Version](https://badge.fury.io/rb/tss.svg)](https://badge.fury.io/rb/tss) 4 | [![Dependency Status](https://gemnasium.com/badges/github.com/grempe/tss-rb.svg)](https://gemnasium.com/github.com/grempe/tss-rb) 5 | [![Build Status](https://travis-ci.org/grempe/tss-rb.svg?branch=master)](https://travis-ci.org/grempe/tss-rb) 6 | [![Coverage Status](https://coveralls.io/repos/github/grempe/tss-rb/badge.svg?branch=master)](https://coveralls.io/github/grempe/tss-rb?branch=master) 7 | [![Code Climate](https://codeclimate.com/github/grempe/tss-rb/badges/gpa.svg)](https://codeclimate.com/github/grempe/tss-rb) 8 | 9 | Ruby Docs : [http://www.rubydoc.info/gems/tss](http://www.rubydoc.info/gems/tss) 10 | 11 | 12 | ## WARNING : BETA CODE 13 | 14 | This code is new and has not yet been tested in production. Use at your own risk. 15 | The share format and interface should be fairly stable now but should not be 16 | considered fully stable until v1.0.0 is released. 17 | 18 | ## About TSS 19 | 20 | This Ruby gem implements Threshold Secret Sharing, as specified in 21 | the Network Working Group Internet-Draft submitted by D. McGrew 22 | ([draft-mcgrew-tss-03.txt](http://tools.ietf.org/html/draft-mcgrew-tss-03)). 23 | 24 | Threshold Secret Sharing (TSS) provides a way to generate `N` shares 25 | from a value, so that any `M` of those shares can be used to 26 | reconstruct the original value, but any `M-1` shares provide no 27 | information about that value. This method can provide shared access 28 | control on key material and other secrets that must be strongly 29 | protected. 30 | 31 | This threshold secret sharing method is based on polynomial interpolation in 32 | GF(256) and also provides a robust format for the storage and transmission 33 | of shares. 34 | 35 | The sharing format is Robust Threshold Secret Sharing (RTSS) as described 36 | in the Internet-Draft. RTSS is a binary data format and a method for 37 | ensuring that any secrets recovered are identical to the secret that was 38 | originally shared. 39 | 40 | This implementation supports RTSS digest types for `NONE`, `SHA1`, and 41 | `SHA256`. `SHA256` is the recommended digest. In the RTSS scheme a digest of 42 | the original secret is concatenated with the secret itself prior to the splitting 43 | of the secret into shares. Later, this digest is compared with any secret recovered 44 | by recombining shares. If the hash of the recovered secret does not match the 45 | original hash stored in the shares the secret will not be returned. The verifier 46 | hash for the secret is not available to shareholders prior to recombining shares. 47 | 48 | The specification also addresses the optional implementation of a `MAGIC_NUMBER` and 49 | advanced error correction schemes. These extras are not currently implemented. 50 | 51 | ## TL;DR 52 | 53 | No time for docs? Here is how to get going in a minute, using the 54 | command line or Ruby, with the default `3 out of 5` secret sharing. 55 | 56 | ### Command Line Interface (CLI) 57 | 58 | ```sh 59 | $ echo 'secret unicode characters ½ ♥ 💩' | tss split -O /tmp/shares.txt 60 | $ tss combine -I /tmp/shares.txt 61 | 62 | RECOVERED SECRET METADATA 63 | ************************* 64 | hash : 6d1bc4242998bc170d35d374ebdf39295b271db900de25d249563a4622f113e0 65 | hash_alg : SHA256 66 | identifier : 6b02cfcfc24a2b50 67 | process_time : 0.55ms 68 | threshold : 3 69 | secret : 70 | secret unicode characters ½ ♥ 💩 71 | ``` 72 | 73 | ### Ruby 74 | 75 | ```ruby 76 | $ bundle exec bin/console 77 | > require 'tss' 78 | => false 79 | > shares = TSS.split(secret: 'my deep dark secret') 80 | => ["tss~v1~506168fade769236~3~NTA2MTY4ZmFkZTc2OTIzNgIDAEEBIM39jPGXFz4zKCObWgp2zQmCuUx92-VUf48FWQFqnLF3bw6VtVjYK7JRfESZIREdzhWcQuTQVuGKazxWRK27Tg==", 81 | "tss~v1~506168fade769236~3~NTA2MTY4ZmFkZTc2OTIzNgIDAEEC1E8YyDhp5NeZvF6vHeUT6HoiU0AgoF69jyjNRbtcIi1YoymJTau1rJP-3nQXOofaB2LgnAhJpbB8vrID9WTKgQ==", 82 | "tss~v1~506168fade769236~3~NTA2MTY4ZmFkZTc2OTIzNgIDAEEDmfvFIKybg8nO9Q9fZ5wARgHFngFQdrbk_arFEbc7s5HedTjzkoZYLlV8xnywt4AVAHAO0nSLY3C7iJPifH5VYA==", 83 | "tss~v1~506168fade769236~3~NTA2MTY4ZmFkZTc2OTIzNgIDAEEEiBoCoHycMNTS5yQEU8_Sc7eVlIZOJP1-ka_7MPTlC1p2DyNdGvQxy3pBFZyH8WXAY9ICBxiZRDCJisFrebviAg==", 84 | "tss~v1~506168fade769236~3~NTA2MTY4ZmFkZTc2OTIzNgIDAEEFxa7fSOhuV8qFrnX0KbbB3cxyWcc-8hUn4y3zZPiCmubw2TInxdncSbzDDZQgfGIPZMDsSWRbgvBOvOCK8KF94w=="] 85 | > secret = TSS.combine(shares: shares) 86 | => {:hash=>"f1b91fef6a7535a974d3644c3eac16d2c907720c981290214d5d1db7cdb724af", 87 | :hash_alg=>"SHA256", 88 | :identifier=>"506168fade769236", 89 | :process_time=>0.94, 90 | :secret=>"my deep dark secret", 91 | :threshold=>3} 92 | > puts secret[:secret] 93 | my deep dark secret 94 | => nil 95 | ``` 96 | 97 | ## Is it any good? 98 | 99 | While this implementation has not yet had a formal security review, the cryptographic 100 | underpinnings were carefully specified in an IETF draft document authored by a 101 | noted cryptographer. I have reached out to individuals respected in the field 102 | for their work in implementing cryptographic solutions to help review this code. 103 | 104 | > I've read draft-mcgrew-tss-03 and then took a look at your code. 105 | > Impressive! Nice docs, clean easy-to-read code. I'd use constant-time 106 | > comparison for hashes [[resolved : 254ecab](https://github.com/grempe/tss-rb/commit/254ecab24a338872a5b05c7446213ef1ddabf4cb)], 107 | > but apart from that I have nothing to add. Good job! 108 | > 109 | > -- Dmitry Chestnykh ([@dchest](https://github.com/dchest)) 110 | > 111 | > [v0.1.0 : 4/13/2016] 112 | 113 | All that being said, if your threat model includes a **N**ation **S**tate **A**ctor 114 | the security of this particular code should probably not be your primary concern. 115 | 116 | ## Suggestions for Use 117 | 118 | * Don't split large texts. Instead, split the much smaller encryption 119 | keys that protect encrypted large texts. Supply the encrypted 120 | files and the shares separately to recipients. Threshold secret sharing can be 121 | very slow at splitting and recombining very large bodies of text, especially 122 | when combined with a large number of shares. Every byte of the secret must 123 | be processed `num_shares` times. 124 | 125 | * Don't treat shares like encrypted data, but instead like the encryption keys 126 | that unlock the data. Shares are keys, and need to be protected as such. There is 127 | nothing to slow down an attacker if they have access to enough shares. 128 | 129 | * If you send keys by email, or some other insecure channel, then your email 130 | provider, or any entity with access to their data, now also has the keys to 131 | your data. They just need to collect enough keys to meet the threshold. Sending 132 | enough shares to recreate a secret through any single provider offers no 133 | more security than sending the key itself. 134 | 135 | * Use public key cryptography to encrypt secret shares with the public key of 136 | each individual recipient. This can protect the share data from unwanted use while 137 | in transit or at rest. OpenPGP would be one such tool for encrypting shares to 138 | send safely. 139 | 140 | * Put careful thought into how you want to distribute shares. It often makes 141 | sense to give individuals more than one share. 142 | 143 | ## Documentation 144 | 145 | There is pretty extensive inline documentation. You can view the latest 146 | auto-generated docs at [http://www.rubydoc.info/gems/tss](http://www.rubydoc.info/gems/tss) 147 | 148 | ## Supported Ruby Versions 149 | 150 | TSS is continuously integration tested on a number of Ruby versions. See the file 151 | `.travis.yml` in the root of this repository for the currently tested versions. 152 | 153 | ## Installation 154 | 155 | Add this line to your application's `Gemfile`: 156 | 157 | ```ruby 158 | gem 'tss', '~> 0.4' 159 | ``` 160 | 161 | And then execute: 162 | ```sh 163 | $ bundle 164 | ``` 165 | 166 | Or install it yourself as: 167 | 168 | ```sh 169 | $ gem install tss 170 | ``` 171 | 172 | ### Installation Security : Signed Ruby Gem 173 | 174 | The TSS gem is cryptographically signed. To be sure the gem you install hasn’t 175 | been tampered with you can install it using the following method: 176 | 177 | Add my public key (if you haven’t already) as a trusted certificate 178 | 179 | ``` 180 | # Caveat: Gem certificates are trusted globally, such that adding a 181 | # cert.pem for one gem automatically trusts all gems signed by that cert. 182 | gem cert --add <(curl -Ls https://raw.github.com/grempe/tss-rb/master/certs/gem-public_cert_grempe_2026.pem) 183 | ``` 184 | 185 | To install, it is possible to specify either `HighSecurity` or `MediumSecurity` 186 | mode. Since the `tss` gem depends on one or more gems that are not cryptographically 187 | signed you will likely need to use `MediumSecurity`. You should receive a warning 188 | if any signed gem does not match its signature. 189 | 190 | ``` 191 | # All dependent gems must be signed and verified. 192 | gem install tss -P HighSecurity 193 | ``` 194 | 195 | ``` 196 | # All signed dependent gems must be verified. 197 | gem install tss -P MediumSecurity 198 | ``` 199 | 200 | ``` 201 | # Same as above, except Bundler only recognizes 202 | # the long --trust-policy flag, not the short -P 203 | bundle --trust-policy MediumSecurity 204 | ``` 205 | 206 | You can [learn more about security and signed Ruby Gems](http://guides.rubygems.org/security/). 207 | 208 | ### Installation Security : Signed Git Commits 209 | 210 | Most, if not all, of the commits and tags to the repository for this code are 211 | signed with my PGP/GPG code signing key. I have uploaded my code signing public 212 | keys to GitHub and you can now verify those signatures with the GitHub UI. 213 | See [this list of commits](https://github.com/grempe/tss-rb/commits/master) 214 | and look for the `Verified` tag next to each commit. You can click on that tag 215 | for additional information. 216 | 217 | You can also clone the repository and verify the signatures locally using your 218 | own GnuPG installation. You can find my certificates and read about how to conduct 219 | this verification at [https://www.rempe.us/keys/](https://www.rempe.us/keys/). 220 | 221 | ## Command Line Interface (CLI) 222 | 223 | When you install the gem a simple `tss` command-line interface (CLI) 224 | is also installed and should be available on your `$PATH`. 225 | 226 | The CLI is a user interface for splitting and combining secrets. You can 227 | see the full set of options available with `tss help`, `tss help split`, 228 | or `tss help combine`. 229 | 230 | ### CLI Secret Splitting 231 | 232 | A secret to be split can be provided using one of three 233 | different input methods; `STDIN`, a path to a file, or when prompted 234 | interactively. In all cases the secret should be `UTF-8` or 235 | `US-ASCII` encoded text and be no larger than 65,535 Bytes 236 | (including header and hash verification bytes). 237 | 238 | Each method for entering a secret may have pros and cons from a security 239 | perspective. You need to understand what threat model you want to protect 240 | against in order to choose wisely. Here are a few examples for how to 241 | split a secret. 242 | 243 | **Example : `STDIN`** 244 | 245 | Can read the secret from `STDIN` and write the split shares to a file. Be 246 | cautioned that this method may leave command history which may contain your 247 | secret. 248 | 249 | ```text 250 | echo 'a secret' | tss split -O /tmp/shares.txt 251 | ``` 252 | 253 | **Example : `--input-file`** 254 | 255 | Can read the secret from a file and write the split shares to `STDOUT`. Be 256 | cautioned that storing the secret on a filesystem may expose you to certain 257 | attacks since it can be hard to fully erase files once written. 258 | 259 | ```text 260 | $ cat /tmp/secret.txt 261 | my secret 262 | $ tss split -I /tmp/secret.txt 263 | tss~v1~8b66eb89ee25a46c~3~OGI2NmViODllZTI1YTQ2YwIDACsBqgQvwFhKboLMAmYwF5_CvaUwM8pmqUipbMRzakdbP53WJa-E6tf2nl2M 264 | tss~v1~8b66eb89ee25a46c~3~OGI2NmViODllZTI1YTQ2YwIDACsCk1EU-HwvC6pjvQ5wDas221Qs4A7TtJNpi-Su8hxOqrsCSQ8t11aiQUpm 265 | tss~v1~8b66eb89ee25a46c~3~OGI2NmViODllZTI1YTQ2YwIDACsDVCwbS0EGF03btYwqqVuk7S0z-nSinHtPpsFaFm5dys85j3JlK-yCVNyP 266 | tss~v1~8b66eb89ee25a46c~3~OGI2NmViODllZTI1YTQ2YwIDACsEPUzgi8CDn4ix1dLQTQDj2yTdeeQcZAvGuMyQ_H7EepN6WO_KB1DuqTxm 267 | tss~v1~8b66eb89ee25a46c~3~OGI2NmViODllZTI1YTQ2YwIDACsF-jHvOP2qg28J3VCK6fBx7V3CY55tTOPglelkGAzXGudBnpKC--rOvKqP 268 | ``` 269 | 270 | **Example : `interactive`** 271 | 272 | Can read the secret interactively from a terminal and write the split shares 273 | to `STDOUT`. This method is more secure since the secret will not be stored 274 | in the command shell history or in a file on a filesystem that is hard to 275 | erase securely. It does not protect you from simple keylogger attacks though. 276 | 277 | ```text 278 | $ tss split 279 | Enter your secret, enter a dot (.) on a line by itself to finish : 280 | secret > the vault password is: 281 | secret > V0ulT! 282 | secret > . 283 | tss~v1~1f8532a9d185efc4~3~MWY4NTMyYTlkMTg1ZWZjNAIDAD8BJkMvkHUGPQk1HQr_jVj9JkhavJVkdeFDEtRAf8O6vjksCjMMjiyuUL0bOD7vDnmdrzHK8QVymXU5vnjAREs= 284 | tss~v1~1f8532a9d185efc4~3~MWY4NTMyYTlkMTg1ZWZjNAIDAD8CJpiKCEtSfmugr_gbvEIBs2M2sZfHxuZsBuPpB35OGeyWmGZCyJCNu0W2z3abkV1Q8uTPvT75YYRpw6BcdvQ= 285 | tss~v1~1f8532a9d185efc4~3~MWY4NTMyYTlkMTg1ZWZjNAIDAD8DdLPAuEg1Ng7hkoKFQmmL-lkILWvQiQ15JELFLJz-PvdZucDCqS-X6QAIbwWE3u2XrTgsH1kmIvpMeZhzfPk= 286 | tss~v1~1f8532a9d185efc4~3~MWY4NTMyYTlkMTg1ZWZjNAIDAD8EJT1XJ-DeOXuE3JD6VpZk79945_peSk1ij1Mk5ql-l2jkv85yGBbyH0VIBmJp2WsEMmESZqTsQ9kKB-0Ljq8= 287 | tss~v1~1f8532a9d185efc4~3~MWY4NTMyYTlkMTg1ZWZjNAIDAD8FdxYdl-O5cR7F4epkqL3upuVGewZJBaZ3rfIIzUvOsHMrnmjyeanoTQD2phF2ltvDbb3xxMMzAKcvvdUkhKI= 288 | ``` 289 | 290 | ### CLI Share Combining 291 | 292 | You can use the CLI to enter shares in order to recover a secret. Of course 293 | you will need at least the number of shares necessary as determined 294 | by the threshold when your shares were created. The `threshold` is visible 295 | as the third field in every `HUMAN` formatted share. 296 | 297 | As with splitting a secret, there are also three methods of getting the shares 298 | into the CLI. `STDIN`, a path to a file containing shares, or interactively. 299 | 300 | Here are some simple examples of using each: 301 | 302 | **Example : `STDIN`** 303 | 304 | ```text 305 | $ echo 'a secret' | tss split | tss combine 306 | 307 | RECOVERED SECRET METADATA 308 | ************************* 309 | hash : d4ea4551e9ff2cf56303875b1901fb8608a6164260c3b20c0976c7b606a4efc0 310 | hash_alg : SHA256 311 | identifier : fff58c38b14734f3 312 | process_time : 0.34ms 313 | threshold : 3 314 | secret : 315 | a secret 316 | ``` 317 | 318 | **Example : `--input-file`** 319 | 320 | ```text 321 | $ echo 'a secret' | tss split -O /tmp/shares.txt 322 | $ tss combine -I /tmp/shares.txt 323 | 324 | RECOVERED SECRET METADATA 325 | ************************* 326 | hash : d4ea4551e9ff2cf56303875b1901fb8608a6164260c3b20c0976c7b606a4efc0 327 | hash_alg : SHA256 328 | identifier : ae2983e30e0471fe 329 | process_time : 0.47ms 330 | threshold : 3 331 | secret : 332 | a secret 333 | ``` 334 | 335 | **Example : `interactive`** 336 | 337 | ```text 338 | $ cat /tmp/shares.txt 339 | # THRESHOLD SECRET SHARING SHARES 340 | # 2017-01-29T02:01:00Z 341 | # https://github.com/grempe/tss-rb 342 | 343 | 344 | tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoBbga2N__A9QOleLhC5R5b7stJ26iMPNpQ95YpmK-tWIrd-CYcrXGmcys= 345 | tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoCGXsZkLOG7uDOVN1Zn46pqftX4noA8LfXugg14KBfSggYP9fw4Ce10YA= 346 | tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoDFl3cwi80fpdh-I9eK3kNa8V9OlXX1Wx8y5a6bk2S0TDJzocr-1C3TWs= 347 | tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoEEspmyzmbnrSr7v41FFlHVTqQ6dx4aho8q5T1CBHQbNimdCv3gVXS50I= 348 | tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoFHeyjmaUpDsMEQqwyoK7jlwS6MfOvT8GX2gp6hvwd9-B3hXssmiLQe6k= 349 | 350 | $ tss combine 351 | Enter shares, one per line, and a dot (.) on a line by itself to finish : 352 | share> tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoBbga2N__A9QOleLhC5R5b7stJ26iMPNpQ95YpmK-tWIrd-CYcrXGmcys= 353 | share> tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoCGXsZkLOG7uDOVN1Zn46pqftX4noA8LfXugg14KBfSggYP9fw4Ce10YA= 354 | share> tss~v1~b9f7f87bc83fd89b~3~YjlmN2Y4N2JjODNmZDg5YgIDACoDFl3cwi80fpdh-I9eK3kNa8V9OlXX1Wx8y5a6bk2S0TDJzocr-1C3TWs= 355 | share> . 356 | 357 | RECOVERED SECRET METADATA 358 | ************************* 359 | hash : d4ea4551e9ff2cf56303875b1901fb8608a6164260c3b20c0976c7b606a4efc0 360 | hash_alg : SHA256 361 | identifier : b9f7f87bc83fd89b 362 | process_time : 1.22ms 363 | threshold : 3 364 | secret : 365 | a secret 366 | ``` 367 | 368 | ## Ruby : Splitting a Secret 369 | 370 | The basic usage is as follows using the arguments described below. 371 | 372 | ```ruby 373 | shares = TSS.split(secret: secret, 374 | threshold: threshold, 375 | num_shares: num_shares, 376 | identifier: identifier, 377 | hash_alg: hash_alg, 378 | padding: true) 379 | ``` 380 | 381 | ### Arguments 382 | 383 | All arguments are passed as keys in a single Hash. 384 | 385 | The `secret` (required) value must be provided as a String with either 386 | a `UTF-8` or `US-ASCII` encoding. The Byte length must be `<= 65,486`. You can 387 | test this beforehand with `'my string secret'.bytes.to_a.length`. Keep in mind 388 | that this length also includes padding and the verification hash so your 389 | secret may need to be shorter depending on the settings you choose. 390 | 391 | Internally, the `secret` String will be converted to and processed as an Array 392 | of Bytes. e.g. `'foo'.bytes.to_a` 393 | 394 | The `num_shares` and `threshold` values are Integers representing the total 395 | number of shares desired, and how many of those shares are required to 396 | re-create a `secret`. Both arguments must be Integers with a value 397 | between `1-255` inclusive. The `num_shares` value must be 398 | greater-than-or-equal-to the `threshold` value. If you don't pass in 399 | these options they will be set to `threshold = 3` and `num_shares = 5` by default. 400 | 401 | The `identifier` is a `1-16` Byte String that will be embedded in 402 | each output share and should uniquely identify a secret. All shares output 403 | from the same secret splitting operation will have the same `identifier`. This 404 | value can be retrieved easily from a share header and should be assumed to be 405 | known to shareholders. Nothing that leaks information about the secret should 406 | be used as an `identifier`. If an `identifier` is not set, it will default 407 | to the output of `SecureRandom.hex(8)` which is 8 random hex bytes (16 characters). 408 | 409 | The `hash_alg` is a String that represents which cryptographic one-way 410 | hash function should be embedded in shares. The hash is used to verify 411 | that the re-combined secret is a match for the original at creation time. 412 | The value of the hash is not available to shareholders until a secret is 413 | successfully re-combined. The hash is calculated from the original secret 414 | and then combined with it prior to secret splitting. This means that the hash 415 | is protected the same way as the secret. The algorithm used is 416 | `secret || hash(secret)`. You can use one of `NONE`, `SHA1`, or `SHA256`. 417 | 418 | The `format` arg takes an uppercase String Enum with either `'HUMAN'` (default) or 419 | `'BINARY'` values. This instructs the output of a split to either provide an 420 | array of binary octet strings (a standard RTSS format for interoperability), or 421 | a human friendly URL Safe Base 64 encoded version of that same binary output. 422 | The `HUMAN` format can be easily shared in a tweet, email, or even a URL. The 423 | `HUMAN` format is prefixed with `tss~VERSION~IDENTIFIER~THRESHOLD~` to make it 424 | easier to visually compare shares and see if they have matching identifiers and 425 | if you have enough shares to reach the threshold. 426 | 427 | The `padding` arg accepts a Boolean to indicate whether to apply PKCS#7 428 | padding to the secret string. This is applied and removed automatically 429 | by default and padding defaults to a block size of 16 bytes. You probably 430 | never need to use this option to turn it off unless you are trying to 431 | trade shares with the Python implementation. 432 | 433 | Since TSS share data is essentially the same size as the original secret 434 | (with a known size header), the padding applied to smaller secrets may help 435 | mask the exact size of the secret itself from an attacker. Padding is not part of 436 | the RTSS spec so other TSS clients won't strip off the padding and may fail 437 | when recombining shares. 438 | 439 | ### Example Usage 440 | 441 | ```ruby 442 | secret = 'foo bar baz' 443 | threshold = 3 444 | num_shares = 5 445 | identifier = SecureRandom.hex(8) 446 | hash_alg = 'SHA256' 447 | format = 'HUMAN' 448 | 449 | s = TSS.split(secret: secret, threshold: threshold, num_shares: num_shares, identifier: identifier, hash_alg: 'SHA256', format: format) 450 | 451 | => ["tss~v1~79923b087dab7fa2~3~Nzk5MjNiMDg3ZGFiN2ZhMgIDADEB2qA6IYq8yOGlPAl0B4MgRsVazZMWGLwRNgGMPKutOYbB0gjkVHNqbNYl-0l1f98W", 452 | "tss~v1~79923b087dab7fa2~3~Nzk5MjNiMDg3ZGFiN2ZhMgIDADECvjwdUHc8MzqvIllR2Rj9TnnlN_2eRUzH6MUsd8ncua4jpXQ3FgM1hUmLHmrgHq0u", 453 | "tss~v1~79923b087dab7fa2~3~Nzk5MjNiMDg3ZGFiN2ZhMgIDADEDAvNIUZ_hiftofyog257YDWds4q9MP14-rDCxQsauUyxqBtzur6Ch5-rSCHRPt4Dv", 454 | "tss~v1~79923b087dab7fa2~3~Nzk5MjNiMDg3ZGFiN2ZhMgIDADEEF7zGEx0GSC6YLgVD6xcQispDCO_JTUSDFbsbpalopakh0FmTfmO-JJKGQSlJb1il", 455 | "tss~v1~79923b087dab7fa2~3~Nzk5MjNiMDg3ZGFiN2ZhMgIDADEFq3OTEvXb8u9fc3Yy6ZE1ydTK3b0bN1Z6UU6GkKYaTytoc_FKx8AqRjHfVzfmxnVk"] 456 | 457 | secret = TSS.combine(shares: s) 458 | 459 | => {:hash=>"dbd318c1c462aee872f41109a4dfd3048871a03dedd0fe0e757ced57dad6f2d7", 460 | :hash_alg=>"SHA256", 461 | :identifier=>"79923b087dab7fa2", 462 | :process_time=>0.92, 463 | :secret=>"foo bar baz", 464 | :threshold=>3} 465 | ``` 466 | 467 | ## Ruby : Combining Shares to Recreate a Secret 468 | 469 | The basic usage is: 470 | 471 | ```ruby 472 | secret = TSS.combine(shares: shares) 473 | ``` 474 | 475 | ### Arguments 476 | 477 | All arguments are passed as keys in a single Hash. The return value is either 478 | a Hash (with the `:secret` key being most important and other metadata provided 479 | for informational purposes), or an `TSS::Error` Exception if the secret could 480 | not be created and verified with its hash. 481 | 482 | `shares:` (required) : Must be provided as an Array of encoded Share Byte Strings. 483 | You must provide at least `threshold` shares as specified when the secret was 484 | split. Providing too few shares will result in a `TSS::ArgumentError` exception 485 | being raised. There are a large number of verifications that are performed on 486 | shares provided to make sure they are valid and consistent with each other. An 487 | Exception will be raised if any of these tests fail. 488 | 489 | `select_by:` : If the number of shares provided as input to the secret 490 | reconstruction operation is greater than the threshold M, then M 491 | of those shares are selected for use in the operation. The method 492 | used to select the shares can be chosen with the `select_by:` argument 493 | which takes the following values as options: 494 | 495 | `select_by: 'FIRST'` : If X shares are required by the threshold and more than X 496 | shares are provided, then the first X shares in the Array of shares provided 497 | will be used. All others will be discarded and the operation will fail if 498 | those selected shares cannot recreate the secret. 499 | 500 | `select_by: 'SAMPLE'` : If X shares are required by the threshold and more than X 501 | shares are provided, then X shares will be randomly selected from the Array 502 | of shares provided. All others will be discarded and the operation will 503 | fail if those selected shares cannot recreate the secret. 504 | 505 | `select_by: 'COMBINATIONS'` : If X shares are required, and more than X shares are 506 | provided, then all possible combinations of the threshold number of shares 507 | will be tried to see if the secret can be recreated. 508 | 509 | **Warning** 510 | 511 | This `COMBINATIONS` flexibility comes with a cost. All combinations of 512 | `threshold` shares must be generated before processing. Due to the math 513 | associated with combinations it is possible that the system would try to 514 | generate a number of combinations that could never be generated or processed 515 | in many times the life of the Universe. This option can only be used if the 516 | possible combinations for the number of shares and the threshold needed to 517 | reconstruct a secret result in a number of combinations that is small enough 518 | to have a chance at being processed. If the number of combinations will be too 519 | large an Exception will be raised before processing has even started. The default 520 | maximum number of combinations that will be tried is 1,000,000. 521 | 522 | **Fun Fact** 523 | 524 | If 255 total shares are available, and the threshold value is 128, it would result in 525 | `2884329411724603169044874178931143443870105850987581016304218283632259375395` 526 | possible combinations of 128 shares that could be tried. That is *almost* as 527 | many combinations (`2.88 * 10^75`) as there are Atoms in the Universe (`10^80`). 528 | 529 | If the combine operation does not result in a secret being successfully 530 | extracted, then a `TSS::Error` exception will be raised. 531 | 532 | A great short read on big numbers is 533 | [On the (Small) Number of Atoms in the Universe](http://norvig.com/atoms.html) 534 | 535 | ### Exception Handling 536 | 537 | Almost all methods in the program have strict contracts associated with them 538 | that enforce argument presence, types, and value boundaries. This contract checking 539 | is provided by the excellent Ruby [Contracts](https://egonschiele.github.io/contracts.ruby/) gem. If a contract violation occurs a `ParamContractError` Exception will be raised. 540 | 541 | The splitting and combining operations may also raise the following 542 | exception types: 543 | 544 | `TSS::NoSecretError`, `TSS::InvalidSecretHashError`, 545 | `TSS::ArgumentError`, `TSS::Error` 546 | 547 | All Exceptions should include hints as to what went wrong in the 548 | `#message` attribute. 549 | 550 | ## Share Data Formats 551 | 552 | ### RTSS Binary 553 | 554 | TSS provides shares in a binary data format with the following fields, and 555 | by default this binary data is wrapped in a `'HUMAN'` text format: 556 | 557 | `Identifier`. This field contains 16 octets. It identifies the secret 558 | with which a share is associated. All of the shares associated 559 | with a particular secret MUST use the same value Identifier. When 560 | a secret is reconstructed, the Identifier fields of each of the 561 | shares used as input MUST have the same value. The value of the 562 | Identifier should be chosen so that it is unique, but the details 563 | on how it is chosen are left as an exercise for the reader. The 564 | characters `a-zA-Z0-9.-_` are allowed in the identifier. 565 | 566 | `Hash Algorithm Identifier`. This field contains a single octet that 567 | indicates the hash function used in the RTSS processing, if any. 568 | A value of `0` indicates that no hash algorithm was used, no hash 569 | was appended to the secret, and no RTSS check should be performed 570 | after the reconstruction of the secret. 571 | 572 | `Threshold`. This field contains a single octet that indicates the 573 | number of shares required to reconstruct the secret. This field 574 | MUST be checked during the reconstruction process, and that 575 | process MUST halt and return an error if the number of shares 576 | available is fewer than the value indicated in this field. 577 | 578 | `Share Length`. This field is two octets long. It contains the number 579 | of octets in the Share Data field, represented as an unsigned 580 | integer in network byte order. 581 | 582 | `Share Data`. This field has a length that is a variable number of 583 | octets. It contains the actual share data. 584 | 585 | ``` 586 | 0 1 2 3 587 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 588 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 589 | | | 590 | | Identifier | 591 | | | 592 | | | 593 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 594 | | Hash Alg. Id. | Threshold | Share Length | 595 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 596 | : : 597 | : Share Data : 598 | : : 599 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 600 | ``` 601 | 602 | This data format has been tested for binary compatibility with the 603 | [seb-m/tss](https://github.com/seb-m/tss) Python implementation of TSS. There 604 | are test cases to ensure it remains compatible. 605 | 606 | ### RTSS Human Friendly Wrapper 607 | 608 | To make `tss` friendlier to use when sending shares to others, an enhanced 609 | text wrapper around the RTSS binary data format is provided. 610 | 611 | Shares formatted this way can easily be shared via most any communication channel. 612 | 613 | The `HUMAN` data format is simply the same RTSS binary data, URL Safe Base64 614 | encoded, and prefixed with a String thet contains tilde `~` separated elements. 615 | The `~` is used as it is compatible with the URL Safe data and the allowed 616 | characters in the rest of the human format string. 617 | 618 | ```text 619 | tss~VERSION~IDENTIFIER~THRESHOLD~BASE64_ENCODED_BINARY_SHARE 620 | ``` 621 | 622 | A typical share with this format looks like: 623 | 624 | ```text 625 | tss~v1~abc~3~YWJjAAAAAAAAAAAAAAAAAAIDACQB10mUbJPQZ94WpgKC2kKivfnSpCHTMr6BajbwzqOrvyMCpH0= 626 | ``` 627 | 628 | ## Performance 629 | 630 | The amount of time it takes to split or combine secrets grows significantly as 631 | the size of the secret and the total `num_shares` and `threshold` increase. 632 | Splitting a secret with the maximum size of `2**16 - 2` (65,534) Bytes and 633 | the maximum `255` shares may take an unreasonably long time to run. Splitting 634 | and Combining involves at least `num_shares**secret_bytes` operations so 635 | larger values can quickly result in huge processing time. If you need to 636 | split large secrets into a large number of shares you should consider 637 | running those operations in a background worker process or thread for 638 | best performance. For example a 64kb file split into 255 shares, 639 | with a threshold of 255 (the max for all three settings), can take 640 | 20 minutes to split and another 20 minutes to combine using a modern CPU. 641 | 642 | A reasonable set of values seems to be what I'll call the 'rule of 64'. If you 643 | keep the `secret <= 64 Bytes`, the `threshold <= 64`, and the `num_shares <= 64` 644 | you can do a round-trip split and combine operation in `~250ms`. These should 645 | be very reasonable and secure max values for most use cases, even as part of a 646 | web request response cycle. Remember, its recommended to split encryption keys, 647 | not plaintext. 648 | 649 | There are some simple benchmark tests to exercise things with `rake bench`. 650 | 651 | ## Development 652 | 653 | After checking out the repo, run `bin/setup` to install dependencies. Then, 654 | run `rake test` to run the tests. You can also run `bin/console` for an 655 | interactive prompt that will allow you to experiment. 656 | 657 | To install this gem onto your local machine, run `bundle exec rake install`. 658 | 659 | You can run the Command Line Interface (CLI) in development 660 | with `bundle exec bin/tss`. 661 | 662 | The formal release process can be found in [RELEASE.md](https://github.com/grempe/tss-rb/blob/master/RELEASE.md) 663 | 664 | ### Contributing 665 | 666 | Bug reports and pull requests are welcome on GitHub 667 | at [https://github.com/grempe/tss-rb](https://github.com/grempe/tss-rb). This 668 | project is intended to be a safe, welcoming space for collaboration, and 669 | contributors are expected to adhere to the 670 | [Contributor Covenant](http://contributor-covenant.org) code of conduct. 671 | 672 | ## Legal 673 | 674 | ### Copyright 675 | 676 | (c) 2016-2017 Glenn Rempe <[glenn@rempe.us](mailto:glenn@rempe.us)> ([https://www.rempe.us/](https://www.rempe.us/)) 677 | 678 | ### License 679 | 680 | The gem is available as open source under the terms of 681 | the [MIT License](http://opensource.org/licenses/MIT). 682 | 683 | ### Warranty 684 | 685 | Unless required by applicable law or agreed to in writing, 686 | software distributed under the License is distributed on an 687 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 688 | either express or implied. See the LICENSE.txt file for the 689 | specific language governing permissions and limitations under 690 | the License. 691 | 692 | ## Thank You! 693 | 694 | This code is an implementation of the Threshold Secret Sharing, as specified in 695 | the Network Working Group Internet-Draft submitted by D. McGrew 696 | ([draft-mcgrew-tss-03.txt](http://tools.ietf.org/html/draft-mcgrew-tss-03)). 697 | This code would not have been possible without this very well designed and 698 | documented specification. Many examples of the relevant text from the specification 699 | have been used as comments to annotate this source code. 700 | 701 | Great respect to Sébastien Martini ([@seb-m](https://github.com/seb-m)) for 702 | his [seb-m/tss](https://github.com/seb-m/tss) Python implementation of TSS. 703 | It was invaluable as a real-world reference implementation of the 704 | TSS Internet-Draft. 705 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | # Exclude: 3 | # - 'vendor/**/*' 4 | # - 'db/schema.rb' 5 | # - 'test/**/*' 6 | UseCache: false 7 | # DisabledByDefault: true 8 | 9 | #################### Lint ################################ 10 | 11 | Lint/AmbiguousOperator: 12 | Description: >- 13 | Checks for ambiguous operators in the first argument of a 14 | method invocation without parentheses. 15 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' 16 | Enabled: true 17 | 18 | Lint/AmbiguousRegexpLiteral: 19 | Description: >- 20 | Checks for ambiguous regexp literals in the first argument of 21 | a method invocation without parenthesis. 22 | Enabled: true 23 | 24 | Lint/AssignmentInCondition: 25 | Description: Don't use assignment in conditions. 26 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition 27 | Enabled: true 28 | AllowSafeAssignment: true 29 | 30 | Lint/BlockAlignment: 31 | Description: 'Align block ends correctly.' 32 | Enabled: true 33 | 34 | Lint/CircularArgumentReference: 35 | Description: "Don't refer to the keyword argument in the default value." 36 | Enabled: true 37 | 38 | Lint/ConditionPosition: 39 | Description: >- 40 | Checks for condition placed in a confusing position relative to 41 | the keyword. 42 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' 43 | Enabled: true 44 | 45 | Lint/Debugger: 46 | Description: 'Check for debugger calls.' 47 | Enabled: true 48 | 49 | Lint/DefEndAlignment: 50 | Description: 'Align ends corresponding to defs correctly.' 51 | Enabled: true 52 | 53 | Lint/DeprecatedClassMethods: 54 | Description: 'Check for deprecated class method calls.' 55 | Enabled: true 56 | 57 | Lint/DuplicateMethods: 58 | Description: 'Check for duplicate methods calls.' 59 | Enabled: true 60 | 61 | Lint/EachWithObjectArgument: 62 | Description: 'Check for immutable argument given to each_with_object.' 63 | Enabled: true 64 | 65 | Lint/ElseLayout: 66 | Description: 'Check for odd code arrangement in an else block.' 67 | Enabled: true 68 | 69 | Lint/EmptyEnsure: 70 | Description: 'Checks for empty ensure block.' 71 | Enabled: true 72 | 73 | Lint/EmptyInterpolation: 74 | Description: 'Checks for empty string interpolation.' 75 | Enabled: true 76 | 77 | Lint/EndAlignment: 78 | Description: 'Align ends correctly.' 79 | Enabled: true 80 | 81 | Lint/EndInMethod: 82 | Description: 'END blocks should not be placed inside method definitions.' 83 | Enabled: true 84 | 85 | Lint/EnsureReturn: 86 | Description: 'Do not use return in an ensure block.' 87 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure' 88 | Enabled: true 89 | 90 | Lint/Eval: 91 | Description: 'The use of eval represents a serious security risk.' 92 | Enabled: true 93 | 94 | Lint/FormatParameterMismatch: 95 | Description: 'The number of parameters to format/sprint must match the fields.' 96 | Enabled: true 97 | 98 | Lint/HandleExceptions: 99 | Description: "Don't suppress exception." 100 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' 101 | Enabled: true 102 | 103 | Lint/InvalidCharacterLiteral: 104 | Description: >- 105 | Checks for invalid character literals with a non-escaped 106 | whitespace character. 107 | Enabled: true 108 | 109 | Lint/LiteralInCondition: 110 | Description: 'Checks of literals used in conditions.' 111 | Enabled: true 112 | 113 | Lint/LiteralInInterpolation: 114 | Description: 'Checks for literals used in interpolation.' 115 | Enabled: true 116 | 117 | Lint/Loop: 118 | Description: >- 119 | Use Kernel#loop with break rather than begin/end/until or 120 | begin/end/while for post-loop tests. 121 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' 122 | Enabled: true 123 | 124 | Lint/NestedMethodDefinition: 125 | Description: 'Do not use nested method definitions.' 126 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' 127 | Enabled: true 128 | 129 | Lint/NonLocalExitFromIterator: 130 | Description: 'Do not use return in iterator to cause non-local exit.' 131 | Enabled: true 132 | 133 | Lint/ParenthesesAsGroupedExpression: 134 | Description: >- 135 | Checks for method calls with a space before the opening 136 | parenthesis. 137 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 138 | Enabled: true 139 | 140 | Lint/RequireParentheses: 141 | Description: >- 142 | Use parentheses in the method call to avoid confusion 143 | about precedence. 144 | Enabled: true 145 | 146 | Lint/RescueException: 147 | Description: 'Avoid rescuing the Exception class.' 148 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' 149 | Enabled: true 150 | 151 | Lint/ShadowingOuterLocalVariable: 152 | Description: >- 153 | Do not use the same name as outer local variable 154 | for block arguments or block local variables. 155 | Enabled: true 156 | 157 | Lint/StringConversionInInterpolation: 158 | Description: 'Checks for Object#to_s usage in string interpolation.' 159 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' 160 | Enabled: true 161 | 162 | Lint/UnderscorePrefixedVariableName: 163 | Description: 'Do not use prefix `_` for a variable that is used.' 164 | Enabled: true 165 | 166 | Lint/UnneededDisable: 167 | Description: >- 168 | Checks for rubocop:disable comments that can be removed. 169 | Note: this cop is not disabled when disabling all cops. 170 | It must be explicitly disabled. 171 | Enabled: true 172 | 173 | Lint/UnusedBlockArgument: 174 | Description: 'Checks for unused block arguments.' 175 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 176 | Enabled: true 177 | 178 | Lint/UnusedMethodArgument: 179 | Description: 'Checks for unused method arguments.' 180 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 181 | Enabled: true 182 | 183 | Lint/UnreachableCode: 184 | Description: 'Unreachable code.' 185 | Enabled: true 186 | 187 | Lint/UselessAccessModifier: 188 | Description: 'Checks for useless access modifiers.' 189 | Enabled: true 190 | 191 | Lint/UselessAssignment: 192 | Description: 'Checks for useless assignment to a local variable.' 193 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 194 | Enabled: true 195 | 196 | Lint/UselessComparison: 197 | Description: 'Checks for comparison of something with itself.' 198 | Enabled: true 199 | 200 | Lint/UselessElseWithoutRescue: 201 | Description: 'Checks for useless `else` in `begin..end` without `rescue`.' 202 | Enabled: true 203 | 204 | Lint/UselessSetterCall: 205 | Description: 'Checks for useless setter call to a local variable.' 206 | Enabled: true 207 | 208 | Lint/Void: 209 | Description: 'Possible use of operator/literal/variable in void context.' 210 | Enabled: true 211 | 212 | ###################### Metrics #################################### 213 | 214 | Metrics/AbcSize: 215 | Description: 'A calculated magnitude based on number of assignments, branches, and conditions.' 216 | Enabled: false 217 | Max: 15 218 | 219 | Metrics/BlockNesting: 220 | Description: 'Avoid excessive block nesting' 221 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' 222 | Enabled: true 223 | Max: 4 224 | 225 | Metrics/ClassLength: 226 | Description: Avoid classes longer than 100 lines of code. 227 | Enabled: false 228 | CountComments: false 229 | Max: 100 230 | 231 | Metrics/CyclomaticComplexity: 232 | Description: A complexity metric that is strongly correlated to the number of test 233 | cases needed to validate a method. 234 | Enabled: true 235 | Max: 6 236 | 237 | Metrics/LineLength: 238 | Description: 'Limit lines to 80 characters.' 239 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' 240 | Enabled: false 241 | 242 | Metrics/MethodLength: 243 | Description: Avoid methods longer than 10 lines of code. 244 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods 245 | Enabled: false 246 | CountComments: false 247 | Max: 10 248 | 249 | Metrics/ModuleLength: 250 | Description: 'Avoid modules longer than 250 lines of code.' 251 | Enabled: true 252 | Max: 250 253 | 254 | Metrics/ParameterLists: 255 | Description: 'Avoid parameter lists longer than three or four parameters.' 256 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' 257 | Enabled: true 258 | Max: 5 259 | CountKeywordArgs: true 260 | 261 | Metrics/PerceivedComplexity: 262 | Description: A complexity metric geared towards measuring complexity for a human 263 | reader. 264 | Enabled: false 265 | Max: 7 266 | 267 | ##################### Performance ############################# 268 | 269 | Performance/Count: 270 | Description: >- 271 | Use `count` instead of `select...size`, `reject...size`, 272 | `select...count`, `reject...count`, `select...length`, 273 | and `reject...length`. 274 | Enabled: true 275 | 276 | Performance/Detect: 277 | Description: >- 278 | Use `detect` instead of `select.first`, `find_all.first`, 279 | `select.last`, and `find_all.last`. 280 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code' 281 | Enabled: true 282 | 283 | Performance/FlatMap: 284 | Description: >- 285 | Use `Enumerable#flat_map` 286 | instead of `Enumerable#map...Array#flatten(1)` 287 | or `Enumberable#collect..Array#flatten(1)` 288 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code' 289 | Enabled: true 290 | EnabledForFlattenWithoutParams: false 291 | # If enabled, this cop will warn about usages of 292 | # `flatten` being called without any parameters. 293 | # This can be dangerous since `flat_map` will only flatten 1 level, and 294 | # `flatten` without any parameters can flatten multiple levels. 295 | 296 | Performance/ReverseEach: 297 | Description: 'Use `reverse_each` instead of `reverse.each`.' 298 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' 299 | Enabled: true 300 | 301 | Performance/Sample: 302 | Description: >- 303 | Use `sample` instead of `shuffle.first`, 304 | `shuffle.last`, and `shuffle[Fixnum]`. 305 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' 306 | Enabled: true 307 | 308 | Performance/Size: 309 | Description: >- 310 | Use `size` instead of `count` for counting 311 | the number of elements in `Array` and `Hash`. 312 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code' 313 | Enabled: true 314 | 315 | Performance/StringReplacement: 316 | Description: >- 317 | Use `tr` instead of `gsub` when you are replacing the same 318 | number of characters. Use `delete` instead of `gsub` when 319 | you are deleting characters. 320 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code' 321 | Enabled: true 322 | 323 | ##################### Rails ################################## 324 | 325 | Rails/ActionFilter: 326 | Description: 'Enforces consistent use of action filter methods.' 327 | Enabled: false 328 | 329 | Rails/Date: 330 | Description: >- 331 | Checks the correct usage of date aware methods, 332 | such as Date.today, Date.current etc. 333 | Enabled: false 334 | 335 | Rails/Delegate: 336 | Description: 'Prefer delegate method for delegations.' 337 | Enabled: false 338 | 339 | Rails/FindBy: 340 | Description: 'Prefer find_by over where.first.' 341 | Enabled: false 342 | 343 | Rails/FindEach: 344 | Description: 'Prefer all.find_each over all.find.' 345 | Enabled: false 346 | 347 | Rails/HasAndBelongsToMany: 348 | Description: 'Prefer has_many :through to has_and_belongs_to_many.' 349 | Enabled: false 350 | 351 | Rails/Output: 352 | Description: 'Checks for calls to puts, print, etc.' 353 | Enabled: false 354 | 355 | Rails/ReadWriteAttribute: 356 | Description: >- 357 | Checks for read_attribute(:attr) and 358 | write_attribute(:attr, val). 359 | Enabled: false 360 | 361 | Rails/ScopeArgs: 362 | Description: 'Checks the arguments of ActiveRecord scopes.' 363 | Enabled: false 364 | 365 | Rails/TimeZone: 366 | Description: 'Checks the correct usage of time zone aware methods.' 367 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' 368 | Reference: 'http://danilenko.org/2012/7/6/rails_timezones' 369 | Enabled: false 370 | 371 | Rails/Validation: 372 | Description: 'Use validates :attribute, hash of validations.' 373 | Enabled: false 374 | 375 | ################## Style ################################# 376 | 377 | Style/AccessModifierIndentation: 378 | Description: Check indentation of private/protected visibility modifiers. 379 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected' 380 | Enabled: false 381 | 382 | Style/AccessorMethodName: 383 | Description: Check the naming of accessor methods for get_/set_. 384 | Enabled: false 385 | 386 | Style/Alias: 387 | Description: 'Use alias_method instead of alias.' 388 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' 389 | Enabled: false 390 | 391 | Style/AlignArray: 392 | Description: >- 393 | Align the elements of an array literal if they span more than 394 | one line. 395 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' 396 | Enabled: false 397 | 398 | Style/AlignHash: 399 | Description: >- 400 | Align the elements of a hash literal if they span more than 401 | one line. 402 | Enabled: false 403 | 404 | Style/AlignParameters: 405 | Description: >- 406 | Align the parameters of a method call if they span more 407 | than one line. 408 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' 409 | Enabled: false 410 | 411 | Style/AndOr: 412 | Description: 'Use &&/|| instead of and/or.' 413 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or' 414 | Enabled: false 415 | 416 | Style/ArrayJoin: 417 | Description: 'Use Array#join instead of Array#*.' 418 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' 419 | Enabled: false 420 | 421 | Style/AsciiComments: 422 | Description: 'Use only ascii symbols in comments.' 423 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' 424 | Enabled: false 425 | 426 | Style/AsciiIdentifiers: 427 | Description: 'Use only ascii symbols in identifiers.' 428 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' 429 | Enabled: false 430 | 431 | Style/Attr: 432 | Description: 'Checks for uses of Module#attr.' 433 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' 434 | Enabled: false 435 | 436 | Style/BeginBlock: 437 | Description: 'Avoid the use of BEGIN blocks.' 438 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' 439 | Enabled: false 440 | 441 | Style/BarePercentLiterals: 442 | Description: 'Checks if usage of %() or %Q() matches configuration.' 443 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand' 444 | Enabled: false 445 | 446 | Style/BlockComments: 447 | Description: 'Do not use block comments.' 448 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' 449 | Enabled: false 450 | 451 | Style/BlockEndNewline: 452 | Description: 'Put end statement of multiline block on its own line.' 453 | Enabled: false 454 | 455 | Style/BlockDelimiters: 456 | Description: >- 457 | Avoid using {...} for multi-line blocks (multiline chaining is 458 | always ugly). 459 | Prefer {...} over do...end for single-line blocks. 460 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 461 | Enabled: false 462 | 463 | Style/BracesAroundHashParameters: 464 | Description: 'Enforce braces style around hash parameters.' 465 | Enabled: false 466 | 467 | Style/CaseEquality: 468 | Description: 'Avoid explicit use of the case equality operator(===).' 469 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' 470 | Enabled: false 471 | 472 | Style/CaseIndentation: 473 | Description: 'Indentation of when in a case/when/[else/]end.' 474 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case' 475 | Enabled: false 476 | 477 | Style/CharacterLiteral: 478 | Description: 'Checks for uses of character literals.' 479 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' 480 | Enabled: false 481 | 482 | Style/ClassAndModuleCamelCase: 483 | Description: 'Use CamelCase for classes and modules.' 484 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' 485 | Enabled: false 486 | 487 | Style/ClassAndModuleChildren: 488 | Description: 'Checks style of children classes and modules.' 489 | Enabled: false 490 | 491 | Style/ClassCheck: 492 | Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' 493 | Enabled: false 494 | 495 | Style/ClassMethods: 496 | Description: 'Use self when defining module/class methods.' 497 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods' 498 | Enabled: false 499 | 500 | Style/ClassVars: 501 | Description: 'Avoid the use of class variables.' 502 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' 503 | Enabled: false 504 | 505 | Style/ClosingParenthesisIndentation: 506 | Description: 'Checks the indentation of hanging closing parentheses.' 507 | Enabled: false 508 | 509 | Style/CollectionMethods: 510 | Description: Preferred collection methods. 511 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size' 512 | Enabled: true 513 | PreferredMethods: 514 | collect: map 515 | collect!: map! 516 | find: detect 517 | find_all: select 518 | reduce: inject 519 | 520 | Style/ColonMethodCall: 521 | Description: 'Do not use :: for method call.' 522 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' 523 | Enabled: false 524 | 525 | Style/CommandLiteral: 526 | Description: 'Use `` or %x around command literals.' 527 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' 528 | Enabled: false 529 | 530 | Style/CommentAnnotation: 531 | Description: 'Checks formatting of annotation comments.' 532 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' 533 | Enabled: false 534 | 535 | Style/CommentIndentation: 536 | Description: 'Indentation of comments.' 537 | Enabled: false 538 | 539 | Style/ConstantName: 540 | Description: 'Constants should use SCREAMING_SNAKE_CASE.' 541 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' 542 | Enabled: false 543 | 544 | Style/DefWithParentheses: 545 | Description: 'Use def with parentheses when there are arguments.' 546 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 547 | Enabled: false 548 | 549 | Style/DeprecatedHashMethods: 550 | Description: 'Checks for use of deprecated Hash methods.' 551 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key' 552 | Enabled: false 553 | 554 | Style/Documentation: 555 | Description: 'Document classes and non-namespace modules.' 556 | Enabled: false 557 | 558 | Style/DotPosition: 559 | Description: Checks the position of the dot in multi-line method calls. 560 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' 561 | Enabled: true 562 | EnforcedStyle: leading 563 | SupportedStyles: 564 | - leading 565 | - trailing 566 | 567 | Style/DoubleNegation: 568 | Description: 'Checks for uses of double negation (!!).' 569 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' 570 | Enabled: false 571 | 572 | Style/EachWithObject: 573 | Description: 'Prefer `each_with_object` over `inject` or `reduce`.' 574 | Enabled: false 575 | 576 | Style/ElseAlignment: 577 | Description: 'Align elses and elsifs correctly.' 578 | Enabled: false 579 | 580 | Style/EmptyElse: 581 | Description: 'Avoid empty else-clauses.' 582 | Enabled: false 583 | 584 | Style/EmptyLineBetweenDefs: 585 | Description: 'Use empty lines between defs.' 586 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' 587 | Enabled: false 588 | 589 | Style/EmptyLines: 590 | Description: "Don't use several empty lines in a row." 591 | Enabled: false 592 | 593 | Style/EmptyLinesAroundAccessModifier: 594 | Description: "Keep blank lines around access modifiers." 595 | Enabled: false 596 | 597 | Style/EmptyLinesAroundBlockBody: 598 | Description: "Keeps track of empty lines around block bodies." 599 | Enabled: false 600 | 601 | Style/EmptyLinesAroundClassBody: 602 | Description: "Keeps track of empty lines around class bodies." 603 | Enabled: false 604 | 605 | Style/EmptyLinesAroundModuleBody: 606 | Description: "Keeps track of empty lines around module bodies." 607 | Enabled: false 608 | 609 | Style/EmptyLinesAroundMethodBody: 610 | Description: "Keeps track of empty lines around method bodies." 611 | Enabled: false 612 | 613 | Style/EmptyLiteral: 614 | Description: 'Prefer literals to Array.new/Hash.new/String.new.' 615 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' 616 | Enabled: false 617 | 618 | Style/EndBlock: 619 | Description: 'Avoid the use of END blocks.' 620 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' 621 | Enabled: false 622 | 623 | Style/EndOfLine: 624 | Description: 'Use Unix-style line endings.' 625 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' 626 | Enabled: false 627 | 628 | Style/EvenOdd: 629 | Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' 630 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 631 | Enabled: false 632 | 633 | Style/ExtraSpacing: 634 | Description: 'Do not use unnecessary spacing.' 635 | Enabled: false 636 | 637 | Style/FileName: 638 | Description: Use snake_case for source file names. 639 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' 640 | Enabled: false 641 | Exclude: [] 642 | 643 | Style/InitialIndentation: 644 | Description: >- 645 | Checks the indentation of the first non-blank non-comment line in a file. 646 | Enabled: false 647 | 648 | Style/FirstParameterIndentation: 649 | Description: 'Checks the indentation of the first parameter in a method call.' 650 | Enabled: false 651 | 652 | Style/FlipFlop: 653 | Description: 'Checks for flip flops' 654 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' 655 | Enabled: false 656 | 657 | Style/For: 658 | Description: 'Checks use of for or each in multiline loops.' 659 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops' 660 | Enabled: false 661 | 662 | Style/FormatString: 663 | Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' 664 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' 665 | Enabled: false 666 | 667 | Style/GlobalVars: 668 | Description: 'Do not introduce global variables.' 669 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' 670 | Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html' 671 | Enabled: false 672 | 673 | Style/GuardClause: 674 | Description: 'Check for conditionals that can be replaced with guard clauses' 675 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 676 | Enabled: false 677 | MinBodyLength: 1 678 | 679 | Style/HashSyntax: 680 | Description: >- 681 | Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax 682 | { :a => 1, :b => 2 }. 683 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals' 684 | Enabled: false 685 | 686 | Style/IfUnlessModifier: 687 | Description: 'Favor modifier if/unless usage when you have a single-line body.' 688 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' 689 | Enabled: false 690 | MaxLineLength: 80 691 | 692 | Style/IfWithSemicolon: 693 | Description: 'Do not use if x; .... Use the ternary operator instead.' 694 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' 695 | Enabled: false 696 | 697 | Style/IndentationConsistency: 698 | Description: 'Keep indentation straight.' 699 | Enabled: false 700 | 701 | Style/IndentationWidth: 702 | Description: 'Use 2 spaces for indentation.' 703 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 704 | Enabled: false 705 | 706 | Style/IndentArray: 707 | Description: >- 708 | Checks the indentation of the first element in an array 709 | literal. 710 | Enabled: false 711 | 712 | Style/IndentHash: 713 | Description: 'Checks the indentation of the first key in a hash literal.' 714 | Enabled: false 715 | 716 | Style/InfiniteLoop: 717 | Description: 'Use Kernel#loop for infinite loops.' 718 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop' 719 | Enabled: false 720 | 721 | Style/InlineComment: 722 | Description: Avoid inline comments. 723 | Enabled: false 724 | 725 | Style/Lambda: 726 | Description: 'Use the new lambda literal syntax for single-line blocks.' 727 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' 728 | Enabled: false 729 | 730 | Style/LambdaCall: 731 | Description: 'Use lambda.call(...) instead of lambda.(...).' 732 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' 733 | Enabled: false 734 | 735 | Style/LeadingCommentSpace: 736 | Description: 'Comments should start with a space.' 737 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' 738 | Enabled: false 739 | 740 | Style/LineEndConcatenation: 741 | Description: >- 742 | Use \ instead of + or << to concatenate two string literals at 743 | line end. 744 | Enabled: false 745 | 746 | Style/MethodCallWithoutArgsParentheses: 747 | Description: 'Do not use parentheses for method calls with no arguments.' 748 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' 749 | Enabled: false 750 | 751 | Style/MethodDefParentheses: 752 | Description: >- 753 | Checks if the method definitions have or don't have 754 | parentheses. 755 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 756 | Enabled: false 757 | 758 | Style/MethodName: 759 | Description: 'Use the configured style when naming methods.' 760 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 761 | Enabled: false 762 | 763 | Style/ModuleFunction: 764 | Description: 'Checks for usage of `extend self` in modules.' 765 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' 766 | Enabled: false 767 | 768 | Style/MultilineBlockChain: 769 | Description: 'Avoid multi-line chains of blocks.' 770 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 771 | Enabled: false 772 | 773 | Style/MultilineBlockLayout: 774 | Description: 'Ensures newlines after multiline block do statements.' 775 | Enabled: false 776 | 777 | Style/MultilineIfThen: 778 | Description: 'Do not use then for multi-line if/unless.' 779 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then' 780 | Enabled: false 781 | 782 | Style/MultilineOperationIndentation: 783 | Description: >- 784 | Checks indentation of binary operations that span more than 785 | one line. 786 | Enabled: false 787 | 788 | Style/MultilineTernaryOperator: 789 | Description: >- 790 | Avoid multi-line ?: (the ternary operator); 791 | use if/unless instead. 792 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary' 793 | Enabled: false 794 | 795 | Style/NegatedIf: 796 | Description: >- 797 | Favor unless over if for negative conditions 798 | (or control flow or). 799 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' 800 | Enabled: false 801 | 802 | Style/NegatedWhile: 803 | Description: 'Favor until over while for negative conditions.' 804 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' 805 | Enabled: false 806 | 807 | Style/NestedTernaryOperator: 808 | Description: 'Use one expression per branch in a ternary operator.' 809 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' 810 | Enabled: false 811 | 812 | Style/Next: 813 | Description: 'Use `next` to skip iteration instead of a condition at the end.' 814 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 815 | Enabled: false 816 | 817 | Style/NilComparison: 818 | Description: 'Prefer x.nil? to x == nil.' 819 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 820 | Enabled: false 821 | 822 | Style/NonNilCheck: 823 | Description: 'Checks for redundant nil checks.' 824 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' 825 | Enabled: false 826 | 827 | Style/Not: 828 | Description: 'Use ! instead of not.' 829 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' 830 | Enabled: false 831 | 832 | Style/NumericLiterals: 833 | Description: >- 834 | Add underscores to large numeric literals to improve their 835 | readability. 836 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' 837 | Enabled: false 838 | 839 | Style/OneLineConditional: 840 | Description: >- 841 | Favor the ternary operator(?:) over 842 | if/then/else/end constructs. 843 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' 844 | Enabled: false 845 | 846 | Style/OpMethod: 847 | Description: 'When defining binary operators, name the argument other.' 848 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' 849 | Enabled: false 850 | 851 | Style/OptionHash: 852 | Description: "Don't use option hashes when you can use keyword arguments." 853 | Enabled: false 854 | 855 | Style/OptionalArguments: 856 | Description: >- 857 | Checks for optional arguments that do not appear at the end 858 | of the argument list 859 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments' 860 | Enabled: false 861 | 862 | Style/ParallelAssignment: 863 | Description: >- 864 | Check for simple usages of parallel assignment. 865 | It will only warn when the number of variables 866 | matches on both sides of the assignment. 867 | This also provides performance benefits 868 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' 869 | Enabled: false 870 | 871 | Style/ParenthesesAroundCondition: 872 | Description: >- 873 | Don't use parentheses around the condition of an 874 | if/unless/while. 875 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' 876 | Enabled: false 877 | 878 | Style/PercentLiteralDelimiters: 879 | Description: 'Use `%`-literal delimiters consistently' 880 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' 881 | Enabled: false 882 | PreferredDelimiters: 883 | "%": "()" 884 | "%i": "()" 885 | "%q": "()" 886 | "%Q": "()" 887 | "%r": "{}" 888 | "%s": "()" 889 | "%w": "()" 890 | "%W": "()" 891 | "%x": "()" 892 | 893 | Style/PercentQLiterals: 894 | Description: 'Checks if uses of %Q/%q match the configured preference.' 895 | Enabled: false 896 | 897 | Style/PerlBackrefs: 898 | Description: 'Avoid Perl-style regex back references.' 899 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' 900 | Enabled: false 901 | 902 | Style/PredicateName: 903 | Description: 'Check the names of predicate methods.' 904 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' 905 | Enabled: true 906 | NamePrefix: 907 | - is_ 908 | - has_ 909 | - have_ 910 | NamePrefixBlacklist: 911 | - is_ 912 | Exclude: 913 | - spec/**/* 914 | 915 | Style/Proc: 916 | Description: 'Use proc instead of Proc.new.' 917 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' 918 | Enabled: false 919 | 920 | Style/RaiseArgs: 921 | Description: Checks the arguments passed to raise/fail. 922 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages 923 | Enabled: false 924 | EnforcedStyle: exploded 925 | SupportedStyles: 926 | - compact 927 | - exploded 928 | 929 | Style/RedundantBegin: 930 | Description: "Don't use begin blocks when they are not needed." 931 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit' 932 | Enabled: false 933 | 934 | Style/RedundantException: 935 | Description: "Checks for an obsolete RuntimeException argument in raise/fail." 936 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror' 937 | Enabled: false 938 | 939 | Style/RedundantReturn: 940 | Description: "Don't use return where it's not required." 941 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' 942 | Enabled: false 943 | 944 | Style/RedundantSelf: 945 | Description: "Don't use self where it's not needed." 946 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required' 947 | Enabled: false 948 | 949 | Style/RegexpLiteral: 950 | Description: 'Use / or %r around regular expressions.' 951 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' 952 | Enabled: false 953 | 954 | Style/RescueEnsureAlignment: 955 | Description: 'Align rescues and ensures correctly.' 956 | Enabled: false 957 | 958 | Style/RescueModifier: 959 | Description: 'Avoid using rescue in its modifier form.' 960 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers' 961 | Enabled: false 962 | 963 | Style/SelfAssignment: 964 | Description: >- 965 | Checks for places where self-assignment shorthand should have 966 | been used. 967 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' 968 | Enabled: false 969 | 970 | Style/Semicolon: 971 | Description: "Don't use semicolons to terminate expressions." 972 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' 973 | Enabled: false 974 | 975 | Style/Send: 976 | Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send` 977 | may overlap with existing methods. 978 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send 979 | Enabled: false 980 | 981 | Style/SignalException: 982 | Description: 'Checks for proper usage of fail and raise.' 983 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' 984 | Enabled: false 985 | EnforcedStyle: semantic 986 | SupportedStyles: 987 | - only_raise 988 | - only_fail 989 | - semantic 990 | 991 | Style/SingleLineBlockParams: 992 | Description: 'Enforces the names of some block params.' 993 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' 994 | Enabled: false 995 | Methods: 996 | - reduce: 997 | - a 998 | - e 999 | - inject: 1000 | - a 1001 | - e 1002 | 1003 | Style/SingleLineMethods: 1004 | Description: 'Avoid single-line methods.' 1005 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' 1006 | Enabled: false 1007 | AllowIfMethodIsEmpty: true 1008 | 1009 | Style/SpaceBeforeFirstArg: 1010 | Description: >- 1011 | Checks that exactly one space is used between a method name 1012 | and the first argument for method calls without parentheses. 1013 | Enabled: true 1014 | 1015 | Style/SpaceAfterColon: 1016 | Description: 'Use spaces after colons.' 1017 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1018 | Enabled: false 1019 | 1020 | Style/SpaceAfterComma: 1021 | Description: 'Use spaces after commas.' 1022 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1023 | Enabled: false 1024 | 1025 | Style/SpaceAroundKeyword: 1026 | Description: 'Use spaces around keywords.' 1027 | Enabled: false 1028 | 1029 | Style/SpaceAfterMethodName: 1030 | Description: >- 1031 | Do not put a space between a method name and the opening 1032 | parenthesis in a method definition. 1033 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 1034 | Enabled: false 1035 | 1036 | Style/SpaceAfterNot: 1037 | Description: Tracks redundant space after the ! operator. 1038 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang' 1039 | Enabled: false 1040 | 1041 | Style/SpaceAfterSemicolon: 1042 | Description: 'Use spaces after semicolons.' 1043 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1044 | Enabled: false 1045 | 1046 | Style/SpaceBeforeBlockBraces: 1047 | Description: >- 1048 | Checks that the left block brace has or doesn't have space 1049 | before it. 1050 | Enabled: false 1051 | 1052 | Style/SpaceBeforeComma: 1053 | Description: 'No spaces before commas.' 1054 | Enabled: false 1055 | 1056 | Style/SpaceBeforeComment: 1057 | Description: >- 1058 | Checks for missing space between code and a comment on the 1059 | same line. 1060 | Enabled: false 1061 | 1062 | Style/SpaceBeforeSemicolon: 1063 | Description: 'No spaces before semicolons.' 1064 | Enabled: false 1065 | 1066 | Style/SpaceInsideBlockBraces: 1067 | Description: >- 1068 | Checks that block braces have or don't have surrounding space. 1069 | For blocks taking parameters, checks that the left brace has 1070 | or doesn't have trailing space. 1071 | Enabled: false 1072 | 1073 | Style/SpaceAroundBlockParameters: 1074 | Description: 'Checks the spacing inside and after block parameters pipes.' 1075 | Enabled: false 1076 | 1077 | Style/SpaceAroundEqualsInParameterDefault: 1078 | Description: >- 1079 | Checks that the equals signs in parameter default assignments 1080 | have or don't have surrounding space depending on 1081 | configuration. 1082 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals' 1083 | Enabled: false 1084 | 1085 | Style/SpaceAroundOperators: 1086 | Description: 'Use a single space around operators.' 1087 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1088 | Enabled: false 1089 | 1090 | Style/SpaceInsideBrackets: 1091 | Description: 'No spaces after [ or before ].' 1092 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 1093 | Enabled: false 1094 | 1095 | Style/SpaceInsideHashLiteralBraces: 1096 | Description: "Use spaces inside hash literal braces - or don't." 1097 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1098 | Enabled: false 1099 | 1100 | Style/SpaceInsideParens: 1101 | Description: 'No spaces after ( or before ).' 1102 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 1103 | Enabled: false 1104 | 1105 | Style/SpaceInsideRangeLiteral: 1106 | Description: 'No spaces inside range literals.' 1107 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals' 1108 | Enabled: false 1109 | 1110 | Style/SpaceInsideStringInterpolation: 1111 | Description: 'Checks for padding/surrounding spaces inside string interpolation.' 1112 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation' 1113 | Enabled: false 1114 | 1115 | Style/SpecialGlobalVars: 1116 | Description: 'Avoid Perl-style global variables.' 1117 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' 1118 | Enabled: false 1119 | 1120 | Style/StringLiterals: 1121 | Description: Checks if uses of quotes match the configured preference. 1122 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals 1123 | Enabled: true 1124 | EnforcedStyle: single_quotes 1125 | SupportedStyles: 1126 | - single_quotes 1127 | - double_quotes 1128 | 1129 | Style/StringLiteralsInInterpolation: 1130 | Description: Checks if uses of quotes inside expressions in interpolated strings 1131 | match the configured preference. 1132 | Enabled: true 1133 | EnforcedStyle: single_quotes 1134 | SupportedStyles: 1135 | - single_quotes 1136 | - double_quotes 1137 | 1138 | Style/StructInheritance: 1139 | Description: 'Checks for inheritance from Struct.new.' 1140 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new' 1141 | Enabled: false 1142 | 1143 | Style/SymbolLiteral: 1144 | Description: 'Use plain symbols instead of string symbols when possible.' 1145 | Enabled: false 1146 | 1147 | Style/SymbolProc: 1148 | Description: 'Use symbols as procs instead of blocks when possible.' 1149 | Enabled: false 1150 | 1151 | Style/Tab: 1152 | Description: 'No hard tabs.' 1153 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 1154 | Enabled: false 1155 | 1156 | Style/TrailingBlankLines: 1157 | Description: 'Checks trailing blank lines and final newline.' 1158 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' 1159 | Enabled: false 1160 | 1161 | Style/TrailingCommaInArguments: 1162 | Description: 'Checks for trailing comma in argument lists.' 1163 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 1164 | Enabled: false 1165 | EnforcedStyleForMultiline: no_comma 1166 | SupportedStyles: 1167 | - comma 1168 | - consistent_comma 1169 | - no_comma 1170 | 1171 | Style/TrailingCommaInLiteral: 1172 | Description: 'Checks for trailing comma in array and hash literals.' 1173 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 1174 | Enabled: false 1175 | EnforcedStyleForMultiline: no_comma 1176 | SupportedStyles: 1177 | - comma 1178 | - consistent_comma 1179 | - no_comma 1180 | 1181 | Style/TrailingWhitespace: 1182 | Description: 'Avoid trailing whitespace.' 1183 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' 1184 | Enabled: false 1185 | 1186 | Style/TrivialAccessors: 1187 | Description: 'Prefer attr_* methods to trivial readers/writers.' 1188 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' 1189 | Enabled: false 1190 | 1191 | Style/UnlessElse: 1192 | Description: >- 1193 | Do not use unless with else. Rewrite these with the positive 1194 | case first. 1195 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' 1196 | Enabled: false 1197 | 1198 | Style/UnneededCapitalW: 1199 | Description: 'Checks for %W when interpolation is not needed.' 1200 | Enabled: false 1201 | 1202 | Style/UnneededPercentQ: 1203 | Description: 'Checks for %q/%Q when single quotes or double quotes would do.' 1204 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' 1205 | Enabled: false 1206 | 1207 | Style/TrailingUnderscoreVariable: 1208 | Description: >- 1209 | Checks for the usage of unneeded trailing underscores at the 1210 | end of parallel variable assignment. 1211 | Enabled: false 1212 | 1213 | Style/VariableInterpolation: 1214 | Description: >- 1215 | Don't interpolate global, instance and class variables 1216 | directly in strings. 1217 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' 1218 | Enabled: false 1219 | 1220 | Style/VariableName: 1221 | Description: 'Use the configured style when naming variables.' 1222 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 1223 | Enabled: false 1224 | 1225 | Style/WhenThen: 1226 | Description: 'Use when x then ... for one-line cases.' 1227 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' 1228 | Enabled: false 1229 | 1230 | Style/WhileUntilDo: 1231 | Description: 'Checks for redundant do after while or until.' 1232 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do' 1233 | Enabled: false 1234 | 1235 | Style/WhileUntilModifier: 1236 | Description: >- 1237 | Favor modifier while/until usage when you have a 1238 | single-line body. 1239 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' 1240 | Enabled: false 1241 | 1242 | Style/WordArray: 1243 | Description: 'Use %w or %W for arrays of words.' 1244 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' 1245 | Enabled: false 1246 | --------------------------------------------------------------------------------