module InsxSync

Constants

VERSION

Public Instance Methods

configure() click to toggle source

Configures the application based on user input. Stores encrpyted config in a file as a delimited base 64 string

# File lib/insxsync.rb, line 226
def configure
  # verify the config directory exists
  Dir.mkdir(File.join(Dir.home, '.insxsync')) if not File.exists?(File.join(Dir.home, '.insxsync'))

  # check if the configuration file exists and ask the user if it should be overwritten
  if File.exists?(File.join(Dir.home, '.insxsync', 'config.ini'))
    require_condition yesno("Configuration exists. Overwrite?", false), "Already configured. No changes made..."
  end

  # open the config file for writing
  File.open(File.join(Dir.home, '.insxsync', 'config.ini'), 'w') do |file|
    # Get the password for the new config and verify it was entered correctly twice
    pass  = get_prompt("Please enter a credential storage password: ")
    pass2 = get_prompt("Please verify the credential storage password: ")
    require_condition pass == pass2, "Password mismatch. Exiting..."

    # Get the access key id and secret access key
    accessKey     = get_prompt("Please enter your AWS access key ID for insxsync: ", true)
    accessSecret  = get_prompt("Please enter your AWS secret access key for insxsync: ", true)

    # check the credentials work if the user wants
    if yesno("Would you like to test your credentials?", true)
      passed, message = testAWS(accessKey, accessSecret, false)
      require_condition passed, "AWS test failed! Invalid AWS credentials specified. No changes made..."
    end

    # generate the data string to be encrypted
    data = "AccessKeyId = '#{accessKey}'\nSecretAccessKey = '#{accessSecret}'"

    # generate the key and IV
    key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, 'P@ssw0rd00!', 5813, 32)
    iv = gen_iv(pass)

    # setup the encryption cipher
    cipher = OpenSSL::Cipher::AES256.new(:CFB)
    cipher.encrypt
    cipher.key = key
    cipher.iv = iv

    # encrypt the data
    encrypted = cipher.update(data) + cipher.final

    # convert the encrypted data to a delimited base64 string
    contents = "===BEGIN AWS CREDENTIALS===\n" + Base64.encode64(encrypted) + "===END AWS CREDENTIALS==="

    # write the contents out to the file
    file.write(contents)
  end
  puts "Configuration successful!"
end
exporting?() click to toggle source

Returns the truth value of whether the application is running$command an export operation @return [true, false] whether the application in running an export

# File lib/insxsync.rb, line 302
def exporting?
  $OLD_ARGV0 == "export"
end
gen_iv(data) click to toggle source

Generates the initialization vector for the applications cryptography based on a passed string Uses a character representation of a truncated 512-bit SHA2 hash @param [String] data a string of data to generate the IV from @return [String] the initialization vector

# File lib/insxsync.rb, line 200
def gen_iv (data)
  # Generate an 512-bit SHA2 hash of the data and map it to an array of bytes
  temp = Digest::SHA2.new(512).update(data).to_s.scan(/../).map(&:hex)

  # set the array equal to an array of each of its halves
  temp = temp.each_slice(32).to_a

  # Add the second half of the array to the first
  temp[1].each_index do |index|
    temp[0][index] += temp[1][index]
  end

  # set the array equal to an array of each half of the zeroth array element
  temp = temp[0].each_slice(16).to_a

  # Add the second half of the array to the first
  temp[1].each_index do |index|
    temp[0][index] += temp[1][index]
  end

  # set the IV to a character representation of the first half of the array of bytes
  iv = temp[0].pack('c*')
end
get_prompt(prompt="Enter Password: ", echo=false) click to toggle source

Displays a prompt for user input. Allows the echo of the input to be disabled @param [String] prompt The prompt to display to the user @param [true, false] whether to echo the input or not @return [String] the users input

# File lib/insxsync.rb, line 296
def get_prompt(prompt="Enter Password: ", echo=false)
  ask(prompt) {|q| q.echo = echo}
end
loadCredentials() click to toggle source

Loads AWS credentials from config file into global variables

