tss 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +9 -0
- data/README.md +23 -19
- data/lib/tss/cli_combine.rb +0 -2
- data/lib/tss/cli_common.rb +0 -2
- data/lib/tss/cli_split.rb +2 -4
- data/lib/tss/cli_version.rb +0 -2
- data/lib/tss/combiner.rb +57 -46
- data/lib/tss/custom_contracts.rb +45 -6
- data/lib/tss/hasher.rb +34 -37
- data/lib/tss/splitter.rb +28 -19
- data/lib/tss/tss.rb +10 -10
- data/lib/tss/util.rb +35 -42
- data/lib/tss/version.rb +1 -1
- data/tss.gemspec +1 -0
- metadata +16 -2
- metadata.gz.sig +4 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: c3e63f35e23f40623afd002c3cba4e7a3407dd19
         | 
| 4 | 
            +
              data.tar.gz: 54fb3ead58b69e1118e13dcd72da08705bed4276
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 1c1e218fbc97f5caee8f2975ac340313104807bd12a1d68b6e47a00ea3befea35c2cdc62785f428dbddd71289a55f3cbad6656438613a43029c41b52cf4b7d9b
         | 
| 7 | 
            +
              data.tar.gz: 1a32eaaf782d3fe963451e2e1e1caff4c2fefe2e412b93ec47b8745eff6004773785fa9b9566a424ab15288b63cbc00957a547f59d7a1fa66e1c2420a423c936
         | 
    
        checksums.yaml.gz.sig
    CHANGED
    
    | Binary file | 
    
        data.tar.gz.sig
    CHANGED
    
    | Binary file | 
    
        data/.yardopts
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            --no-private  lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md
         | 
| 1 | 
            +
            --plugin contracts -e lib/tss/custom_contracts.rb --no-private  lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,14 @@ | |
| 1 1 | 
             
            # CHANGELOG
         | 
| 2 2 |  | 
| 3 | 
            +
            ## v0.4.0 (9/24/2016)
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            * Breaking change to force upcasing of some addition string args
         | 
| 6 | 
            +
            * Use yard-contracts
         | 
| 7 | 
            +
            * yardoc cleanups
         | 
| 8 | 
            +
            * Remove int_commas utility method
         | 
| 9 | 
            +
            * Hash w/ sha256 a,b strings in secure_compare
         | 
| 10 | 
            +
            * Deeper Contracts integration
         | 
| 11 | 
            +
             
         | 
| 3 12 | 
             
            ## v0.3.0 (9/24/2016)
         | 
| 4 13 |  | 
| 5 14 | 
             
            * Breaking change, identifier cannot be an empty string
         | 
    
        data/README.md
    CHANGED
    
    | @@ -161,7 +161,7 @@ It may work on others as well. | |
| 161 161 | 
             
            Add this line to your application's `Gemfile`:
         | 
| 162 162 |  | 
| 163 163 | 
             
            ```ruby
         | 
| 164 | 
            -
            gem 'tss', '~> 0. | 
| 164 | 
            +
            gem 'tss', '~> 0.3'
         | 
| 165 165 | 
             
            ```
         | 
| 166 166 |  | 
| 167 167 | 
             
            And then execute:
         | 
| @@ -296,7 +296,7 @@ tss~v1~546604c0b5e9138b~3~NTQ2NjA0YzBiNWU5MTM4YgIDAD8FqmJGRCiXokksUc3E1-gQNuNf2l | |
| 296 296 | 
             
            You can use the CLI to enter shares in order to recover a secret. Of course
         | 
| 297 297 | 
             
            you will need at least the number of shares necessary as determined
         | 
| 298 298 | 
             
            by the threshold when your shares were created. The `threshold` is visible
         | 
| 299 | 
            -
            as the third field in every ` | 
| 299 | 
            +
            as the third field in every `HUMAN` formatted share.
         | 
| 300 300 |  | 
| 301 301 | 
             
            As with splitting a secret, there are also three methods of getting the shares
         | 
| 302 302 | 
             
            into the CLI. `STDIN`, a path to a file containing shares, or interactively.
         | 
| @@ -419,12 +419,12 @@ and then combined with it prior to secret splitting. This means that the hash | |
| 419 419 | 
             
            is protected the same way as the secret. The algorithm used is
         | 
| 420 420 | 
             
            `secret || hash(secret)`. You can use one of `NONE`, `SHA1`, or `SHA256`.
         | 
| 421 421 |  | 
| 422 | 
            -
            The `format` arg takes  | 
| 423 | 
            -
            `' | 
| 422 | 
            +
            The `format` arg takes an uppercase String Enum with either `'HUMAN'` (default) or
         | 
| 423 | 
            +
            `'BINARY'` values. This instructs the output of a split to either provide an
         | 
| 424 424 | 
             
            array of binary octet strings (a standard RTSS format for interoperability), or
         | 
| 425 425 | 
             
            a human friendly URL Safe Base 64 encoded version of that same binary output.
         | 
| 426 | 
            -
            The ` | 
| 427 | 
            -
            ` | 
| 426 | 
            +
            The `HUMAN` format can be easily shared in a tweet, email, or even a URL. The
         | 
| 427 | 
            +
            `HUMAN` format is prefixed with `tss~VERSION~IDENTIFIER~THRESHOLD~` to make it
         | 
| 428 428 | 
             
            easier to visually compare shares and see if they have matching identifiers and
         | 
| 429 429 | 
             
            if you have enough shares to reach the threshold.
         | 
| 430 430 |  | 
| @@ -475,7 +475,7 @@ threshold  = 3 | |
| 475 475 | 
             
            num_shares = 5
         | 
| 476 476 | 
             
            identifier = SecureRandom.hex(8)
         | 
| 477 477 | 
             
            hash_alg   = 'SHA256'
         | 
| 478 | 
            -
            format     = ' | 
| 478 | 
            +
            format     = 'HUMAN'
         | 
| 479 479 |  | 
| 480 480 | 
             
            s = TSS.split(secret: secret, threshold: threshold, num_shares: num_shares, identifier: identifier, hash_alg: 'SHA256', pad_blocksize: 16, format: format)
         | 
| 481 481 |  | 
| @@ -523,23 +523,23 @@ of those shares are selected for use in the operation.  The method | |
| 523 523 | 
             
            used to select the shares can be chosen with the `select_by:` argument
         | 
| 524 524 | 
             
            which takes the following values as options:
         | 
| 525 525 |  | 
| 526 | 
            -
            `select_by: ' | 
| 526 | 
            +
            `select_by: 'FIRST'` : If X shares are required by the threshold and more than X
         | 
| 527 527 | 
             
            shares are provided, then the first X shares in the Array of shares provided
         | 
| 528 528 | 
             
            will be used. All others will be discarded and the operation will fail if
         | 
| 529 529 | 
             
            those selected shares cannot recreate the secret.
         | 
| 530 530 |  | 
| 531 | 
            -
            `select_by: ' | 
| 531 | 
            +
            `select_by: 'SAMPLE'` : If X shares are required by the threshold and more than X
         | 
| 532 532 | 
             
            shares are provided, then X shares will be randomly selected from the Array
         | 
| 533 533 | 
             
            of shares provided.  All others will be discarded and the operation will
         | 
| 534 534 | 
             
            fail if those selected shares cannot recreate the secret.
         | 
| 535 535 |  | 
| 536 | 
            -
            `select_by: ' | 
| 536 | 
            +
            `select_by: 'COMBINATIONS'` : If X shares are required, and more than X shares are
         | 
| 537 537 | 
             
            provided, then all possible combinations of the threshold number of shares
         | 
| 538 538 | 
             
            will be tried to see if the secret can be recreated.
         | 
| 539 539 |  | 
| 540 540 | 
             
            **Warning**
         | 
| 541 541 |  | 
| 542 | 
            -
            This ` | 
| 542 | 
            +
            This `COMBINATIONS` flexibility comes with a cost. All combinations of
         | 
| 543 543 | 
             
            `threshold` shares must be generated before processing. Due to the math
         | 
| 544 544 | 
             
            associated with combinations it is possible that the system would try to
         | 
| 545 545 | 
             
            generate a number of combinations that could never be generated or processed
         | 
| @@ -565,9 +565,9 @@ A great short read on this is | |
| 565 565 |  | 
| 566 566 | 
             
            ### Exception Handling
         | 
