class SymmetricEncryption::CLI

Constants

KEYSTORES

Attributes

activate_key[R]
app_name[R]
cipher_name[R]
cleanup_keys[R]
compress[R]
config_file_path[R]
decrypt[R]
encrypt[R]
environment[R]
environments[RW]
generate[R]
key_path[R]
keystore[R]
migrate[R]
new_keys[R]
output_file_name[R]
prompt[R]
random_password[R]
re_encrypt[R]
regions[R]
rolling_deploy[R]
rotate_kek[R]
rotate_keys[R]
show_version[R]
version[R]

Public Class Methods

new(argv) click to toggle source
# File lib/symmetric_encryption/cli.rb, line 17
def initialize(argv)
  @version          = current_version
  @environment      = ENV["SYMMETRIC_ENCRYPTION_ENV"] || ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
  @config_file_path = File.expand_path(ENV["SYMMETRIC_ENCRYPTION_CONFIG"] || "config/symmetric-encryption.yml")
  @app_name         = "symmetric-encryption"
  @key_path         = "#{ENV['HOME']}/.symmetric-encryption"
  @cipher_name      = "aes-256-cbc"
  @rolling_deploy   = false
  @prompt           = false
  @show_version     = false
  @keystore         = :file

  if argv.empty?
    puts parser
    exit(-10)
  end
  parser.parse!(argv)
end
run!(argv) click to toggle source
# File lib/symmetric_encryption/cli.rb, line 13
def self.run!(argv)
  new(argv).run!
end

Public Instance Methods

parser() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 72
    def parser
      @parser ||= OptionParser.new do |opts|
        opts.banner = <<~BANNER
          Symmetric Encryption v#{VERSION}

            For more information, see: https://encryption.rocketjob.io/

            Note:
              It is recommended to backup the current configuration file, or place it in version control before running
              the configuration manipulation commands below.

          symmetric-encryption [options]
        BANNER

        opts.on "-e", "--encrypt [FILE_NAME]", "Encrypt a file, or read from stdin if no file name is supplied." do |file_name|
          @encrypt = file_name || STDIN
        end

        opts.on "-d", "--decrypt [FILE_NAME]", "Decrypt a file, or read from stdin if no file name is supplied." do |file_name|
          @decrypt = file_name || STDIN
        end

        opts.on "-o", "--output FILE_NAME",
                "Write encrypted or decrypted file to this file, otherwise output goes to stdout." do |file_name|
          @output_file_name = file_name
        end

        opts.on "-P", "--prompt", "When encrypting or decrypting, prompt for a string encrypt or decrypt." do
          @prompt = true
        end

        opts.on "-z", "--compress", "Compress encrypted output file. [Default for encrypting files]" do
          @compress = true
        end

        opts.on "-Z", "--no-compress", "Does not compress the output file. [Default for encrypting strings]" do
          @compress = false
        end

        opts.on "-E", "--env ENVIRONMENT",
                "Environment to use in the config file. Default: SYMMETRIC_ENCRYPTION_ENV || RACK_ENV || RAILS_ENV || 'development'" do |environment|
          @environment = environment
        end

        opts.on "-c", "--config CONFIG_FILE_PATH",
                "File name & path to the Symmetric Encryption configuration file. Default: config/symmetric-encryption.yml or Env var: `SYMMETRIC_ENCRYPTION_CONFIG`" do |path|
          @config_file_path = path
        end

        opts.on "-m", "--migrate", "Migrate configuration file to new format." do
          @migrate = true
        end

        opts.on "-r", "--re-encrypt [PATTERN]",
                'ReEncrypt all files matching the pattern. Default:  "**/*.{yml,rb}"' do |pattern|
          @re_encrypt = pattern || "**/*.{yml,rb}"
        end

        opts.on "-n", "--new-password [SIZE]",
                "Generate a new random password using only characters that are URL-safe base64. Default size is 22." do |size|
          @random_password = (size || 22).to_i
        end

        opts.on "-g", "--generate", "Generate a new configuration file and encryption keys for every environment." do |config|
          @generate = config
        end

        opts.on "-s", "--keystore heroku|environment|file|aws|gcp",
                "Which keystore to use during generation or re-encryption." do |keystore|
          @keystore = (keystore || "file").downcase.to_sym
        end

        opts.on "-B", "--regions [us-east-1,us-east-2,us-west-1,us-west-2]",
                "AWS KMS Regions to encrypt data key with." do |regions|
          @regions = regions.to_s.split(",").collect(&:strip) if regions
        end

        opts.on "-K", "--key-path KEY_PATH",
                "Output path in which to write generated key files. Default: ~/.symmetric-encryption" do |path|
          @key_path = path
        end

        opts.on "-a", "--app-name NAME",
                "Application name to use when generating a new configuration. Default: symmetric-encryption" do |name|
          @app_name = name
        end

        opts.on "-S", "--environments ENVIRONMENTS",
                "Comma separated list of environments for which to generate the config file. Default: development,test,release,production" do |environments|
          @environments = environments.split(",").collect(&:strip).collect(&:to_sym)
        end

        opts.on "-C", "--cipher-name NAME",
                "Name of the cipher to use when generating a new config file, or when rotating keys. Default: aes-256-cbc" do |name|
          @cipher_name = name
        end

        opts.on "-R", "--rotate-keys",
                "Generates a new encryption key version, encryption key files, and updates the configuration file." do
          @rotate_keys = true
        end

        opts.on "-U", "--rotate-kek",
                "Replace the existing key encrypting keys only, the data encryption key is not changed, and updates the configuration file." do
          @rotate_kek = true
        end

        opts.on "-D", "--rolling-deploy",
                "During key rotation, support a rolling deploy by placing the new key second in the list so that it is not activated yet." do
          @rolling_deploy = true
        end

        opts.on "-A", "--activate-key", "Activates the key by moving the key with the highest version to the top." do
          @activate_key = true
        end

        opts.on "-X", "--cleanup-keys",
                "Removes all encryption keys, except the one with the highest version from the configuration file." do
          @cleanup_keys = true
        end

        opts.on "-V", "--key-version NUMBER",
                "Encryption key version to use when encrypting or re-encrypting. Default: (Current global version)." do |number|
          @version = number.to_i
        end

        opts.on "-L", "--ciphers", "List available OpenSSL ciphers." do
          puts "OpenSSL v#{OpenSSL::VERSION}. Available Ciphers:"
          puts OpenSSL::Cipher.ciphers.join("\n")
          exit
        end

        opts.on "-v", "--version", "Display Symmetric Encryption version." do
          @show_version = true
        end

        opts.on("-h", "--help", "Prints this help.") do
          puts opts
          exit
        end
      end
    end
