phime42/rubin

View on GitHub
classes.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'rubygems'
require 'bundler/setup'
require 'time'
require 'sequel'
require 'json'
require 'rbnacl/libsodium'
require 'sqlite3'
require 'digest'
require 'base64'
require 'socket'
require 'irc-socket'
require 'irc_parser'
require 'thread'


$dbpath = "sqlite://test.db"


##
# manages the startup of all bots and clients
class Starter
  def initialize  # note: for each plugin call (network-plugin, irc-plugin...: start a new thread!)
    services_to_start = []
    db = DatabaseBox.new
    db.output_all_clients.each do |x|
      if x[:type].eql? 'irc'
        irc_queue = Queue.new
        services_to_start << Thread.new {
          puts "connecting to #{x[:channel]} on #{x[:host]}..."
          host = x[:host].split(':')[1].split('//')[1]  # http://irc.freenode.org:7000 --> irc.freenode.org
          port = x[:host].split(':')[2]
          channel = x[:channel]
          nick = x[:nick]
          RelayChat.new(host, port, channel, nick).connect
        }
      elsif x[:type].eql? 'email'
        # do something pretty with email
      elsif x[:type].eql? 'xmpp'
        # do something pretty with xmpp
      end
    end
    services_to_start.each{ |t| t.join }
  end
end


##
# This module opens an IRC connection and saves all the raw irc data to the database
class RelayChat
  def initialize(server, port, channel, nick)
    @server = server
    @port = port
    @channel = channel
    @nick = nick
  end

  # connects to the given irc channel
  def connect
    irc = IRCSocket.new(@server, @port, true)
    irc.connect

    if irc.connected?
      irc.nick @nick
      irc.user(@nick, 0, "*", @nick)

      sending = Thread.new {
        # this thread waits for a message sent by the user and sends it to the irc channel of this instance
        }

      receiving = Thread.new {
        # this thread reads messages from this instance's irc and writes them to the database
        while line = irc.read
          if line.split[1] == '376'
            irc.join @channel
          end
          # puts line  # reactivate if a detailed unfiltered output of the irc server messages is desired
          msg = IRCParser.parse_raw("#{line}\r\n")
          if msg[1].eql? 'JOIN'
            # connected to channel
            puts "joined channel #{@channel}@#{@server}"
          elsif msg[1].eql? 'PRIVMSG'
            sender_nick = msg[0].split('!~')[0]  # strips off client and IP
            receiving_channel = msg[2][0]
            message = msg[2][1]

            adapter = EncryptedAdapter.new
            if receiving_channel.eql? @nick
              # received private message
              adapter.write_encrypted_message(Time.new, "#{@channel.split('#')[1]}@#{@server}", true, sender_nick, message, 'nil')
            else
              adapter.write_encrypted_message(Time.new, "#{@channel.split('#')[1]}@#{@server}", false, sender_nick, message, 'nil')
            end
          end
        end
      }

      [sending, receiving].each{ |t| t.join }

    end
  end

  def send_to_channel(message)
  end

  def send_to_user(user, message)
  end

end