# File lib/insxsync.rb, line 34
def loadCredentials
  # get config file path
  configFile = File.join(Dir.home, '.insxsync', 'config.ini')

  # Ensure config exists
  require_condition File.exists?(configFile), "Application not configured. Exiting..."

  # get the password for credential storage
  p = get_prompt("Please enter credential storage password: ")

  # get config file contents
  content = File.open(configFile, 'r').read

  # get the encrypted file contents
  encrypted = content.scan(/===BEGIN AWS CREDENTIALS===.(.*).===END AWS CREDENTIALS===/m).flatten[0]

  # Generate IV and key from password
  iv = gen_iv(p)
  key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(p, 'P@ssw0rd00!', 5813, 32)

  # create and configure decipher
  decipher = OpenSSL::Cipher::AES256.new(:CFB)
  decipher.decrypt
  decipher.key = key
  decipher.iv = iv

  # decrypt contents
  plain = decipher.update(Base64.decode64(encrypted)) + decipher.final

  # retrieve access key id and secret access key
  accessKeyId = plain.scan(/AccessKeyId\s*=\s*'([^']*)'/).flatten[0]
  secretAccessKey = plain.scan(/SecretAccessKey\s*=\s*'([^']*)'/).flatten[0]

  require_condition accessKeyId.not_nil?, "AWS credentials could not be retrieved. Please check your password and try again..."
  require_condition secretAccessKey.not_nil?, "AWS credentials could not be retrieved. Please check your password and try again..."

  $AWS_ID = accessKeyId
  $AWS_SECRET = secretAccessKey
end
require_condition(condition, message) click to toggle source

Exits the application and displays the message specified @param [true, false] condition the truth value of the condition to be tested @param [String] message the message to be displayed in the event of a false condition

# File lib/insxsync.rb, line 342
def require_condition(condition, message)
  if not condition
    $stderr.puts message
    exit -1
  end
end
syncing?() click to toggle source

Returns the truth value of whether the application is running an sync operation @return [true, false] whether the application in running an sync

# File lib/insxsync.rb, line 308
def syncing?
  $OLD_ARGV0 =~ /sync(-uat)?/
end
testAWS(key=nil, secret=nil, echo = true) click to toggle source

Tests all necessary connections and acces to AWS @param [String] key AWS Access Key ID. Default: nil @param [String] secret AWS Secret Access Key Default: nil @param [true, false] echo whether to echo test progress or not @return [Boolean] truth value of test success @return [String] explanatory message if failed, nil otherwise