run!() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 36
def run!
  raise(ArgumentError, "Cannot cleanup keys and rotate keys at the same time") if cleanup_keys && rotate_keys

  if show_version
    puts "Symmetric Encryption v#{VERSION}"
    puts "OpenSSL v#{OpenSSL::VERSION}"
    puts "Environment: #{environment}"
  elsif encrypt
    load_config
    prompt ? encrypt_string : encrypt_file(encrypt)
  elsif decrypt
    load_config
    prompt ? decrypt_string : decrypt_file(decrypt)
  elsif random_password
    load_config
    gen_random_password(random_password)
  elsif migrate
    run_migrate
  elsif re_encrypt
    load_config
    SymmetricEncryption::Utils::ReEncryptFiles.new(version: version).process_directory(re_encrypt)
  elsif activate_key
    run_activate_key
  elsif rotate_kek
    run_rotate_kek
  elsif rotate_keys
    run_rotate_keys
  elsif cleanup_keys
    run_cleanup_keys
  elsif generate
    generate_new_config
  else
    puts parser
  end
end

Private Instance Methods

config_file_does_not_exist!() click to toggle source

Ensure that the config file does not already exist before generating a new one.

# File lib/symmetric_encryption/cli.rb, line 359
def config_file_does_not_exist!
  return unless File.exist?(config_file_path)

  puts "\nConfiguration file already exists, please move or rename: #{config_file_path}\n\n"
  exit(-1)
end
current_version() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 352
def current_version
  SymmetricEncryption.cipher.version
rescue SymmetricEncryption::ConfigError
  nil
end
decrypt_file(input_file_name) click to toggle source
# File lib/symmetric_encryption/cli.rb, line 304
def decrypt_file(input_file_name)
  SymmetricEncryption::Reader.decrypt(source: input_file_name, target: output_file_name || STDOUT, version: version)
end
decrypt_string() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 308
def decrypt_string
  begin
    require "highline"
  rescue LoadError
    puts("\nPlease install gem highline before using the command line task to decrypt an entered string.\n   gem install \"highline\"\n\n")
    exit(-2)
  end

  encrypted = HighLine.new.ask("Enter the value to decrypt:")
  text      = SymmetricEncryption.cipher(version).decrypt(encrypted)

  puts("\n\nEncrypted: #{encrypted}")
  output_file_name ? File.open(output_file_name, "wb") { |f| f << text } : puts("Decrypted: #{text}\n\n")