class DatabaseBox  # OPTIMIZE: rewrite this class to be more ordered and suitable for general use
  attr_reader :messages_ds, :keys_ds # make dataset readable
  def initialize
    # check if database was used before, otherwise generate what we need
    @DB
    @messages_ds
    @keys_ds
    @clients_ds

    setup_message_database
    setup_key_database
    setup_client_database

    @DB = Sequel.connect($dbpath)
    @messages_ds = @DB[:messages]  # create dataset for messages
    @keys_ds = @DB[:keys]  # create dataset for keys
    @clients_ds = @DB[:clients]
  end

  # reads the client table and outputs all clients as an array
  def output_all_clients
    client_array = []
    @clients_ds.to_a.each do |element|
      client_array << element
    end
    client_array
  end

  def register_new_client(description, host, type, nick, realname, channel)
    if @clients_ds.where(:host=>host).where(:channel=>channel).to_a.length.eql? 0  # do not load configuration if a config with same channel and same server already exists
      @clients_ds.insert(:description=>description, :host=>host, :type=>type, :nick=>nick, :realname=>realname, :channel=>channel)
    end
  end

  # write the message from the client application to the database
  def write_message_to_database(timestamp, client, private, sender, message, attachment, key_id)
    @messages_ds.insert(:time => timestamp, :client => client, :private => private, :sender => sender, :message => message, :attachment => attachment, :key_id =>key_id)
  end

  # searches for the message with id = message_id and key_id = key_id
  def output_message_by_id(message_id, key_id)
    message = @messages_ds.where(:id => message_id).to_a[0]  # outputs a message
    database_key_id = message[:key_id]
    if database_key_id.eql?(key_id.to_i)
      message
    else
      nil
    end
  end

  def output_all_message_ids_by_key_id(key_id)
    # outputs all key-ids available for the given key_id
    # lets the client decide which messages he wants to download and saves time and resources
    # todo: implement ranges ("the last 1000 messages that are for me etc")
    message_id_output_array = []
    @messages_ds.where(:key_id => key_id).to_a.each do |element|
      message_id_output_array << element[:id]
    end
    message_id_output_array
  end

  def output_new_message_ids(key_id, message_id)
    # outputs an array of every message id bigger than the given message_id
    message_id_output_array = []
    @messages_ds.where(:key_id => key_id).to_a.each do |element|
      if (element[:id] > message_id)  # todo: substitute with direct sequel query
        message_id_output_array << element[:id]
      end
    end
    message_id_output_array
  end

  def output_message_by_days(key_id, days)
    # queries the database for a certain range of days since today
  end

  def register_key (description, host, private_key, public_key)
    @keys_ds.insert(:description => description, :host => host, :private_key => private_key, :public_key => public_key, :revoked => false)
  end

  def revoke_key(public_key, key_id)
    if !public_key.nil?
      @keys_ds.where(:public_key=>Base64.encode64(public_key)).update(:revoked=>true)
    end
    if !key_id.nil?
      @keys_ds.where(:id => key_id).update(:revoked=>true)
    end
  end

  def output_host_keypair
    found_keypairs = @keys_ds.exclude(:private_key=>nil).exclude(:revoked=>true).to_a
    if found_keypairs.length < 1
      # no keypair found. have to generate one
      # todo: generate keypair
      # crypto = CryptoBox.new  # todo: put host key generation somewhere else where it makes sense and is executed not just by accident
      [nil, nil]
    elsif found_keypairs.length == 1
      # exactly one keypair found. returning it
      [Base64.decode64(found_keypairs[0][:public_key]), Base64.decode64(found_keypairs[0][:private_key])]
    elsif found_keypairs.length > 1
      # there's more than one valid hostkey in the database. something went terribly wrong
      # todo: think about handling it; maybe delete every host key present
      # todo: implement helpful logging
      puts 'there is more then one valid host key present, database corrupt. data breach?'
    end
  end

  def output_host_public_key
    public_key, private_key = self.output_host_keypair
    public_key
  end

  def output_all_keys
    pubkey_array = []
    found_keys = @keys_ds.where(:revoked=>false).where(:private_key=>nil).to_a
    found_keys.each do |element|
      pubkey_array << [Base64.decode64(element[:public_key]), element[:id]]
    end
    pubkey_array
  end

  def check_for_revocation(key_id)
    # takes a key_id and looks it up in the keystore database table. Returns true if revoked, false if not revoked
    key_hash = @keys_ds.where(:id=>key_id).to_a
    key_hash[0][:revoked]
  end

  private

  def hash_key(public_key)
    Base64.encode64(Digest::SHA256.digest public_key)
  end

  def setup_message_database
    @DB = Sequel.sqlite
    @DB = Sequel.connect($dbpath)
    @DB.create_table? :messages do
      primary_key :id  # wtf
      Datetime :time  # time when the message was received
      String :client  # client (IRC XYZ)
      TrueClass :private  # true if the message is private (for IRC, e.g.)
      String :sender  # client specific sender of the message
      String :message  # message; only to use if it's clear that it's just a string!
      File :attachment  # to save images and complete emails
      Integer :key_id  # references to the id of the key database for public_key
    end
    @messages_ds = @DB[:message]  # dataset creation
    @DB.disconnect
  end

  def setup_client_database
    # the client database holds information about the various information sources (read: clients) that the
    # server should be take into account
    @DB = Sequel.sqlite
    @DB = Sequel.connect($dbpath)
    @DB.create_table? :clients do
      primary_key :id  # id
      String :description  # description of client
      String :host  # contains url / whatever of the host
      String :type  # contains desired type of connection (only 'irc' is supported by now!)
      String :nick  # irc only
      String :realname  # irc only
      String :channel  # maybe someday multiple channels; one for now. irc only
    end
  end

  # sets up the key database table
  def setup_key_database
    @DB = Sequel.sqlite
    @DB = Sequel.connect($dbpath)
    @DB.create_table? :keys do
      primary_key :id  # id
      String :description  # description of the respective key
      String :host  # contains url / whatever of the host
      String :private_key  # only contains a value if it's a local private key, otherwise nil
      String :public_key  # contains public key of respectiv host
      TrueClass :revoked  # false if the key is revoked
    end
  end