| 567 567 |  | 
| 568 | 
            -
             | 
| 569 | 
            -
             | 
| 570 | 
            -
            a ` | 
| 568 | 
            +
            Almost all methods in the program have strict contracts associated with them
         | 
| 569 | 
            +
            that enforce argument presence, types, and value boundaries. This contract checking
         | 
| 570 | 
            +
            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.
         | 
| 571 571 |  | 
| 572 572 | 
             
            The splitting and combining operations may also raise the following
         | 
| 573 573 | 
             
            exception types:
         | 
| @@ -583,7 +583,7 @@ All Exceptions should include hints as to what went wrong in the | |
| 583 583 | 
             
            ### RTSS Binary
         | 
| 584 584 |  | 
| 585 585 | 
             
            TSS provides shares in a binary data format with the following fields, and
         | 
| 586 | 
            -
            by default this binary data is wrapped in a `' | 
| 586 | 
            +
            by default this binary data is wrapped in a `'HUMAN'` text format:
         | 
| 587 587 |  | 
| 588 588 | 
             
            `Identifier`. This field contains 16 octets. It identifies the secret
         | 
| 589 589 | 
             
            with which a share is associated.  All of the shares associated
         | 
| @@ -641,7 +641,7 @@ text wrapper around the RTSS binary data format is provided. | |
| 641 641 |  | 
| 642 642 | 
             
            Shares formatted this way can easily be shared via most any communication channel.
         | 
| 643 643 |  | 
| 644 | 
            -
            The ` | 
| 644 | 
            +
            The `HUMAN` data format is simply the same RTSS binary data, URL Safe Base64
         | 
| 645 645 | 
             
            encoded, and prefixed with a String thet contains tilde `~` separated elements.
         | 
| 646 646 | 
             
            The `~` is used as it is compatible with the URL Safe data and the allowed
         | 
| 647 647 | 
             
            characters in the rest of the human format string.
         | 
| @@ -666,12 +666,16 @@ and Combining involves at least `num_shares**secret_bytes` operations so | |
| 666 666 | 
             
            larger values can quickly result in huge processing time. If you need to
         | 
| 667 667 | 
             
            split large secrets into a large number of shares you should consider
         | 
| 668 668 | 
             
            running those operations in a background worker process or thread for
         | 
| 669 | 
            -
            best performance.
         | 
| 669 | 
            +
            best performance. For example a 64kb file split into 255 shares,
         | 
| 670 | 
            +
            with a threshold of 255 (the max for all three settings), can take
         | 
| 671 | 
            +
            20 minutes to split and another 20 minutes to combine using a modern CPU.
         | 
| 670 672 |  | 
| 671 673 | 
             
            A reasonable set of values seems to be what I'll call the 'rule of 64'. If you
         | 
| 672 674 | 
             
            keep the `secret <= 64 Bytes`, the `threshold <= 64`, and the `num_shares <= 64`
         | 
| 673 | 
            -
            you can do a round-trip split and combine operation in `~250ms | 
| 674 | 
            -
             | 
| 675 | 
            +
            you can do a round-trip split and combine operation in `~250ms`. These should
         | 
| 676 | 
            +
            be very reasonable and secure max values for most use cases, even as part of a
         | 
| 677 | 
            +
            web request response cycle. Remember, its recommended to split encryption keys,
         | 
| 678 | 
            +
            not plaintext.
         | 
| 675 679 |  | 
| 676 680 | 
             
            There are some simple benchmark tests to exercise things with `rake bench`.
         | 
| 677 681 |  | 
    
        data/lib/tss/cli_combine.rb
    CHANGED
    
    
    
        data/lib/tss/cli_common.rb
    CHANGED
    
    
    
        data/lib/tss/cli_split.rb
    CHANGED
    
    | @@ -1,7 +1,5 @@ | |
| 1 1 | 
             
            require 'thor'
         | 
| 2 2 |  | 
| 3 | 
            -
            # Command Line Interface (CLI)
         | 
| 4 | 
            -
            # See also, `bin/tss` executable.
         | 
| 5 3 | 
             
            module TSS
         | 
| 6 4 | 
             
              class CLI < Thor
         | 
| 7 5 | 
             
                include Thor::Actions
         | 
| @@ -10,7 +8,7 @@ module TSS | |
| 10 8 | 
             
                method_option :num_shares, :aliases => '-n', :banner => 'num_shares', :type => :numeric, :desc => '# of shares total that will be generated'
         | 
| 11 9 | 
             
                method_option :identifier, :aliases => '-i', :banner => 'identifier', :type => :string, :desc => 'A unique identifier string, 0-16 Bytes, [a-zA-Z0-9.-_]'
         | 
| 12 10 | 
             
                method_option :hash_alg, :aliases => '-h', :banner => 'hash_alg', :type => :string, :desc => 'A hash type for verification, NONE, SHA1, SHA256'
         | 
| 13 | 
            -
                method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => ' | 
| 11 | 
            +
                method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'HUMAN', :desc => 'Share output format, BINARY or HUMAN'
         | 
| 14 12 | 
             
                method_option :pad_blocksize, :aliases => '-p', :banner => 'pad_blocksize', :type => :numeric, :desc => 'Block size # secrets will be left-padded to, 0-255'
         | 
| 15 13 | 
             
                method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read the secret from'
         | 
| 16 14 | 
             
                method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the shares to'
         | 
| @@ -57,7 +55,7 @@ module TSS | |
| 57 55 |  | 
| 58 56 | 
             
                  Example w/ options:
         | 
| 59 57 |  | 
| 60 | 
            -
                  $ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f  | 
| 58 | 
            +
                  $ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f HUMAN
         | 
| 61 59 |  | 
| 62 60 | 
             
                  Enter your secret:
         | 
| 63 61 |  | 
    
        data/lib/tss/cli_version.rb
    CHANGED
    
    
    
        data/lib/tss/combiner.rb
    CHANGED
    
    | @@ -1,5 +1,8 @@ | |
| 1 1 | 
             
            module TSS
         | 
| 2 | 
            -
              #  | 
| 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
         | 
| 3 6 | 
             
              # into the original secret the shares were split from. It is also responsible
         | 
| 4 7 | 
             
              # for doing extensive validation of user provided shares and ensuring
         | 
| 5 8 | 
             
              # that any recovered secret matches the hash of the original secret.
         | 
| @@ -11,15 +14,17 @@ module TSS | |
| 11 14 |  | 
| 12 15 | 
             
                attr_reader :shares, :select_by
         | 
| 13 16 |  | 
| 14 | 
            -
                Contract ({ :shares => C:: | 
| 17 | 
            +
                Contract ({ :shares => C::ArrayOfShares, :select_by => C::Maybe[C::SelectByArg] }) => C::Any
         | 
| 15 18 | 
             
                def initialize(opts = {})
         | 
| 16 19 | 
             
                  # clone the incoming shares so the object passed to this
         | 
| 17 20 | 
             
                  # function doesn't get modified.
         | 
| 18 21 | 
             
                  @shares = opts.fetch(:shares).clone
         | 
| 19 | 
            -
                   | 
| 20 | 
            -
                  @select_by = opts.fetch(:select_by, 'first')
         | 
| 22 | 
            +
                  @select_by = opts.fetch(:select_by, 'FIRST')
         | 
| 21 23 | 
             
                end
         | 
| 22 24 |  | 
| 25 | 
            +
                # Warning, you probably don't want to use this directly. Instead
         | 
| 26 | 
            +
                # see the TSS module.
         | 
| 27 | 
            +
                #
         | 
| 23 28 | 
             
                # To reconstruct a secret from a set of shares, the following
         | 
| 24 29 | 
             
                # procedure, or any equivalent method, is used:
         | 
| 25 30 | 
             
                #
         | 
| @@ -52,8 +57,11 @@ module TSS | |
| 52 57 | 
             
                #
         | 
| 53 58 | 
             
                #   The output string is returned (along with some metadata).
         | 
| 54 59 | 
             
                #
         | 
| 60 | 
            +
                #
         | 
| 61 | 
            +
                # @return an Hash of combined secret attributes
         | 
| 62 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
         | 
| 55 63 | 
             
                # rubocop:disable CyclomaticComplexity
         | 
| 56 | 
            -
                Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C:: | 
| 64 | 
            +
                Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C::HashAlgArg, :identifier => C::IdentifierArg, :process_time => C::Num, :secret => C::SecretArg, :threshold => C::ThresholdArg})
         | 