end
encrypt_file(input_file_name) click to toggle source
# File lib/symmetric_encryption/cli.rb, line 299
    def encrypt_file(input_file_name)
      SymmetricEncryption::Writer.encrypt(source: input_file_name, target: output_file_name || STDOUT, compress: compress,
version: version)
    end
encrypt_string() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 323
def encrypt_string
  begin
    require "highline"
  rescue LoadError
    puts("\nPlease install gem highline before using the command line task to encrypt an entered string.\n   gem install \"highline\"\n\n")
    exit(-2)
  end
  value1 = nil
  value2 = 0

  while value1 != value2
    value1 = HighLine.new.ask("Enter the value to encrypt:") { |q| q.echo = "*" }
    value2 = HighLine.new.ask("Re-enter the value to encrypt:") { |q| q.echo = "*" }

    puts("Values do not match, please try again") if value1 != value2
  end
  compress  = false if compress.nil?
  encrypted = SymmetricEncryption.cipher(version).encrypt(value1, compress: compress)
  output_file_name ? File.open(output_file_name, "wb") { |f| f << encrypted } : puts("\n\nEncrypted: #{encrypted}\n\n")
end
gen_random_password(size) click to toggle source
# File lib/symmetric_encryption/cli.rb, line 344
def gen_random_password(size)
  p = SymmetricEncryption.random_password(size)
  puts("\nGenerated Password: #{p}")
  encrypted = SymmetricEncryption.encrypt(p)
  puts("Encrypted: #{encrypted}\n\n")
  File.open(output_file_name, "wb") { |f| f << encrypted } if output_file_name
end
generate_new_config() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 223
def generate_new_config
  unless KEYSTORES.include?(keystore)
    puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
    exit(-3)
  end

  config_file_does_not_exist!
  self.environments ||= %i[development test release production]
  args = {
    app_name:     app_name,
    environments: environments,
    cipher_name:  cipher_name
  }
  args[:key_path]   = key_path if key_path
  args[:regions]    = regions if regions && !regions.empty?
  cfg               = Keystore.generate_data_keys(keystore: keystore, **args)
  Config.write_file(config_file_path, cfg)
  puts "New configuration file created at: #{config_file_path}"
end
load_config() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 219
def load_config
  Config.load!(file_name: config_file_path, env: environment)
end
run_activate_key() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 284
def run_activate_key
  config = Config.read_file(config_file_path)
  config.each_pair do |env, cfg|
    next if environments && !environments.include?(env.to_sym)
    next unless ciphers = cfg[:ciphers]

    highest = ciphers.max_by { |i| i[:version] }
    ciphers.delete(highest)
    ciphers.unshift(highest)
  end

  Config.write_file(config_file_path, config)
  puts "Activated the keys with the highest versions in: #{config_file_path}"
end
run_cleanup_keys() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 269
def run_cleanup_keys
  config = Config.read_file(config_file_path)
  config.each_pair do |env, cfg|
    next if environments && !environments.include?(env.to_sym)
    next unless ciphers = cfg[:ciphers]

    highest = ciphers.max_by { |i| i[:version] }
    ciphers.clear
    ciphers << highest
  end

  Config.write_file(config_file_path, config)
  puts "Removed all but the key with the highest version in: #{config_file_path}"
end
run_migrate() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 243
def run_migrate
  config = Config.read_file(config_file_path)
  Config.write_file(config_file_path, config)
  puts "Existing configuration file successfully migrated to the new format: #{config_file_path}"
end
run_rotate_kek() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 262
def run_rotate_kek
  config = Config.read_file(config_file_path)
  SymmetricEncryption::Keystore.rotate_key_encrypting_keys!(config, environments: environments || [], app_name: app_name)
  Config.write_file(config_file_path, config)
  puts "Existing configuration file updated with new key encrypting keys: #{config_file_path}"
end
run_rotate_keys() click to toggle source
# File lib/symmetric_encryption/cli.rb, line 249
    def run_rotate_keys
      if keystore && !KEYSTORES.include?(keystore)
        puts "Invalid keystore option: #{keystore}, must be one of #{KEYSTORES.join(', ')}"
        exit(-3)
      end

      config = Config.read_file(config_file_path)
      SymmetricEncryption::Keystore.rotate_keys!(config, environments: environments || [], app_name: app_name,
rolling_deploy: rolling_deploy, keystore: keystore)
      Config.write_file(config_file_path, config)
      puts "Existing configuration file updated with new keys: #{config_file_path}"
    end