-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclasses.rb
377 lines (324 loc) · 13.1 KB
/
classes.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
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)