# File lib/insxsync.rb, line 80
def testAWS(key=nil, secret=nil, echo = true)
  # if both key and secret are not specified
  if ((key.not_nil? and secret.nil?) or (key.nil? and secret.not_nil?))
    # return false for failure. Message not needed since the presence of
    # key or secret implies a programmatic test, not interactive
    return false
  end

  # if no key was specified
  if key.nil?
    # get the password for credential storage
    p = get_prompt("Please enter credential storage password: ")

    print "Locating configuration...\t\t" if echo

    # get config file path
    configFile = File.join(Dir.home, '.insxsync', 'config.ini')

    # Fail if it does not exist
    if not File.exists?(configFile)
      return false, "\nApplication not configured. Please run 'insxsync configure' to fix. Exiting..."
    end

    print "OK\n" if echo


    print "Decrypting credentials...\t\t" if echo
    # get config file contents
    content = File.open(configFile, 'r').read

    # get the encrypted file contents
    encrypted = content.scan(/===BEGIN AWS CREDENTIALS===.(.*).===END AWS CREDENTIALS===/m).flatten[0]

    # Generate IV and key from password
    iv = gen_iv(p)
    key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(p, 'P@ssw0rd00!', 5813, 32)

    # create and configure decipher
    decipher = OpenSSL::Cipher::AES256.new(:CFB)
    decipher.decrypt
    decipher.key = key
    decipher.iv = iv

    # decrypt contents
    plain = decipher.update(Base64.decode64(encrypted)) + decipher.final

    # retrieve access key id and secret access key
    accessKeyId = plain.scan(/AccessKeyId\s*=\s*'([^']*)'/).flatten[0]
    secretAccessKey = plain.scan(/SecretAccessKey\s*=\s*'([^']*)'/).flatten[0]

    # if either are nil, fail
    if accessKeyId.nil? or secretAccessKey.nil?
      return false, "\nAWS credentials could not be retrieved. Please check your password and try again..."
    end

    print "OK\n" if echo
  else # key and secret provided as arguments
    accessKeyId = key
    secretAccessKey = secret
  end


  print "Accessing S3 Buckets...\t\t" if echo

  # create AWS SDK objects for test
  begin
    # This is the bucket object for sync data
    syncBucket = AWS::S3.new(:access_key_id=>accessKeyId, :secret_access_key=>secretAccessKey).buckets[$SYNC_BUCKET]

    # this assumes the role on the production account to access CGE backups
    sts = AWS::STS.new(:access_key_id=>accessKeyId, :secret_access_key=>secretAccessKey)
    role = sts.assume_role(:role_arn => $ROLE_ARN, :role_session_name => SecureRandom.uuid.gsub(/-/,''), :duration_seconds => 60 * 15)

    # store role credentials
    creds = role[:credentials]

    # open the CGE backup bucket
    cgeBucket = AWS::S3.new(creds).buckets[$CGE_BUCKET]
  rescue AWS::STS::Errors::InvalidClientTokenId # in event key does not have permission for role
    return false, "\nAWS credentials invalid. Please check them and try again..."
  rescue AWS::STS::Errors::SignatureDoesNotMatch # in event key and secret do not match
    return false, "\nAWS credentials invalid. Please check them and try again..."
  end
  print "OK\n" if echo

  print "Enumerating objects...\t\t" if echo
  cgeBucket.objects.each
  syncBucket.objects.each
  print "OK\n" if echo

  print "Testing upload...\t\t" if echo
  syncBucket.objects['insxsync.upload'].write("This file was uploaded successfully!")
  print "OK\n" if echo

  print "Testing deletion...\t\t" if echo
  syncBucket.objects['insxsync.upload'].delete
  print "OK\n" if echo

  print "Testing download...\t\t" if echo
  syncTest = syncBucket.objects['insxsync.test'].read
  cgeTest = cgeBucket.objects['insxsync.test'].read

  if syncTest != "This file was downloaded successfully!"
    return false, "Download from sync bucket failed!"
  end

  if cgeTest != "This file was downloaded successfully!"
    return false, "Download from CGE backups bucket failed!"
  end

  print "OK\n" if echo

  puts "Test successful!" if echo
  return true
end
which(*cmds) click to toggle source

Acts like the *nix which command. Given an array of commands ordered from most prefered to lease, it returns the full path to the first command it finds. Operating system agnostic. @overload which(cmd)

Finds the path to the specified command.
@param [String] cmd command name
@return [String] full path to specified command. Nil if the command was not found.

@overload which(cmd)

Finds the path to the specified command.
@param [Array<String>] cmds command names in order of preference
@return [String] full path to first command found. Nil if none were found.
# File lib/insxsync.rb, line 322
def which(*cmds)
  # For each command
  cmds.flatten.each do |cmd|
    # Get the list of executable extensions (if any)
    exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
    exts.push('') if not exts.include?('')  # Needs to inlcude a blank extension for *nix systems
    ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|  # For each dir in the PATH variable
      exts.each { |ext|
        # Check to see if the command exists in the directory with one of the possible extensions
        exe = File.join(path, "#{cmd}#{ext}")
        return exe if File.executable? exe
      }
    end
  end
  return nil  # Oops, guess the command isn't there
end
yesno(prompt = 'Continue?', default = true) click to toggle source

Asks the user for a single character of input for yes or no @param [String] prmpts the prompt to ask @param [true, false] default default choice @return [true, false] the user's choice

# File lib/insxsync.rb, line 281
def yesno(prompt = 'Continue?', default = true)
  a = ''
  s = default ? '[Y/n]' : '[y/N]'
  d = default ? 'y' : 'n'
  until %w[y n].include? a
    a = ask("#{prompt} #{s} ") { |q| q.limit = 1; q.case = :downcase }
    a = d if a.length == 0
  end
  a == 'y'
end