| 57 65 | 
             
                def combine
         | 
| 58 66 | 
             
                  # unwrap 'human' shares into binary shares
         | 
| 59 67 | 
             
                  if all_shares_appear_human?(shares)
         | 
| @@ -70,9 +78,9 @@ module TSS | |
| 70 78 |  | 
| 71 79 | 
             
                  # Select a subset of the shares provided using the chosen selection
         | 
| 72 80 | 
             
                  # method. If there are exactly the right amount of shares this is a no-op.
         | 
| 73 | 
            -
                  if select_by == ' | 
| 81 | 
            +
                  if select_by == 'FIRST'
         | 
| 74 82 | 
             
                    @shares = shares.shift(threshold)
         | 
| 75 | 
            -
                  elsif select_by == ' | 
| 83 | 
            +
                  elsif select_by == 'SAMPLE'
         | 
| 76 84 | 
             
                    @shares = shares.sample(threshold)
         | 
| 77 85 | 
             
                  end
         | 
| 78 86 |  | 
| @@ -85,7 +93,7 @@ module TSS | |
| 85 93 |  | 
| 86 94 | 
             
                  shares_bytes_have_valid_indexes!(shares_bytes)
         | 
| 87 95 |  | 
| 88 | 
            -
                  if select_by == ' | 
| 96 | 
            +
                  if select_by == 'COMBINATIONS'
         | 
| 89 97 | 
             
                    share_combinations_mode_allowed!(hash_id)
         | 
| 90 98 | 
             
                    share_combinations_out_of_bounds!(shares, threshold)
         | 
| 91 99 |  | 
| @@ -107,7 +115,7 @@ module TSS | |
| 107 115 | 
             
                  # Return a Hash with the secret and metadata
         | 
| 108 116 | 
             
                  {
         | 
| 109 117 | 
             
                    hash: secret[:hash],
         | 
| 110 | 
            -
                    hash_alg: secret[:hash_alg] | 
| 118 | 
            +
                    hash_alg: secret[:hash_alg],
         | 
| 111 119 | 
             
                    identifier: identifier,
         | 
| 112 120 | 
             
                    process_time: ((Time.now - start_processing_time)*1000).round(2),
         | 
| 113 121 | 
             
                    secret: Util.bytes_to_utf8(secret[:secret]),
         | 
| @@ -122,12 +130,12 @@ module TSS | |
| 122 130 | 
             
                # and validate it against any one-way hash that was embedded in the shares
         | 
| 123 131 | 
             
                # along with the secret.
         | 
| 124 132 | 
             
                #
         | 
| 125 | 
            -
                # @param hash_id  | 
| 126 | 
            -
                # @param shares_bytes  | 
| 127 | 
            -
                # @return  | 
| 133 | 
            +
                # @param hash_id the ID of the one-way hash function to test with
         | 
| 134 | 
            +
                # @param shares_bytes the shares as Byte Arrays to be evaluated
         | 
| 135 | 
            +
                # @return returns the secret as an Array of Bytes if it was recovered from the shares and validated
         | 
| 128 136 | 
             
                # @raise [TSS::NoSecretError] if the secret was not able to be recovered (with no hash)
         | 
| 129 137 | 
             
                # @raise [TSS::InvalidSecretHashError] if the secret was able to be recovered but the hash test failed
         | 
| 130 | 
            -
                Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C:: | 
| 138 | 
            +
                Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C::HashAlgArg })
         | 
| 131 139 | 
             
                def extract_secret_from_shares!(hash_id, shares_bytes)
         | 
| 132 140 | 
             
                  secret = []
         | 
| 133 141 |  | 
| @@ -172,8 +180,9 @@ module TSS | |
| 172 180 |  | 
| 173 181 | 
             
                # Strip off leading padding chars ("\u001F", decimal 31)
         | 
| 174 182 | 
             
                #
         | 
| 175 | 
            -
                # @param secret  | 
| 176 | 
            -
                # @return  | 
| 183 | 
            +
                # @param secret the secret to be stripped
         | 
| 184 | 
            +
                # @return returns the secret, stripped of the leading padding char
         | 
| 185 | 
            +
                # @raise [ParamContractError] if secret appears invalid
         | 
| 177 186 | 
             
                Contract C::ArrayOf[C::Num] => C::Maybe[Array]
         | 
| 178 187 | 
             
                def strip_left_pad(secret)
         | 
| 179 188 | 
             
                  secret.shift while secret.first == 31
         | 
| @@ -181,8 +190,9 @@ module TSS | |
| 181 190 |  | 
| 182 191 | 
             
                # Do all of the shares match the pattern expected of human style shares?
         | 
| 183 192 | 
             
                #
         | 
| 184 | 
            -
                # @param shares  | 
| 185 | 
            -
                # @return  | 
| 193 | 
            +
                # @param shares the shares to be evaluated
         | 
| 194 | 
            +
                # @return returns true if all shares match the patterns, false if not
         | 
| 195 | 
            +
                # @raise [ParamContractError] if shares appear invalid
         | 
| 186 196 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 187 197 | 
             
                def all_shares_appear_human?(shares)
         | 
| 188 198 | 
             
                  shares.all? do |s|
         | 
| @@ -194,9 +204,9 @@ module TSS | |
| 194 204 |  | 
| 195 205 | 
             
                # Convert an Array of human style shares to binary style
         | 
| 196 206 | 
             
                #
         | 
| 197 | 
            -
                # @param shares  | 
| 198 | 
            -
                # @return  | 
| 199 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 207 | 
            +
                # @param shares the shares to be converted
         | 
| 208 | 
            +
                # @return returns an Array of String shares in binary octet String format
         | 
| 209 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
         | 
| 200 210 | 
             
                Contract C::ArrayOf[String] => C::ArrayOf[String]
         | 
| 201 211 | 
             
                def convert_shares_human_to_binary(shares)
         | 
| 202 212 | 
             
                  shares.map do |s|
         | 
| @@ -216,9 +226,9 @@ module TSS | |
| 216 226 |  | 
| 217 227 | 
             
                # Do all shares have a common Byte size? They are invalid if not.
         | 
| 218 228 | 
             
                #
         | 
| 219 | 
            -
                # @param shares  | 
| 220 | 
            -
                # @return  | 
| 221 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 229 | 
            +
                # @param shares the shares to be evaluated
         | 
| 230 | 
            +
                # @return returns true if all shares have the same Byte size
         | 
| 231 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
         | 
| 222 232 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 223 233 | 
             
                def shares_have_same_bytesize!(shares)
         | 
| 224 234 | 
             
                  shares.each do |s|
         | 
| @@ -231,9 +241,9 @@ module TSS | |
| 231 241 |  | 
| 232 242 | 
             
                # Do all shares have a valid header and match each other? They are invalid if not.
         | 
| 233 243 | 
             
                #
         | 
| 234 | 
            -
                # @param shares  | 
| 235 | 
            -
                # @return  | 
| 236 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 244 | 
            +
                # @param shares the shares to be evaluated
         | 
| 245 | 
            +
                # @return returns true if all shares have the same header
         | 
| 246 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
         | 
| 237 247 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 238 248 | 
             
                def shares_have_valid_headers!(shares)
         | 
| 239 249 | 
             
                  fh = Util.extract_share_header(shares.first)
         | 
| @@ -253,9 +263,9 @@ module TSS | |
| 253 263 |  | 
| 254 264 | 
             
                # Do all shares have a the expected length? They are invalid if not.
         | 
| 255 265 | 
             
                #
         | 
| 256 | 
            -
                # @param shares  | 
| 257 | 
            -
                # @return  | 
| 258 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 266 | 
            +
                # @param shares the shares to be evaluated
         | 
| 267 | 
            +
                # @return returns true if all shares have the same header
         | 
| 268 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
         | 
| 259 269 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 260 270 | 
             
                def shares_have_expected_length!(shares)
         | 
| 261 271 | 
             
                  shares.each do |s|
         | 
| @@ -268,9 +278,9 @@ module TSS | |
| 268 278 |  | 
| 269 279 | 
             
                # Were enough shares provided to meet the threshold? They are invalid if not.
         | 
