<commit_before>###############################################################################
#
# thesplit - An API server to support the secure sharing of secrets.
# Copyright (c) 2016  <NAME>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

class SecretsController < ApplicationController
  post '/' do
    Stats.store('views/api/v1/secrets', count: 1, post: 1)

    # ID is 16 byte hash of the data that was stored
    param :id, String, required: true, min_length: 32, max_length: 32,
                       format: settings.hex_regex

    param :boxNonceB64, String, required: true, min_length: 24, max_length: 64,
                                format: settings.base64_regex

    param :boxB64, String, required: true, min_length: 1,
                           max_length: settings.secrets_max_length,
                           format: settings.base64_regex

    param :scryptSaltB64, String, required: true, min_length: 24, max_length: 64,
                                  format: settings.base64_regex

    # Differentiate between the client_hash_id and the server_hash_id.
    # The client hash is the hash of the contents generated by the client
    # and used as the client ID for requesting a secret.
    #
    # The server_hash_id is just the SHA256(client_hash_id) and is what is
    # used on the server side to store and find secrets.
    #
    # This simple step is taken help prevent reversing of the server storeage
    # key back to its original client ID. This prevents an attacker who gains
    # control of a DB dump from being able to search for that ID globally
    # (e.g. searching all email, or all Twitter, etc.) in order to
    # learn something about who has held that key ID. A small step
    # towards protecting the anonymity of the creator.
    #
    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)
    vault_index_key = "secret/#{server_hash_id}"

    scrypt_salt_b64 = params['scryptSaltB64']
    box_nonce_b64 = params['boxNonceB64']
    box_b64 = params['boxB64']

    t     = Time.now
    t_exp = t + settings.secrets_expire_in

    obj = { boxNonceB64: box_nonce_b64,
            boxB64: box_b64,
            scryptSaltB64: scrypt_salt_b64 }

    if Vault.logical.read(vault_index_key).present?
      halt 409, error_json('Data conflict, secret with ID already exists', 409)
    end

    one_time_token = vault_token_2<PASSWORD>

    # store the value of the one time token in a place we can find it
    Vault.logical.write(vault_index_key, token: one_time_token)

    # Store secret data using the one-time-use token
    # Instantiate a new Vault::Client in order to auth with the one-time token
    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(one_time_token)
    # token num_uses - 1
    vc.logical.write("cubbyhole/#{server_hash_id}", obj)

    # Send the hash to the blockchain. It will be passed through
    # SHA256 one more time before anchoring on the BTC blockchain.
    BlockchainSendHashWorker.perform_async(server_hash_id)

    return success_json(id: client_hash_id, createdAt: t.utc.iso8601,
                        expiresAt: t_exp.utc.iso8601)
  end

  options '/' do
    response.headers['Allow'] = 'POST'
    200
  end

  delete '/:id' do
    Stats.store('views/api/v1/secrets/id', count: 1)

    # ID is 16 byte hash of the data that was stored
    param :id, String, required: true, min_length: 32, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)
    vault_index_key = "secret/#{server_hash_id}"

    # find and revoke the token, which will also destroy any cubbyhole data
    vault_token = Vault.logical.read(vault_index_key)

    raise Sinatra::NotFound if vault_token.blank? || vault_token.data.blank?

    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(vault_token.data[:token])
    # token num_uses - 1
    # revocation of token also destroys any cubbyhole secrets
    vc.auth_token.revoke_self

    # deleting the index that let us find the token
    Vault.logical.delete(vault_index_key)

    return success_json
  end

  get '/:id' do
    Stats.store('views/api/v1/secrets/id', count: 1)

    # ID is the 16 byte hash of the data that was stored
    param :id, String, required: true, min_length: 32, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)

    vault_secret = nil
    vault_index_key = "secret/#{server_hash_id}"

    # Retrive the one-time use token using the app token
    vault_token = Vault.logical.read(vault_index_key)

    raise Sinatra::NotFound if vault_token.blank? || vault_token.data.blank?

    # Instantiate a new Vault::Client in order to auth with the one-time token
    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(vault_token.data[:token])
    # token num_uses - 1
    # one-time token private cubbyhole
    vault_secret = vc.logical.read("cubbyhole/#{server_hash_id}")

    # cleanup the index with the cubbyhole token
    Vault.logical.delete(vault_index_key)

    raise Sinatra::NotFound if vault_secret.blank? || vault_secret.data.blank?

    return success_json(vault_secret.data)
  end

  options '/:id' do
    response.headers['Allow'] = 'GET,DELETE'
    200
  end

  get '/:id/receipt' do
    Stats.store('views/api/v1/secrets/id/validate', count: 1)

    # ID is the 16 byte hash of the data that was stored
    param :id, String, required: true, min_length: 32, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)

    r = $redis.hgetall("blockchain:id:#{server_hash_id}")

    raise Sinatra::NotFound if r.blank?

    begin
      hash_item = JSON.parse(r['hash_item']) unless r['hash_item'].blank?
      receipt = JSON.parse(r['receipt']) unless r['receipt'].blank?
      confirmed = Time.parse(r['confirmed']) unless r['confirmed'].blank?
    rescue StandardError
      halt 500, error_json('server blockchain receipt could not be parsed', 500)
    end

    t_receipt = Tierion::HashApi::Receipt.new(receipt) unless receipt.blank?

    if t_receipt && !t_receipt.valid?
      halt 500, error_json('server receipt is invalid', 500)
    end

    obj = {}
    obj[:hash_item] = hash_item.present? ? hash_item : nil
    obj[:receipt] = t_receipt.present? ? t_receipt : nil
    obj[:confirmed] = confirmed.present? ? confirmed.utc.iso8601 : nil<commit_msg>Allow the ID to be between 10 and 16 bytes (20-32 hex chars) in length<commit_after>###############################################################################