end

class EncryptedAdapter
  def initialize
    # no initialization needed so far
  end

  def write_encrypted_message(timestamp, client, private_bool, sender, message, attachment)
    # a drop-in encryption-enabling wrapper for DatabaseBox
    # encrypts every message's sender, message and attachments with every single pubkey in the key database
    database = DatabaseBox.new
    crypto = CryptoBox.new
    database.output_all_keys.each do |public_key|  # todo: not capable of multiple clients; ATM every message is encrypted for every pubkey known to the database
      enc_sender = Base64.encode64(crypto.host_encrypt_string(sender, public_key[0]))
      enc_message = Base64.encode64(crypto.host_encrypt_string(message, public_key[0]))
      enc_attachment = Base64.encode64(crypto.host_encrypt_string(attachment, public_key[0]))
      database.write_message_to_database(timestamp, client, private_bool, enc_sender, enc_message, enc_attachment, public_key[1])
    end
  end

end

##
# The class CryptoBox poses as a generic adapter for cryptographic services. It uses the NaCl library by djb as backend.
# Ruby binding is provided by RbNaCl by Tony Arcieri

class CryptoBox
  def initialize
    database = DatabaseBox.new
    pub, priv = database.output_host_keypair
    if pub == nil and priv == nil
      # apparently, there is no host keypair available, so a new one has to be generated, but not without snitching
      puts 'No host keypair available, have to generate a new one! Possible temper alert!'
      new_pub, new_priv = generate_keypair
      database.register_key('host', '127.0.0.1', Base64.encode64(new_priv), Base64.encode64(new_pub))
    else
      @public_key = pub
      @private_key = priv
    end
  end

  # Generates a keypair in order to receive messages. For testing only.
  def testing_generate_receiving_keypair
    db = DatabaseBox.new
    pub, priv = generate_a_keypair
    db.register_key('example description', 'horst', nil, Base64.encode64(pub))
  end

  # Generates a new NaCl keypair
  # also sets the @public_key and @private_key instance variables
  # Due to the switch to RbNaCl a seperate handling of the messenge's nonce is no longer needed!
  def generate_keypair
    keypair = RbNaCl::PrivateKey.generate
    @private_key = keypair
    @public_key = keypair.public_key
    return @public_key, @private_key
  end

  # Generates a new NaCl keypair, but does not touch instance variables.
  def generate_a_keypair
    keypair = RbNaCl::PrivateKey.generate
    private_key = keypair
    public_key = keypair.public_key
    return public_key, private_key
  end

  # Encrypts a given string with the given pubkey, signed with the host keypair
  def host_encrypt_string(string_to_encrypt, receiver_public_key)
    RbNaCl::SimpleBox.from_keypair(receiver_public_key, @private_key).encrypt(string_to_encrypt)
  end

  # Encrypts a given string with the given receiver public key and signs the message with a given private key
  def encrypt_string(string_to_encrypt, sender_private_key, receiver_public_key)
    RbNaCl::SimpleBox.from_keypair(receiver_public_key, sender_private_key).encrypt(string_to_encrypt)
  end

  # Decrypts a given string with the given receiver private key and
  # checks the signature of the message with a given public key

  def decrypt_string(string_to_decrypt, receiver_private_key, sender_public_key)
    RbNaCl::SimpleBox.from_keypair(sender_public_key, receiver_private_key).decrypt(string_to_decrypt)
  end

end



# db = DatabaseBox.new
# puts db.output_messages_by_id(10,3)