| 270 280 | 
             
                #
         | 
| 271 | 
            -
                # @param shares  | 
| 272 | 
            -
                # @return  | 
| 273 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 281 | 
            +
                # @param shares the shares to be evaluated
         | 
| 282 | 
            +
                # @return returns true if there are enough shares
         | 
| 283 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
         | 
| 274 284 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 275 285 | 
             
                def shares_meet_threshold_min!(shares)
         | 
| 276 286 | 
             
                  fh = Util.extract_share_header(shares.first)
         | 
| @@ -283,8 +293,9 @@ module TSS | |
| 283 293 |  | 
| 284 294 | 
             
                # Were enough shares provided to meet the threshold? They are invalid if not.
         | 
| 285 295 | 
             
                #
         | 
| 286 | 
            -
                # @param shares  | 
| 287 | 
            -
                # @return  | 
| 296 | 
            +
                # @param shares the shares to be evaluated
         | 
| 297 | 
            +
                # @return returns true if all tests pass
         | 
| 298 | 
            +
                # @raise [ParamContractError] if shares appear invalid
         | 
| 288 299 | 
             
                Contract C::ArrayOf[String] => C::Bool
         | 
| 289 300 | 
             
                def validate_all_shares(shares)
         | 
| 290 301 | 
             
                  if shares_have_valid_headers!(shares) &&
         | 
| @@ -299,9 +310,9 @@ module TSS | |
| 299 310 |  | 
| 300 311 | 
             
                # Do all the shares have a valid first-byte index? They are invalid if not.
         | 
| 301 312 | 
             
                #
         | 
| 302 | 
            -
                # @param shares_bytes  | 
| 303 | 
            -
                # @return  | 
| 304 | 
            -
                # @raise [TSS::ArgumentError] if shares appear invalid
         | 
| 313 | 
            +
                # @param shares_bytes the shares as Byte Arrays to be evaluated
         | 
| 314 | 
            +
                # @return returns true if there are enough shares
         | 
| 315 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if shares bytes appear invalid
         | 
| 305 316 | 
             
                Contract C::ArrayOf[C::ArrayOf[C::Num]] => C::Bool
         | 
| 306 317 | 
             
                def shares_bytes_have_valid_indexes!(shares_bytes)
         | 
| 307 318 | 
             
                  u = shares_bytes.map do |s|
         | 
| @@ -320,9 +331,9 @@ module TSS | |
| 320 331 | 
             
                # Is it valid to use combinations mode? Only when there is an embedded non-zero
         | 
| 321 332 | 
             
                # hash_id Integer to test the results against. Invalid if not.
         | 
| 322 333 | 
             
                #
         | 
| 323 | 
            -
                # @param hash_id  | 
| 324 | 
            -
                # @return  | 
| 325 | 
            -
                # @raise [TSS::ArgumentError] if hash_id represents a non hashing type
         | 
| 334 | 
            +
                # @param hash_id the shares as Byte Arrays to be evaluated
         | 
| 335 | 
            +
                # @return returns true if OK to use combinations mode
         | 
| 336 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if hash_id represents a non hashing type
         | 
| 326 337 | 
             
                Contract C::Int => C::Bool
         | 
| 327 338 | 
             
                def share_combinations_mode_allowed!(hash_id)
         | 
| 328 339 | 
             
                  unless Hasher.codes_without_none.include?(hash_id)
         | 
| @@ -340,16 +351,16 @@ module TSS | |
| 340 351 | 
             
                # e.g. 255 total shares, with threshold of 128, results in # combinations of:
         | 
| 341 352 | 
             
                # 2884329411724603169044874178931143443870105850987581016304218283632259375395
         | 
| 342 353 | 
             
                #
         | 
| 343 | 
            -
                # @param shares  | 
| 344 | 
            -
                # @param threshold  | 
| 345 | 
            -
                # @param max_combinations  | 
| 346 | 
            -
                # @return  | 
| 347 | 
            -
                # @raise [TSS::ArgumentError] if the number of possible combinations is unreasonably high
         | 
| 354 | 
            +
                # @param shares the shares to be evaluated
         | 
| 355 | 
            +
                # @param threshold the threshold value set in the shares
         | 
| 356 | 
            +
                # @param max_combinations the max (1_000_000) number of combinations allowed
         | 
| 357 | 
            +
                # @return returns true if a reasonable number of combinations
         | 
| 358 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if args are invalid or the number of possible combinations is unreasonably high
         | 
| 348 359 | 
             
                Contract C::ArrayOf[String], C::Int, C::Int => C::Bool
         | 
| 349 360 | 
             
                def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000)
         | 
| 350 361 | 
             
                  combinations = Util.calc_combinations(shares.size, threshold)
         | 
| 351 362 | 
             
                  if combinations > max_combinations
         | 
| 352 | 
            -
                    raise TSS::ArgumentError, "invalid options, too many combinations (#{ | 
| 363 | 
            +
                    raise TSS::ArgumentError, "invalid options, too many combinations (#{combinations})"
         | 
| 353 364 | 
             
                  else
         | 
| 354 365 | 
             
                    return true
         | 
| 355 366 | 
             
                  end
         | 
    
        data/lib/tss/custom_contracts.rb
    CHANGED
    
    | @@ -1,10 +1,23 @@ | |
| 1 | 
            -
            module  | 
| 1 | 
            +
            module Contracts
         | 
| 2 2 |  | 
| 3 3 | 
             
              # Custom Contracts
         | 
| 4 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 | 
            +
             | 
| 5 18 | 
             
              class SecretArg
         | 
| 6 19 | 
             
                def self.valid? val
         | 
| 7 | 
            -
                  val. | 
| 20 | 
            +
                  val.is_a?(String) &&
         | 
| 8 21 | 
             
                  val.length.between?(1,65502) &&
         | 
| 9 22 | 
             
                  ['UTF-8', 'US-ASCII'].include?(val.encoding.name) &&
         | 
| 10 23 | 
             
                  val.slice(0) != "\u001F"
         | 
| @@ -17,7 +30,6 @@ module TSS | |
| 17 30 |  | 
| 18 31 | 
             
              class ThresholdArg
         | 
| 19 32 | 
             
                def self.valid? val
         | 
| 20 | 
            -
                  val.present? &&
         | 
| 21 33 | 
             
                  val.is_a?(Integer) &&
         | 
| 22 34 | 
             
                  val.between?(1,255)
         | 
| 23 35 | 
             
                end
         | 
| @@ -29,7 +41,6 @@ module TSS | |
| 29 41 |  | 
| 30 42 | 
             
              class NumSharesArg
         | 
| 31 43 | 
             
                def self.valid? val
         | 
| 32 | 
            -
                  val.present? &&
         | 
| 33 44 | 
             
                  val.is_a?(Integer) &&
         | 
| 34 45 | 
             
                  val.between?(1,255)
         | 
| 35 46 | 
             
                end
         | 
| @@ -41,7 +52,6 @@ module TSS | |
| 41 52 |  | 
| 42 53 | 
             
              class IdentifierArg
         | 
| 43 54 | 
             
                def self.valid? val
         | 
| 44 | 
            -
                  val.present? &&
         | 
| 45 55 | 
             
                  val.is_a?(String) &&
         | 
| 46 56 | 
             
                  val.length.between?(1,16) &&
         | 
| 47 57 | 
             
                  val =~ /^[a-zA-Z0-9\-\_\.]*$/i
         | 
| @@ -52,9 +62,38 @@ module TSS | |
| 52 62 | 
             
                end
         | 
| 53 63 | 
             
              end
         | 
| 54 64 |  | 
| 65 | 
            +
              class HashAlgArg
         | 
| 66 | 
            +
                def self.valid? val
         | 
| 67 | 
            +
                  Contracts::Enum['NONE', 'SHA1', 'SHA256'].valid?(val)
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def self.to_s
         | 
| 71 | 
            +
                  'must be a uppercase String specifying the hash algorithm to use [NONE, SHA1, SHA256].'
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              class FormatArg
         | 
| 76 | 
            +
                def self.valid? val
         | 
| 77 | 
            +
                  Contracts::Enum['BINARY', 'HUMAN'].valid?(val)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def self.to_s
         | 