#
# thesplit - An API server to support the secure sharing of secrets.
# Copyright (c) 2016  <NAME>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

class SecretsController < ApplicationController
  post '/' do
    Stats.store('views/api/v1/secrets', count: 1, post: 1)

    param :id, String, required: true, min_length: 20, max_length: 32,
                       format: settings.hex_regex

    param :boxNonceB64, String, required: true, min_length: 24, max_length: 64,
                                format: settings.base64_regex

    param :boxB64, String, required: true, min_length: 1,
                           max_length: settings.secrets_max_length,
                           format: settings.base64_regex

    param :scryptSaltB64, String, required: true, min_length: 24, max_length: 64,
                                  format: settings.base64_regex

    # Differentiate between the client_hash_id and the server_hash_id.
    # The client hash is the hash of the contents generated by the client
    # and used as the client ID for requesting a secret.
    #
    # The server_hash_id is just the SHA256(client_hash_id) and is what is
    # used on the server side to store and find secrets.
    #
    # This simple step is taken help prevent reversing of the server storeage
    # key back to its original client ID. This prevents an attacker who gains
    # control of a DB dump from being able to search for that ID globally
    # (e.g. searching all email, or all Twitter, etc.) in order to
    # learn something about who has held that key ID. A small step
    # towards protecting the anonymity of the creator.
    #
    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)
    vault_index_key = "secret/#{server_hash_id}"

    scrypt_salt_b64 = params['scryptSaltB64']
    box_nonce_b64 = params['boxNonceB64']
    box_b64 = params['boxB64']

    t     = Time.now
    t_exp = t + settings.secrets_expire_in

    obj = { boxNonceB64: box_nonce_b64,
            boxB64: box_b64,
            scryptSaltB64: scrypt_salt_b64 }

    if Vault.logical.read(vault_index_key).present?
      halt 409, error_json('Data conflict, secret with ID already exists', 409)
    end

    one_time_token = vault_token_24h_1x

    # store the value of the one time token in a place we can find it
    Vault.logical.write(vault_index_key, token: one_time_token)

    # Store secret data using the one-time-use token
    # Instantiate a new Vault::Client in order to auth with the one-time token
    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(one_time_token)
    # token num_uses - 1
    vc.logical.write("cubbyhole/#{server_hash_id}", obj)

    # Send the hash to the blockchain. It will be passed through
    # SHA256 one more time before anchoring on the BTC blockchain.
    BlockchainSendHashWorker.perform_async(server_hash_id)

    return success_json(id: client_hash_id, createdAt: t.utc.iso8601,
                        expiresAt: t_exp.utc.iso8601)
  end

  options '/' do
    response.headers['Allow'] = 'POST'
    200
  end

  delete '/:id' do
    Stats.store('views/api/v1/secrets/id', count: 1)

    param :id, String, required: true, min_length: 20, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)
    vault_index_key = "secret/#{server_hash_id}"

    # find and revoke the token, which will also destroy any cubbyhole data
    vault_token = Vault.logical.read(vault_index_key)

    raise Sinatra::NotFound if vault_token.blank? || vault_token.data.blank?

    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(vault_token.data[:token])
    # token num_uses - 1
    # revocation of token also destroys any cubbyhole secrets
    vc.auth_token.revoke_self

    # deleting the index that let us find the token
    Vault.logical.delete(vault_index_key)

    return success_json
  end

  get '/:id' do
    Stats.store('views/api/v1/secrets/id', count: 1)

    param :id, String, required: true, min_length: 20, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)

    vault_secret = nil
    vault_index_key = "secret/#{server_hash_id}"

    # Retrive the one-time use token using the app token
    vault_token = Vault.logical.read(vault_index_key)

    raise Sinatra::NotFound if vault_token.blank? || vault_token.data.blank?

    # Instantiate a new Vault::Client in order to auth with the one-time token
    vc = Vault::Client.new
    # token num_uses - 1
    vc.auth.token(vault_token.data[:token])
    # token num_uses - 1
    # one-time token private cubbyhole
    vault_secret = vc.logical.read("cubbyhole/#{server_hash_id}")

    # cleanup the index with the cubbyhole token
    Vault.logical.delete(vault_index_key)

    raise Sinatra::NotFound if vault_secret.blank? || vault_secret.data.blank?

    return success_json(vault_secret.data)
  end

  options '/:id' do
    response.headers['Allow'] = 'GET,DELETE'
    200
  end

  get '/:id/receipt' do
    Stats.store('views/api/v1/secrets/id/validate', count: 1)

    param :id, String, required: true, min_length: 20, max_length: 32,
                       format: settings.hex_regex

    client_hash_id = params['id']
    server_hash_id = Digest::SHA256.hexdigest(client_hash_id)

    r = $redis.hgetall("blockchain:id:#{server_hash_id}")

    raise Sinatra::NotFound if r.blank?

    begin
      hash_item = JSON.parse(r['hash_item']) unless r['hash_item'].blank?
      receipt = JSON.parse(r['receipt']) unless r['receipt'].blank?
      confirmed = Time.parse(r['confirmed']) unless r['confirmed'].blank?
    rescue StandardError
      halt 500, error_json('server blockchain receipt could not be parsed', 500)
    end

    t_receipt = Tierion::HashApi::Receipt.new(receipt) unless receipt.blank?

    if t_receipt && !t_receipt.valid?
      halt 500, error_json('server receipt is invalid', 500)
    end

    obj = {}
    obj[:hash_item] = hash_item.present? ? hash_item : nil
    obj[:receipt] = t_receipt.present? ? t_receipt : nil
    obj[:confirmed] = confirmed.present? ? confirmed.utc.iso8601 : nil
{'fraction_non_alphanumeric': 0.10925592351392545, 'fraction_numerical': 0.02009145074130525, 'mean_word_length': 3.1767939814814814, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2210485', 'n_tokens_mistral': 4787, 'n_tokens_neox': 4356, 'n_words': 1377}