| 81 | 
            +
                  'must be a uppercase String specifying the desired String share format [BINARY, HUMAN].'
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
              class SelectByArg
         | 
| 86 | 
            +
                def self.valid? val
         | 
| 87 | 
            +
                  Contracts::Enum['FIRST', 'SAMPLE', 'COMBINATIONS'].valid?(val)
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def self.to_s
         | 
| 91 | 
            +
                  'must be a uppercase String specifying the desired way to sample shares provided [FIRST, SAMPLE, COMBINATIONS].'
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 55 95 | 
             
              class PadBlocksizeArg
         | 
| 56 96 | 
             
                def self.valid? val
         | 
| 57 | 
            -
                  val.present? &&
         | 
| 58 97 | 
             
                  val.is_a?(Integer) &&
         | 
| 59 98 | 
             
                  val.between?(0,255)
         | 
| 60 99 | 
             
                end
         | 
    
        data/lib/tss/hasher.rb
    CHANGED
    
    | @@ -5,15 +5,15 @@ module TSS | |
| 5 5 | 
             
                include Contracts::Core
         | 
| 6 6 | 
             
                C = Contracts
         | 
| 7 7 |  | 
| 8 | 
            -
                HASHES = { NONE | 
| 9 | 
            -
                           SHA1 | 
| 10 | 
            -
                           SHA256 | 
| 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 11 |  | 
| 12 12 | 
             
                # Lookup the Symbol key for a Hash with the code.
         | 
| 13 13 | 
             
                #
         | 
| 14 | 
            -
                # @param code  | 
| 15 | 
            -
                # @return  | 
| 16 | 
            -
                Contract C::Int => C::Maybe[ | 
| 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 17 | 
             
                def self.key_from_code(code)
         | 
| 18 18 | 
             
                  return nil unless Hasher.codes.include?(code)
         | 
| 19 19 | 
             
                  HASHES.each do |k, v|
         | 
| @@ -23,16 +23,16 @@ module TSS | |
| 23 23 |  | 
| 24 24 | 
             
                # Lookup the hash code for the hash matching hash_key.
         | 
| 25 25 | 
             
                #
         | 
| 26 | 
            -
                # @param hash_key  | 
| 27 | 
            -
                # @return  | 
| 28 | 
            -
                Contract C:: | 
| 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 29 | 
             
                def self.code(hash_key)
         | 
| 30 | 
            -
                  HASHES[hash_key | 
| 30 | 
            +
                  HASHES[hash_key][:code]
         | 
| 31 31 | 
             
                end
         | 
| 32 32 |  | 
| 33 33 | 
             
                # Lookup all valid hash codes, including NONE.
         | 
| 34 34 | 
             
                #
         | 
| 35 | 
            -
                # @return  | 
| 35 | 
            +
                # @return all hash codes including NONE
         | 
| 36 36 | 
             
                Contract C::None => C::ArrayOf[C::Int]
         | 
| 37 37 | 
             
                def self.codes
         | 
| 38 38 | 
             
                  HASHES.map do |_k, v|
         | 
| @@ -42,7 +42,7 @@ module TSS | |
| 42 42 |  | 
| 43 43 | 
             
                # All valid hash codes that actually do hashing, excluding NONE.
         | 
| 44 44 | 
             
                #
         | 
| 45 | 
            -
                # @return  | 
| 45 | 
            +
                # @return all hash codes excluding NONE
         | 
| 46 46 | 
             
                Contract C::None => C::ArrayOf[C::Int]
         | 
| 47 47 | 
             
                def self.codes_without_none
         | 
| 48 48 | 
             
                  HASHES.map do |_k, v|
         | 
| @@ -52,49 +52,46 @@ module TSS | |
| 52 52 |  | 
| 53 53 | 
             
                # Lookup the size in Bytes for a specific hash_key.
         | 
| 54 54 | 
             
                #
         | 
| 55 | 
            -
                # @param hash_key  | 
| 56 | 
            -
                # @return  | 
| 57 | 
            -
                Contract C:: | 
| 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 58 | 
             
                def self.bytesize(hash_key)
         | 
| 59 | 
            -
                  HASHES[hash_key | 
| 59 | 
            +
                  HASHES[hash_key][:bytesize]
         | 
| 60 60 | 
             
                end
         | 
| 61 61 |  | 
| 62 62 | 
             
                # Return a hexdigest hash for a String using hash_key hash algorithm.
         | 
| 63 | 
            -
                # Returns '' if hash_key ==  | 
| 63 | 
            +
                # Returns '' if hash_key == 'NONE'
         | 
| 64 64 | 
             
                #
         | 
| 65 | 
            -
                # @param hash_key  | 
| 66 | 
            -
                # @param str  | 
| 67 | 
            -
                # @return  | 
| 68 | 
            -
                Contract C:: | 
| 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 69 | 
             
                def self.hex_string(hash_key, str)
         | 
| 70 | 
            -
                  hash_key  | 
| 71 | 
            -
                  return '' if hash_key == :NONE
         | 
| 70 | 
            +
                  return '' if hash_key == 'NONE'
         | 
| 72 71 | 
             
                  HASHES[hash_key][:hasher].send(:hexdigest, str)
         | 
| 73 72 | 
             
                end
         | 
| 74 73 |  | 
| 75 74 | 
             
                # Return a Byte String hash for a String using hash_key hash algorithm.
         | 
| 76 | 
            -
                # Returns '' if hash_key ==  | 
| 75 | 
            +
                # Returns '' if hash_key == 'NONE'
         | 
| 77 76 | 
             
                #
         | 
| 78 | 
            -
                # @param hash_key  | 
| 79 | 
            -
                # @param str  | 
| 80 | 
            -
                # @return  | 
| 81 | 
            -
                Contract C:: | 
| 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
         | 
| 82 81 | 
             
                def self.byte_string(hash_key, str)
         | 
| 83 | 
            -
                  hash_key  | 
| 84 | 
            -
                  return '' if hash_key == :NONE
         | 
| 82 | 
            +
                  return '' if hash_key == 'NONE'
         | 
| 85 83 | 
             
                  HASHES[hash_key][:hasher].send(:digest, str)
         | 
| 86 84 | 
             
                end
         | 
| 87 85 |  | 
| 88 86 | 
             
                # Return a Byte Array hash for a String using hash_key hash algorithm.
         | 
| 89 | 
            -
                # Returns [] if hash_key ==  | 
| 87 | 
            +
                # Returns [] if hash_key == 'NONE'
         | 
| 90 88 | 
             
                #
         | 
| 91 | 
            -
                # @param hash_key  | 
| 92 | 
            -
                # @param str  | 
| 93 | 
            -
                # @return  | 
| 94 | 
            -
                Contract C:: | 
| 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]
         | 
| 95 93 | 
             
                def self.byte_array(hash_key, str)
         | 
| 96 | 
            -
                  hash_key  | 
| 97 | 
            -
                  return [] if hash_key == :NONE
         | 
| 94 | 
            +
                  return [] if hash_key == 'NONE'
         | 
| 98 95 | 
             
                  HASHES[hash_key][:hasher].send(:digest, str).unpack('C*')
         | 
| 99 96 | 
             
                end
         | 
| 100 97 | 
             
              end
         | 
    
        data/lib/tss/splitter.rb
    CHANGED
    
    | @@ -1,5 +1,8 @@ | |
| 1 1 | 
             
            module TSS
         | 
| 2 | 
            -
              #  | 
| 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.
         | 
| 3 6 | 
             
              class Splitter
         | 
| 4 7 | 
             
                include Contracts::Core
         | 
| 5 8 | 
             
                include Util
         | 
| @@ -8,14 +11,14 @@ module TSS | |
| 8 11 |  | 
| 9 12 | 
             
                attr_reader :secret, :threshold, :num_shares, :identifier, :hash_alg, :format, :pad_blocksize
         | 
| 10 13 |  | 
| 11 | 
            -
                Contract ({ :secret =>  | 
| 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], :pad_blocksize => C::Maybe[C::PadBlocksizeArg] }) => C::Any
         | 
| 12 15 | 
             
                def initialize(opts = {})
         | 
| 13 16 | 
             
                  @secret = opts.fetch(:secret)
         | 
| 14 17 | 
             
                  @threshold = opts.fetch(:threshold, 3)
         | 
| 15 18 | 
             
                  @num_shares = opts.fetch(:num_shares, 5)
         | 
| 16 19 | 
             
                  @identifier = opts.fetch(:identifier, SecureRandom.hex(8))
         | 
| 17 20 | 
             
                  @hash_alg = opts.fetch(:hash_alg, 'SHA256')
         | 
| 18 | 
            -
                  @format = opts.fetch(:format, ' | 
| 21 | 
            +
                  @format = opts.fetch(:format, 'HUMAN')
         | 
| 19 22 | 
             
                  @pad_blocksize = opts.fetch(:pad_blocksize, 0)
         | 
| 20 23 | 
             
                end
         | 
| 21 24 |  | 
| @@ -26,6 +29,9 @@ module TSS | |
| 26 29 | 
             
                                 'n', :share_len
         | 
| 27 30 | 
             
                                ])
         | 
| 28 31 |  | 
| 32 | 
            +
                # Warning, you probably don't want to use this directly. Instead
         | 
| 33 | 
            +
                # see the TSS module.
         | 
| 34 | 
            +
                #
         | 
| 29 35 | 
             
                # To split a secret into a set of shares, the following
         | 
| 30 36 | 
             
                # procedure, or any equivalent method, is used:
         | 
| 31 37 | 
             
                #
         | 
| @@ -49,7 +55,9 @@ module TSS | |
| 49 55 | 
             
                #   If the operation can not be completed successfully, then an error
         | 
| 50 56 | 
             
                #   code should be returned.
         | 
| 51 57 | 
             
                #
         | 
| 52 | 
            -
                 | 
| 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
         | 
| 53 61 | 
             
                def split
         | 
| 54 62 | 
             
                  num_shares_not_less_than_threshold!(threshold, num_shares)
         | 
| 55 63 |  | 
| @@ -115,7 +123,7 @@ module TSS | |
| 115 123 | 
             
                    binary = (header + s.pack('C*')).force_encoding('ASCII-8BIT')
         | 
| 116 124 | 
             
                    # join with URL safe '~'
         | 
| 117 125 | 
             
                    human = ['tss', 'v1', identifier, threshold, Base64.urlsafe_encode64(binary)].join('~')
         | 
| 118 | 
            -
                    format == ' | 
| 126 | 
            +
                    format == 'BINARY' ? binary : human
         | 
| 119 127 | 
             
                  end
         | 
| 120 128 |  | 
| 121 129 | 
             
                  return shares
         | 
| @@ -125,11 +133,11 @@ module TSS | |
| 125 133 |  | 
| 126 134 | 
             
                # The num_shares must be greater than or equal to the threshold or it is invalid.
         | 
| 127 135 | 
             
                #
         | 
| 128 | 
            -
                # @param threshold  | 
| 129 | 
            -
                # @param num_shares  | 
| 130 | 
            -
                # @return  | 
| 131 | 
            -
                # @raise [TSS::ArgumentError] if invalid
         | 
| 132 | 
            -
                Contract  | 
| 136 | 
            +
                # @param threshold the threshold value
         | 
| 137 | 
            +
                # @param num_shares the num_shares value
         | 
| 138 | 
            +
                # @return returns true if num_shares is >= threshold
         | 
| 139 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if invalid
         | 
| 140 | 
            +
                Contract C::ThresholdArg, C::NumSharesArg => C::Bool
         | 
| 133 141 | 
             
                def num_shares_not_less_than_threshold!(threshold, num_shares)
         | 
| 134 142 | 
             
                  if num_shares < threshold
         | 
| 135 143 | 
             
                    raise TSS::ArgumentError, "invalid num_shares, must be >= threshold (#{threshold})"
         | 
| @@ -141,9 +149,9 @@ module TSS | |
| 141 149 | 
             
                # The total Byte size of the secret, including padding and hash, must be
         | 
| 142 150 | 
             
                # less than the max allowed Byte size or it is invalid.
         | 
| 143 151 | 
             
                #
         | 
| 144 | 
            -
                # @param secret_bytes  | 
| 145 | 
            -
                # @return  | 
| 146 | 
            -
                # @raise [TSS::ArgumentError] if invalid
         | 
| 152 | 
            +
                # @param secret_bytes the Byte Array containing the secret
         | 
| 153 | 
            +
                # @return returns true if num_shares is >= threshold
         | 
| 154 | 
            +
                # @raise [ParamContractError, TSS::ArgumentError] if invalid
         | 
| 147 155 | 
             
                Contract C::ArrayOf[C::Int] => C::Bool
         | 
| 148 156 | 
             
                def secret_bytes_is_smaller_than_max_size!(secret_bytes)
         | 
| 149 157 | 
             
                  if secret_bytes.size >= 65_535
         | 
| @@ -155,12 +163,13 @@ module TSS | |
| 155 163 |  | 
| 156 164 | 
             
                # Construct a binary share header from its constituent parts.
         | 
| 157 165 | 
             
                #
         | 
| 158 | 
            -
                # @param identifier  | 
| 159 | 
            -
                # @param hash_alg  | 
| 160 | 
            -
                # @param threshold  | 
| 161 | 
            -
                # @param share_len  | 
| 162 | 
            -
                # @return  | 
| 163 | 
            -
                 | 
| 166 | 
            +
                # @param identifier the unique identifier String
         | 
| 167 | 
            +
                # @param hash_alg the hash algorithm String
         | 
| 168 | 
            +
                # @param threshold the threshold value
         | 
| 169 | 
            +
                # @param share_len the length of the share in Bytes
         | 
| 170 | 
            +
                # @return returns an octet String of Bytes containing the binary header
         | 
| 171 | 
            +
                # @raise [ParamContractError] if invalid
         | 
| 172 | 
            +
                Contract C::IdentifierArg, C::HashAlgArg, C::ThresholdArg, C::Int => String
         | 
| 164 173 | 
             
                def share_header(identifier, hash_alg, threshold, share_len)
         | 
| 165 174 | 
             
                  SHARE_HEADER_STRUCT.encode(identifier: identifier,
         | 
| 166 175 | 
             
                                             hash_id: Hasher.code(hash_alg),
         | 
    
        data/lib/tss/tss.rb
    CHANGED
    
    | @@ -60,7 +60,7 @@ module TSS | |
| 60 60 | 
             
              #   to `SHA256`. The use of `NONE` is discouraged as it does not allow those
         | 
| 61 61 | 
             
              #   who are recombining the shares to verify if they have in fact recovered
         | 
| 62 62 | 
             
              #   the correct secret.
         | 
| 63 | 
            -
              # @option opts [String] :format (' | 
| 63 | 
            +
              # @option opts [String] :format ('BINARY') the format of the String share output, 'BINARY' or 'HUMAN'
         | 
| 64 64 | 
             
              # @option opts [Integer] :pad_blocksize (0) An integer representing the nearest multiple of Bytes
         | 
| 65 65 | 
             
              #   to left pad the secret to. Defaults to not adding any padding (0). Padding
         | 
| 66 66 | 
             
              #   is done with the "\u001F" character (decimal 31 in a Byte Array).
         | 
| @@ -75,9 +75,9 @@ module TSS | |
| 75 75 | 
             
              #   place. You would also need to manually remove the padding you added after
         | 
| 76 76 | 
             
              #   the share is recombined, or instruct recipients to ignore it.
         | 
| 77 77 | 
             
              #
         | 
| 78 | 
            -
              # @return  | 
| 78 | 
            +
              # @return an Array of formatted String shares
         | 
| 79 79 | 
             
              # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
         | 
| 80 | 
            -
              Contract ({ :secret =>  | 
| 80 | 
            +
              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], :pad_blocksize => C::Maybe[C::PadBlocksizeArg] }) => C::ArrayOfShares
         | 
| 81 81 | 
             
              def self.split(opts)
         | 
| 82 82 | 
             
                TSS::Splitter.new(opts).split
         | 
| 83 83 | 
             
              end
         | 
| @@ -88,26 +88,26 @@ module TSS | |
| 88 88 | 
             
              #
         | 
| 89 89 | 
             
              # @param [Hash] opts the options to create a message with.
         | 
| 90 90 | 
             
              # @option opts [Array<String>] :shares an Array of String shares to try to recombine into a secret
         | 
| 91 | 
            -
              # @option opts [String] :select_by (' | 
| 91 | 
            +
              # @option opts [String] :select_by ('FIRST') the method to use for selecting
         | 
| 92 92 | 
             
              #   shares from the Array if more then threshold shares are provided. Can be
         | 
| 93 | 
            -
              #   ' | 
| 93 | 
            +
              #   upper case 'FIRST', 'SAMPLE', or 'COMBINATIONS'.
         | 
| 94 94 | 
             
              #
         | 
| 95 95 | 
             
              #   If the number of shares provided as input to the secret
         | 
| 96 96 | 
             
              #   reconstruction operation is greater than the threshold M, then M
         | 
| 97 97 | 
             
              #   of those shares are selected for use in the operation.  The method
         | 
| 98 98 | 
             
              #   used to select the shares can be chosen using the following values:
         | 
| 99 99 | 
             
              #
         | 
| 100 | 
            -
              #   ` | 
| 100 | 
            +
              #   `FIRST` : If X shares are required by the threshold and more than X
         | 
| 101 101 | 
             
              #   shares are provided, then the first X shares in the Array of shares provided
         | 
| 102 102 | 
             
              #   will be used. All others will be discarded and the operation will fail if
         | 
| 103 103 | 
             
              #   those selected shares cannot recreate the secret.
         | 
| 104 104 | 
             
              #
         | 
| 105 | 
            -
              #   ` | 
| 105 | 
            +
              #   `SAMPLE` : If X shares are required by the threshold and more than X
         | 
| 106 106 | 
             
              #   shares are provided, then X shares will be randomly selected from the Array
         | 
| 107 107 | 
             
              #   of shares provided.  All others will be discarded and the operation will
         | 
| 108 108 | 
             
              #   fail if those selected shares cannot recreate the secret.
         | 
| 109 109 | 
             
              #
         | 
| 110 | 
            -
              #   ` | 
| 110 | 
            +
              #   `COMBINATIONS` : If X shares are required, and more than X shares are
         | 
| 111 111 | 
             
              #   provided, then all possible combinations of the threshold number of shares
         | 
| 112 112 | 
             
              #   will be tried to see if the secret can be recreated.
         | 
| 113 113 | 
             
              #   This flexibility comes with a cost. All combinations of `threshold` shares
         | 
| @@ -120,11 +120,11 @@ module TSS | |
| 120 120 | 
             
              #   of combinations will be too large then the an Exception will be raised before
         | 
| 121 121 | 
             
              #   processing has started.
         | 
| 122 122 | 
             
              #
         | 
| 123 | 
            -
              # @return  | 
| 123 | 
            +
              # @return a Hash containing the now combined secret and other metadata
         | 
| 124 124 | 
             
              # @raise [TSS::NoSecretError] if the secret cannot be re-created from the shares provided
         | 
| 125 125 | 
             
              # @raise [TSS::InvalidSecretHashError] if the embedded hash of the secret does not match the hash of the recreated secret
         | 
| 126 126 | 
             
              # @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
         | 
| 127 | 
            -
              Contract ({ :shares => C:: | 
| 127 | 
            +
              Contract ({ :shares => C::ArrayOfShares, :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})
         | 
| 128 128 | 
             
              def self.combine(opts)
         | 
| 129 129 | 
             
                TSS::Combiner.new(opts).combine
         | 
| 130 130 | 
             
              end
         | 
    
        data/lib/tss/util.rb
    CHANGED
    
    | @@ -217,8 +217,8 @@ module TSS | |
| 217 217 |  | 
| 218 218 | 
             
                # Convert a UTF-8 String to an Array of Bytes
         | 
| 219 219 | 
             
                #
         | 
| 220 | 
            -
                # @param str  | 
| 221 | 
            -
                # @return  | 
| 220 | 
            +
                # @param str a UTF-8 String to convert
         | 
| 221 | 
            +
                # @return an Array of Integer Bytes
         | 
| 222 222 | 
             
                Contract String => C::ArrayOf[C::Int]
         | 
| 223 223 | 
             
                def self.utf8_to_bytes(str)
         | 
| 224 224 | 
             
                  str.bytes.to_a
         | 
| @@ -226,8 +226,8 @@ module TSS | |
| 226 226 |  | 
| 227 227 | 
             
                # Convert an Array of Bytes to a UTF-8 String
         | 
| 228 228 | 
             
                #
         | 
| 229 | 
            -
                # @param bytes  | 
| 230 | 
            -
                # @return  | 
| 229 | 
            +
                # @param bytes an Array of Bytes to convert
         | 
| 230 | 
            +
                # @return a UTF-8 String
         | 
| 231 231 | 
             
                Contract C::ArrayOf[C::Int] => String
         | 
| 232 232 | 
             
                def self.bytes_to_utf8(bytes)
         | 
| 233 233 | 
             
                  bytes.pack('C*').force_encoding('utf-8')
         | 
| @@ -235,8 +235,8 @@ module TSS | |
| 235 235 |  | 
| 236 236 | 
             
                # Convert an Array of Bytes to a hex String
         | 
| 237 237 | 
             
                #
         | 
| 238 | 
            -
                # @param bytes  | 
| 239 | 
            -
                # @return  | 
| 238 | 
            +
                # @param bytes an Array of Bytes to convert
         | 
| 239 | 
            +
                # @return a hex String
         | 
| 240 240 | 
             
                Contract C::ArrayOf[C::Int] => String
         | 
| 241 241 | 
             
                def self.bytes_to_hex(bytes)
         | 
| 242 242 | 
             
                  hex = ''
         | 
| @@ -246,8 +246,8 @@ module TSS | |
| 246 246 |  | 
| 247 247 | 
             
                # Convert a hex String to an Array of Bytes
         | 
| 248 248 | 
             
                #
         | 
| 249 | 
            -
                # @param str  | 
| 250 | 
            -
                # @return  | 
| 249 | 
            +
                # @param str a hex String to convert
         | 
| 250 | 
            +
                # @return an Array of Integer Bytes
         | 
| 251 251 | 
             
                Contract String => C::ArrayOf[C::Int]
         | 
| 252 252 | 
             
                def self.hex_to_bytes(str)
         | 
| 253 253 | 
             
                  [str].pack('H*').unpack('C*')
         | 
| @@ -255,8 +255,8 @@ module TSS | |
| 255 255 |  | 
| 256 256 | 
             
                # Convert a hex String to a UTF-8 String
         | 
| 257 257 | 
             
                #
         | 
| 258 | 
            -
                # @param hex  | 
| 259 | 
            -
                # @return  | 
| 258 | 
            +
                # @param hex a hex String to convert
         | 
| 259 | 
            +
                # @return a UTF-8 String
         | 
| 260 260 | 
             
                Contract String => String
         | 
| 261 261 | 
             
                def self.hex_to_utf8(hex)
         | 
| 262 262 | 
             
                  bytes_to_utf8(hex_to_bytes(hex))
         | 
| @@ -264,8 +264,8 @@ module TSS | |
| 264 264 |  | 
| 265 265 | 
             
                # Convert a UTF-8 String to a hex String
         | 
| 266 266 | 
             
                #
         | 
| 267 | 
            -
                # @param str  | 
| 268 | 
            -
                # @return  | 
| 267 | 
            +
                # @param str a UTF-8 String to convert
         | 
| 268 | 
            +
                # @return a hex String
         | 
| 269 269 | 
             
                Contract String => String
         | 
| 270 270 | 
             
                def self.utf8_to_hex(str)
         | 
| 271 271 | 
             
                  bytes_to_hex(utf8_to_bytes(str))
         | 
| @@ -273,10 +273,10 @@ module TSS | |
| 273 273 |  | 
| 274 274 | 
             
                # Left pad a String with pad_char in multiples of byte_multiple
         | 
| 275 275 | 
             
                #
         | 
| 276 | 
            -
                # @param byte_multiple  | 
| 277 | 
            -
                # @param input_string  | 
| 278 | 
            -
                # @param pad_char  | 
| 279 | 
            -
                # @return  | 
| 276 | 
            +
                # @param byte_multiple pad in blocks of this size
         | 
| 277 | 
            +
                # @param input_string the String to pad
         | 
| 278 | 
            +
                # @param pad_char the String to pad with
         | 
| 279 | 
            +
                # @return a padded String
         | 
| 280 280 | 
             
                Contract C::Int, String, String => String
         | 
| 281 281 | 
             
                def self.left_pad(byte_multiple, input_string, pad_char = "\u001F")
         | 
| 282 282 | 
             
                  return input_string if byte_multiple == 0
         | 
| @@ -290,30 +290,33 @@ module TSS | |
| 290 290 | 
             
                #   https://github.com/rack/rack/blob/master/lib/rack/utils.rb
         | 
| 291 291 | 
             
                #
         | 
| 292 292 | 
             
                #   NOTE: the values compared should be of fixed length, such as strings
         | 
| 293 | 
            -
                #   that have already been  | 
| 293 | 
            +
                #   that have already been hashed. This should not be used
         | 
| 294 294 | 
             
                #   on variable length plaintext strings because it could leak length info
         | 
| 295 | 
            -
                #   via timing attacks. | 
| 296 | 
            -
                #   in as the second parameter so as not to leak info about the secret.
         | 
| 295 | 
            +
                #   via timing attacks.
         | 
| 297 296 | 
             
                #
         | 
| 298 | 
            -
                #  | 
| 299 | 
            -
                #  | 
| 300 | 
            -
                # | 
| 297 | 
            +
                #   The user provided value should always be passed in as the second
         | 
| 298 | 
            +
                #   parameter so as not to leak info about the secret.
         | 
| 299 | 
            +
                #
         | 
| 300 | 
            +
                # @param a the private value
         | 
| 301 | 
            +
                # @param b the user provided value
         | 
| 302 | 
            +
                # @return whether the strings match or not
         | 
| 301 303 | 
             
                Contract String, String => C::Bool
         | 
| 302 304 | 
             
                def self.secure_compare(a, b)
         | 
| 303 305 | 
             
                  return false unless a.bytesize == b.bytesize
         | 
| 306 | 
            +
                  ah = Digest::SHA256.hexdigest(a)
         | 
| 307 | 
            +
                  bh = Digest::SHA256.hexdigest(b)
         | 
| 304 308 |  | 
| 305 | 
            -
                  l =  | 
| 306 | 
            -
             | 
| 309 | 
            +
                  l = ah.unpack('C*')
         | 
| 307 310 | 
             
                  r, i = 0, -1
         | 
| 308 | 
            -
                   | 
| 311 | 
            +
                  bh.each_byte { |v| r |= v ^ l[i+=1] }
         | 
| 309 312 | 
             
                  r == 0
         | 
| 310 313 | 
             
                end
         | 
| 311 314 |  | 
| 312 315 | 
             
                # Extract the header data from a binary share.
         | 
| 313 316 | 
             
                # Extra "\x00" padding in the identifier will be removed.
         | 
| 314 317 | 
             
                #
         | 
| 315 | 
            -
                # @param share  | 
| 316 | 
            -
                # @return  | 
| 318 | 
            +
                # @param share a binary octet share
         | 
| 319 | 
            +
                # @return header attributes
         | 
| 317 320 | 
             
                Contract String => Hash
         | 
| 318 321 | 
             
                def self.extract_share_header(share)
         | 
| 319 322 | 
             
                  h = Splitter::SHARE_HEADER_STRUCT.decode(share)
         | 
| @@ -323,8 +326,8 @@ module TSS | |
| 323 326 |  | 
| 324 327 | 
             
                # Calculate the factorial for an Integer.
         | 
| 325 328 | 
             
                #
         | 
| 326 | 
            -
                # @param n  | 
| 327 | 
            -
                # @return  | 
| 329 | 
            +
                # @param n the Integer to calculate for
         | 
| 330 | 
            +
                # @return the factorial of n
         | 
| 328 331 | 
             
                Contract C::Int => C::Int
         | 
| 329 332 | 
             
                def self.factorial(n)
         | 
| 330 333 | 
             
                  (1..n).reduce(:*) || 1
         | 
| @@ -340,22 +343,12 @@ module TSS | |
| 340 343 | 
             
                # * http://chriscontinanza.com/2010/10/29/Array.html
         | 
| 341 344 | 
             
                # * http://stackoverflow.com/questions/2434503/ruby-factorial-function
         | 
| 342 345 | 
             
                #
         | 
| 343 | 
            -
                # @param n  | 
| 344 | 
            -
                # @param r  | 
| 345 | 
            -
                # @return  | 
| 346 | 
            +
                # @param n the total number of shares
         | 
| 347 | 
            +
                # @param r the threshold number of shares
         | 
| 348 | 
            +
                # @return the number of possible combinations
         | 
| 346 349 | 
             
                Contract C::Int, C::Int => C::Int
         | 
| 347 350 | 
             
                def self.calc_combinations(n, r)
         | 
| 348 351 | 
             
                  factorial(n) / (factorial(r) * factorial(n - r))
         | 
| 349 352 | 
             
                end
         | 
| 350 | 
            -
             | 
| 351 | 
            -
                # Converts an Integer into a delimiter separated String.
         | 
| 352 | 
            -
                #
         | 
| 353 | 
            -
                # @param n [Integer] an Integer to convert
         | 
| 354 | 
            -
                # @param delimiter [String] the String to delimit n in three Integer groups
         | 
| 355 | 
            -
                # @return [String] the object converted into a comma separated String.
         | 
| 356 | 
            -
                Contract C::Int, String => String
         | 
| 357 | 
            -
                def self.int_commas(n, delimiter = ',')
         | 
| 358 | 
            -
                  n.to_s.reverse.gsub(%r{([0-9]{3}(?=([0-9])))}, "\\1#{delimiter}").reverse
         | 
| 359 | 
            -
                end
         | 
| 360 353 | 
             
              end
         | 
| 361 354 | 
             
            end
         | 
    
        data/lib/tss/version.rb
    CHANGED
    
    
    
        data/tss.gemspec
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: tss
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.4.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Glenn Rempe
         | 
| @@ -30,7 +30,7 @@ cert_chain: | |
| 30 30 | 
             
              zieXiXZSAojfFx9g91fKdIrlPbInHU/BaCxXSLBwvOM0drE+c2ue9X8gB55XAhzX
         | 
| 31 31 | 
             
              37oBiw==
         | 
| 32 32 | 
             
              -----END CERTIFICATE-----
         | 
| 33 | 
            -
            date: 2016-09- | 
| 33 | 
            +
            date: 2016-09-25 00:00:00.000000000 Z
         | 
| 34 34 | 
             
            dependencies:
         | 
| 35 35 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 36 36 | 
             
              name: sysrandom
         | 
| @@ -186,6 +186,20 @@ dependencies: | |
| 186 186 | 
             
                - - "~>"
         | 
| 187 187 | 
             
                  - !ruby/object:Gem::Version
         | 
| 188 188 | 
             
                    version: '1.3'
         | 
| 189 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 190 | 
            +
              name: yard-contracts
         | 
| 191 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 192 | 
            +
                requirements:
         | 
| 193 | 
            +
                - - "~>"
         | 
| 194 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 195 | 
            +
                    version: '0.1'
         | 
| 196 | 
            +
              type: :development
         | 
| 197 | 
            +
              prerelease: false
         | 
| 198 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 199 | 
            +
                requirements:
         | 
| 200 | 
            +
                - - "~>"
         | 
| 201 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 202 | 
            +
                    version: '0.1'
         | 
| 189 203 | 
             
            description: |2
         | 
| 190 204 | 
             
                  Threshold Secret Sharing (TSS) provides a way to generate N shares
         | 
| 191 205 | 
             
                  from a value, so that any M of those shares can be used to
         | 
    
        metadata.gz.sig
    CHANGED
    
    | @@ -1,2 +1,4 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 1 | 
            +
            H��P��
         | 
| 2 | 
            +
            �jT�*�O��0��k"��~�	���{�9B�O��pf_�ݤCKf���
         | 
| 3 | 
            +
            ����f�#}�T�$߽h<����B,A�8�2H���
         | 
| 4 | 
            +
            V���S}ͷ[�Q4���,V������/����t�"#I���3ދ���,����7��p�/Sq�UƦI0�Q�%8y&;�j�F4ؐң�y@��,;>O��Z��٨3xmO\���cԟ'�3��ӓ8�jC�X�9���.�^0�����B��3�u����
         |