lanet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +437 -0
- data/Rakefile +12 -0
- data/bin/console +11 -0
- data/bin/lanet +7 -0
- data/bin/setup +8 -0
- data/exe/lanet +8 -0
- data/index.html +233 -0
- data/lib/lanet/cli.rb +229 -0
- data/lib/lanet/encryptor.rb +84 -0
- data/lib/lanet/ping.rb +297 -0
- data/lib/lanet/receiver.rb +21 -0
- data/lib/lanet/scanner.rb +293 -0
- data/lib/lanet/sender.rb +21 -0
- data/lib/lanet/version.rb +5 -0
- data/lib/lanet.rb +45 -0
- data/sig/lanet.rbs +4 -0
- metadata +128 -0
data/index.html
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Lanet - Lightweight LAN Communication Tool for Networking</title>
|
7
|
+
<style>
|
8
|
+
body {
|
9
|
+
font-family: 'Arial', sans-serif;
|
10
|
+
line-height: 1.6;
|
11
|
+
color: #333;
|
12
|
+
margin: 0;
|
13
|
+
padding: 0;
|
14
|
+
background-color: #f4f4f4;
|
15
|
+
}
|
16
|
+
.container {
|
17
|
+
max-width: 1200px;
|
18
|
+
margin: 0 auto;
|
19
|
+
padding: 20px;
|
20
|
+
}
|
21
|
+
header {
|
22
|
+
background: linear-gradient(90deg, #007BFF, #0056b3);
|
23
|
+
color: white;
|
24
|
+
padding: 40px 0;
|
25
|
+
text-align: center;
|
26
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
27
|
+
}
|
28
|
+
header h1 {
|
29
|
+
font-size: 3em;
|
30
|
+
margin: 0;
|
31
|
+
}
|
32
|
+
header p {
|
33
|
+
font-size: 1.2em;
|
34
|
+
margin-top: 10px;
|
35
|
+
}
|
36
|
+
.badges {
|
37
|
+
margin-top: 20px;
|
38
|
+
}
|
39
|
+
.badges img {
|
40
|
+
margin: 0 10px;
|
41
|
+
}
|
42
|
+
section {
|
43
|
+
margin: 40px 0;
|
44
|
+
padding: 20px;
|
45
|
+
background-color: white;
|
46
|
+
border-radius: 8px;
|
47
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
48
|
+
}
|
49
|
+
h2 {
|
50
|
+
color: #007BFF;
|
51
|
+
font-size: 2em;
|
52
|
+
margin-bottom: 20px;
|
53
|
+
}
|
54
|
+
h3 {
|
55
|
+
color: #0056b3;
|
56
|
+
font-size: 1.5em;
|
57
|
+
margin-top: 30px;
|
58
|
+
}
|
59
|
+
code {
|
60
|
+
background-color: #f4f4f4;
|
61
|
+
padding: 2px 4px;
|
62
|
+
border-radius: 4px;
|
63
|
+
font-family: 'Courier New', monospace;
|
64
|
+
}
|
65
|
+
pre {
|
66
|
+
background-color: #f4f4f4;
|
67
|
+
padding: 15px;
|
68
|
+
border-radius: 8px;
|
69
|
+
overflow-x: auto;
|
70
|
+
}
|
71
|
+
.feature-list {
|
72
|
+
list-style-type: none;
|
73
|
+
padding: 0;
|
74
|
+
}
|
75
|
+
.feature-list li {
|
76
|
+
margin: 10px 0;
|
77
|
+
font-size: 1.1em;
|
78
|
+
}
|
79
|
+
.feature-list li::before {
|
80
|
+
content: '🚀';
|
81
|
+
margin-right: 10px;
|
82
|
+
}
|
83
|
+
.cli-example, .api-example {
|
84
|
+
margin: 20px 0;
|
85
|
+
}
|
86
|
+
.cli-example h4, .api-example h4 {
|
87
|
+
margin-bottom: 10px;
|
88
|
+
font-size: 1.2em;
|
89
|
+
}
|
90
|
+
footer {
|
91
|
+
text-align: center;
|
92
|
+
padding: 20px;
|
93
|
+
background-color: #007BFF;
|
94
|
+
color: white;
|
95
|
+
margin-top: 40px;
|
96
|
+
}
|
97
|
+
footer a {
|
98
|
+
color: white;
|
99
|
+
text-decoration: none;
|
100
|
+
}
|
101
|
+
</style>
|
102
|
+
</head>
|
103
|
+
<body>
|
104
|
+
<header>
|
105
|
+
<div class="container">
|
106
|
+
<h1>Lanet</h1>
|
107
|
+
<p>A lightweight, powerful LAN communication tool for Networking</p>
|
108
|
+
<div class="badges">
|
109
|
+
<a href="https://badge.fury.io/rb/lanet"><img src="https://badge.fury.io/rb/lanet.svg" alt="Gem Version"></a>
|
110
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
|
111
|
+
</div>
|
112
|
+
</div>
|
113
|
+
</header>
|
114
|
+
|
115
|
+
<div class="container">
|
116
|
+
<section id="about">
|
117
|
+
<h2>About Lanet</h2>
|
118
|
+
<p>Lanet is a lightweight and powerful LAN communication tool for Ruby that enables secure message exchange between devices on the same network. It simplifies peer-to-peer networking with built-in encryption, network discovery, and both targeted and broadcast messaging capabilities.</p>
|
119
|
+
</section>
|
120
|
+
|
121
|
+
<section id="features">
|
122
|
+
<h2>Features</h2>
|
123
|
+
<ul class="feature-list">
|
124
|
+
<li>Simple API - An intuitive Ruby interface for straightforward network communication.</li>
|
125
|
+
<li>Built-in encryption - Optional message encryption with AES-256-GCM for confidentiality.</li>
|
126
|
+
<li>Network scanning - Automatically discover active devices on your local network.</li>
|
127
|
+
<li>Targeted messaging - Send messages to specific IP addresses.</li>
|
128
|
+
<li>Broadcasting - Send messages to all devices on the network.</li>
|
129
|
+
<li>Host pinging - Check host availability and measure response times (with a familiar <code>ping</code> interface).</li>
|
130
|
+
<li>Command-line interface - Perform common network operations directly from your terminal.</li>
|
131
|
+
<li>Extensible - Easily build custom tools and integrations using the Lanet API.</li>
|
132
|
+
<li>Configurable - Adjust port settings, encryption keys, and network scan ranges.</li>
|
133
|
+
</ul>
|
134
|
+
</section>
|
135
|
+
|
136
|
+
<section id="installation">
|
137
|
+
<h2>Installation</h2>
|
138
|
+
<p>Add this line to your application's Gemfile:</p>
|
139
|
+
<pre><code>gem 'lanet'</code></pre>
|
140
|
+
<p>And then execute:</p>
|
141
|
+
<pre><code>bundle install</code></pre>
|
142
|
+
<p>Or install it yourself as:</p>
|
143
|
+
<pre><code>gem install lanet</code></pre>
|
144
|
+
</section>
|
145
|
+
|
146
|
+
<section id="usage">
|
147
|
+
<h2>Usage</h2>
|
148
|
+
<h3>Command Line Interface (CLI)</h3>
|
149
|
+
<div class="cli-example">
|
150
|
+
<h4>Scanning the network</h4>
|
151
|
+
<pre><code>lanet scan --range 192.168.1.0/24</code></pre>
|
152
|
+
<p>With verbose output (shows hostname, MAC address, open ports, and response time):</p>
|
153
|
+
<pre><code>lanet scan --range 192.168.1.0/24 --verbose</code></pre>
|
154
|
+
</div>
|
155
|
+
<div class="cli-example">
|
156
|
+
<h4>Sending a message to a specific target</h4>
|
157
|
+
<pre><code>lanet send --target 192.168.1.5 --message "Hello there!"</code></pre>
|
158
|
+
<p>Sending an encrypted message:</p>
|
159
|
+
<pre><code>lanet send --target 192.168.1.5 --message "Secret message" --key "my_secret_key"</code></pre>
|
160
|
+
</div>
|
161
|
+
<div class="cli-example">
|
162
|
+
<h4>Broadcasting a message to all devices</h4>
|
163
|
+
<pre><code>lanet broadcast --message "Announcement for everyone!"</code></pre>
|
164
|
+
</div>
|
165
|
+
<div class="cli-example">
|
166
|
+
<h4>Pinging a specific host</h4>
|
167
|
+
<pre><code>lanet ping --host 192.168.1.5</code></pre>
|
168
|
+
</div>
|
169
|
+
|
170
|
+
<h3>Ruby API</h3>
|
171
|
+
<div class="api-example">
|
172
|
+
<h4>Scanning the network</h4>
|
173
|
+
<pre><code>require 'lanet'
|
174
|
+
|
175
|
+
scanner = Lanet.scanner
|
176
|
+
active_ips = scanner.scan('192.168.1.0/24')
|
177
|
+
puts "Found devices: #{active_ips.join(', ')}"</code></pre>
|
178
|
+
</div>
|
179
|
+
<div class="api-example">
|
180
|
+
<h4>Sending a message</h4>
|
181
|
+
<pre><code>sender = Lanet.sender
|
182
|
+
sender.send_to('192.168.1.5', 'Hello from Ruby!')</code></pre>
|
183
|
+
<p>Broadcasting a message:</p>
|
184
|
+
<pre><code>sender.broadcast('Announcement to all devices!')</code></pre>
|
185
|
+
</div>
|
186
|
+
<div class="api-example">
|
187
|
+
<h4>Listening for messages</h4>
|
188
|
+
<pre><code>receiver = Lanet.receiver
|
189
|
+
receiver.listen do |data, ip|
|
190
|
+
puts "Received from #{ip}: #{data}"
|
191
|
+
end</code></pre>
|
192
|
+
</div>
|
193
|
+
</section>
|
194
|
+
|
195
|
+
<section id="configuration">
|
196
|
+
<h2>Configuration</h2>
|
197
|
+
<p>Lanet can be configured with several options:</p>
|
198
|
+
<ul>
|
199
|
+
<li><strong>Port</strong>: Default is 5000, but can be changed for both sending and receiving.</li>
|
200
|
+
<li><strong>Encryption Keys</strong>: Use your own encryption keys for secure communication.</li>
|
201
|
+
<li><strong>Custom Ranges</strong>: Scan specific network ranges to discover devices.</li>
|
202
|
+
</ul>
|
203
|
+
</section>
|
204
|
+
|
205
|
+
<section id="development">
|
206
|
+
<h2>Development</h2>
|
207
|
+
<p>After checking out the repo, run <code>bin/setup</code> to install dependencies. Then, run <code>rake spec</code> to run the tests.</p>
|
208
|
+
<p>To install this gem locally, run <code>bundle exec rake install</code>.</p>
|
209
|
+
</section>
|
210
|
+
|
211
|
+
<section id="contributing">
|
212
|
+
<h2>Contributing</h2>
|
213
|
+
<p>Bug reports and pull requests are welcome on GitHub at <a href="https://github.com/davidesantangelo/lanet">https://github.com/davidesantangelo/lanet</a>. Follow these steps:</p>
|
214
|
+
<ol>
|
215
|
+
<li>Fork the repository</li>
|
216
|
+
<li>Create your feature branch (<code>git checkout -b my-new-feature</code>)</li>
|
217
|
+
<li>Commit your changes (<code>git commit -am 'Add some feature'</code>)</li>
|
218
|
+
<li>Push to the branch (<code>git push origin my-new-feature</code>)</li>
|
219
|
+
<li>Create a new Pull Request</li>
|
220
|
+
</ol>
|
221
|
+
</section>
|
222
|
+
|
223
|
+
<section id="license">
|
224
|
+
<h2>License</h2>
|
225
|
+
<p>The gem is available under the <a href="https://opensource.org/licenses/MIT">MIT License</a>.</p>
|
226
|
+
</section>
|
227
|
+
</div>
|
228
|
+
|
229
|
+
<footer>
|
230
|
+
<p>© 2025 Lanet. All rights reserved. | <a href="https://github.com/davidesantangelo/lanet">View on GitHub</a></p>
|
231
|
+
</footer>
|
232
|
+
</body>
|
233
|
+
</html>
|
data/lib/lanet/cli.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "lanet/sender"
|
5
|
+
require "lanet/receiver"
|
6
|
+
require "lanet/scanner"
|
7
|
+
require "lanet/ping"
|
8
|
+
require "lanet/encryptor"
|
9
|
+
|
10
|
+
module Lanet
|
11
|
+
class CLI < Thor
|
12
|
+
# Add this method to silence Thor errors and disable exit on failure
|
13
|
+
def self.exit_on_failure?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "send --target IP --message MSG [--key KEY] [--port PORT]", "Send a message to a specific target"
|
18
|
+
option :target, required: true
|
19
|
+
option :message, required: true
|
20
|
+
option :key
|
21
|
+
option :port, type: :numeric, default: 5000
|
22
|
+
def send
|
23
|
+
sender = Sender.new(options[:port])
|
24
|
+
message = Encryptor.prepare_message(options[:message], options[:key])
|
25
|
+
sender.send_to(options[:target], message)
|
26
|
+
puts "Message sent to #{options[:target]}"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "broadcast --message MSG [--key KEY] [--port PORT]", "Broadcast a message to all devices"
|
30
|
+
option :message, required: true
|
31
|
+
option :key
|
32
|
+
option :port, type: :numeric, default: 5000
|
33
|
+
def broadcast
|
34
|
+
sender = Sender.new(options[:port])
|
35
|
+
message = Encryptor.prepare_message(options[:message], options[:key])
|
36
|
+
sender.broadcast(message)
|
37
|
+
puts "Message broadcasted"
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "scan --range CIDR [--timeout TIMEOUT] [--threads THREADS] [--verbose]",
|
41
|
+
"Scan for active devices in the given range"
|
42
|
+
option :range, required: true
|
43
|
+
option :timeout, type: :numeric, default: 1
|
44
|
+
option :threads, type: :numeric, default: 32
|
45
|
+
option :verbose, type: :boolean, default: false
|
46
|
+
def scan
|
47
|
+
scanner = Scanner.new
|
48
|
+
results = scanner.scan(
|
49
|
+
options[:range],
|
50
|
+
options[:timeout],
|
51
|
+
options[:threads],
|
52
|
+
options[:verbose]
|
53
|
+
)
|
54
|
+
|
55
|
+
puts "\nActive devices:"
|
56
|
+
|
57
|
+
if options[:verbose]
|
58
|
+
results.each do |host|
|
59
|
+
puts "─" * 50
|
60
|
+
puts "IP: #{host[:ip]}"
|
61
|
+
puts "Hostname: #{host[:hostname]}" if host[:hostname]
|
62
|
+
puts "MAC: #{host[:mac]}" if host[:mac] # Add this line to display MAC addresses
|
63
|
+
puts "Response time: #{host[:response_time]}ms" if host[:response_time]
|
64
|
+
puts "Detection method: #{host[:detection_method]}" if host[:detection_method]
|
65
|
+
|
66
|
+
next unless host[:ports] && !host[:ports].empty?
|
67
|
+
|
68
|
+
puts "Open ports:"
|
69
|
+
host[:ports].each do |port, service|
|
70
|
+
puts " - #{port}: #{service}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
puts "─" * 50
|
74
|
+
puts "Found #{results.size} active hosts."
|
75
|
+
else
|
76
|
+
results.each { |ip| puts ip }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "listen [--port PORT] [--key KEY]", "Listen for incoming messages"
|
81
|
+
option :port, type: :numeric, default: 5000
|
82
|
+
option :key
|
83
|
+
def listen
|
84
|
+
receiver = Receiver.new(options[:port])
|
85
|
+
puts "Listening on port #{options[:port]}..."
|
86
|
+
receiver.listen do |data, ip|
|
87
|
+
message = Encryptor.process_message(data, options[:key])
|
88
|
+
puts "From #{ip}: #{message}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc "ping [HOST]", "Ping a host or multiple hosts with real-time output"
|
93
|
+
option :host, type: :string, desc: "Single host to ping"
|
94
|
+
option :hosts, type: :string, desc: "Comma-separated list of hosts to ping"
|
95
|
+
option :timeout, type: :numeric, default: 1, desc: "Ping timeout in seconds"
|
96
|
+
option :count, type: :numeric, default: 5, desc: "Number of ping packets to send"
|
97
|
+
option :quiet, type: :boolean, default: false, desc: "Only display summary"
|
98
|
+
option :continuous, type: :boolean, default: false, desc: "Ping continuously until interrupted"
|
99
|
+
def ping(target_host = nil)
|
100
|
+
# Support both traditional command (lanet ping 192.168.1.1) and option-style (--host)
|
101
|
+
target = target_host || options[:host] || options[:hosts]
|
102
|
+
|
103
|
+
unless target
|
104
|
+
puts "Error: Missing host to ping"
|
105
|
+
puts "Usage: lanet ping HOST"
|
106
|
+
puts " or: lanet ping --host HOST"
|
107
|
+
puts " or: lanet ping --hosts HOST1,HOST2,HOST3"
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
pinger = Lanet::Ping.new(timeout: options[:timeout], count: options[:count])
|
112
|
+
|
113
|
+
if target_host || options[:host]
|
114
|
+
# For a single host, we use real-time output unless quiet is specified
|
115
|
+
host = target_host || options[:host]
|
116
|
+
if options[:quiet]
|
117
|
+
result = pinger.ping_host(host, false, options[:continuous])
|
118
|
+
display_ping_summary(host, result)
|
119
|
+
else
|
120
|
+
pinger.ping_host(host, true, options[:continuous]) # Real-time output with optional continuous mode
|
121
|
+
end
|
122
|
+
else
|
123
|
+
hosts = options[:hosts].split(",").map(&:strip)
|
124
|
+
|
125
|
+
if options[:quiet]
|
126
|
+
results = pinger.ping_hosts(hosts, false, options[:continuous])
|
127
|
+
hosts.each do |host|
|
128
|
+
display_ping_summary(host, results[host])
|
129
|
+
puts "\n" unless host == hosts.last
|
130
|
+
end
|
131
|
+
else
|
132
|
+
# Real-time output for multiple hosts
|
133
|
+
pinger.ping_hosts(hosts, true, options[:continuous])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def display_ping_details(host, result)
|
141
|
+
# Display header like standard ping command
|
142
|
+
puts "PING #{host} (#{host}): 56 data bytes"
|
143
|
+
|
144
|
+
if result[:status]
|
145
|
+
# Display individual ping responses
|
146
|
+
unless options[:quiet]
|
147
|
+
result[:responses].each do |response|
|
148
|
+
puts "64 bytes from #{host}: icmp_seq=#{response[:seq]} ttl=#{response[:ttl]} time=#{response[:time]} ms"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Display summary
|
153
|
+
transmitted = options[:count]
|
154
|
+
received = result[:responses].size
|
155
|
+
loss_pct = ((transmitted - received) / transmitted.to_f * 100).round(1)
|
156
|
+
|
157
|
+
puts "\n--- #{host} ping statistics ---"
|
158
|
+
puts "#{transmitted} packets transmitted, #{received} packets received, #{loss_pct}% packet loss"
|
159
|
+
|
160
|
+
if received.positive?
|
161
|
+
times = result[:responses].map { |r| r[:time] }
|
162
|
+
min = times.min
|
163
|
+
avg = times.sum / times.size
|
164
|
+
max = times.max
|
165
|
+
mdev = Math.sqrt(times.map { |t| (t - avg)**2 }.sum / times.size).round(3)
|
166
|
+
|
167
|
+
puts "round-trip min/avg/max/stddev = #{min}/#{avg.round(3)}/#{max}/#{mdev} ms"
|
168
|
+
end
|
169
|
+
else
|
170
|
+
puts "No response from #{host}"
|
171
|
+
puts result[:output] if result[:output].to_s.strip != ""
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Display only the summary portion
|
176
|
+
def display_ping_summary(host, result)
|
177
|
+
if result[:status]
|
178
|
+
transmitted = options[:count]
|
179
|
+
received = result[:responses].size
|
180
|
+
loss_pct = ((transmitted - received) / transmitted.to_f * 100).round(1)
|
181
|
+
|
182
|
+
puts "--- #{host} ping statistics ---"
|
183
|
+
puts "#{transmitted} packets transmitted, #{received} packets received, #{loss_pct}% packet loss"
|
184
|
+
|
185
|
+
if received.positive?
|
186
|
+
times = result[:responses].map { |r| r[:time] }
|
187
|
+
min = times.min
|
188
|
+
avg = times.sum / times.size
|
189
|
+
max = times.max
|
190
|
+
mdev = Math.sqrt(times.map { |t| (t - avg)**2 }.sum / times.size).round(3)
|
191
|
+
|
192
|
+
puts "round-trip min/avg/max/stddev = #{min}/#{avg.round(3)}/#{max}/#{mdev} ms"
|
193
|
+
end
|
194
|
+
else
|
195
|
+
puts "No response from #{host}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def display_ping_result(host, result)
|
200
|
+
# Keep the old method for backward compatibility
|
201
|
+
puts "Host: #{host}"
|
202
|
+
puts "Status: #{result[:status] ? "reachable" : "unreachable"}"
|
203
|
+
|
204
|
+
if result[:status]
|
205
|
+
puts "Response time: #{result[:response_time]}ms"
|
206
|
+
puts "Packet loss: #{result[:packet_loss]}%"
|
207
|
+
end
|
208
|
+
|
209
|
+
return unless options[:verbose]
|
210
|
+
|
211
|
+
puts "\nOutput:"
|
212
|
+
puts result[:output]
|
213
|
+
end
|
214
|
+
|
215
|
+
# Override method_missing to provide helpful error messages for common mistakes
|
216
|
+
def method_missing(method, *args)
|
217
|
+
if method.to_s == "ping" && args.any?
|
218
|
+
invoke "ping", [], { host: args.first, timeout: options[:timeout], count: options[:count],
|
219
|
+
quiet: options[:quiet], continuous: options[:continuous] }
|
220
|
+
else
|
221
|
+
super
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def respond_to_missing?(method, include_private = false)
|
226
|
+
method.to_s == "ping" || super
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "digest"
|
5
|
+
require "base64"
|
6
|
+
|
7
|
+
module Lanet
|
8
|
+
class Encryptor
|
9
|
+
# Constants
|
10
|
+
CIPHER_ALGORITHM = "AES-256-CBC"
|
11
|
+
ENCRYPTED_PREFIX = "E"
|
12
|
+
PLAINTEXT_PREFIX = "P"
|
13
|
+
IV_SIZE = 16
|
14
|
+
|
15
|
+
# Error class for encryption/decryption failures
|
16
|
+
class Error < StandardError; end
|
17
|
+
|
18
|
+
# Encrypts a message if key is provided, otherwise marks it as plaintext
|
19
|
+
# @param message [String] the message to prepare
|
20
|
+
# @param key [String, nil] encryption key or nil for plaintext
|
21
|
+
# @return [String] prepared message with appropriate prefix
|
22
|
+
def self.prepare_message(message, key)
|
23
|
+
return PLAINTEXT_PREFIX + message.to_s if key.nil? || key.empty?
|
24
|
+
|
25
|
+
begin
|
26
|
+
cipher = OpenSSL::Cipher.new("AES-128-CBC")
|
27
|
+
cipher.encrypt
|
28
|
+
cipher.key = derive_key(key)
|
29
|
+
iv = cipher.random_iv
|
30
|
+
encrypted = cipher.update(message.to_s) + cipher.final
|
31
|
+
encoded = Base64.strict_encode64(iv + encrypted)
|
32
|
+
"#{ENCRYPTED_PREFIX}#{encoded}"
|
33
|
+
rescue StandardError => e
|
34
|
+
raise Error, "Encryption failed: #{e.message}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Processes a message, decrypting if necessary
|
39
|
+
# @param data [String] the data to process
|
40
|
+
# @param key [String, nil] decryption key or nil
|
41
|
+
# @return [String] processed message
|
42
|
+
def self.process_message(data, key)
|
43
|
+
return "[Empty message]" if data.nil? || data.empty?
|
44
|
+
|
45
|
+
prefix = data[0]
|
46
|
+
content = data[1..]
|
47
|
+
|
48
|
+
case prefix
|
49
|
+
when ENCRYPTED_PREFIX
|
50
|
+
if key.nil? || key.strip.empty?
|
51
|
+
"[Encrypted message received, but no key provided]"
|
52
|
+
else
|
53
|
+
begin
|
54
|
+
decode_encrypted_message(content, key)
|
55
|
+
rescue StandardError => e
|
56
|
+
"Decryption failed: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
when PLAINTEXT_PREFIX
|
60
|
+
content
|
61
|
+
else
|
62
|
+
"[Invalid message format]"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.derive_key(key)
|
67
|
+
digest = OpenSSL::Digest.new("SHA256")
|
68
|
+
OpenSSL::PKCS5.pbkdf2_hmac(key, "salt", 1000, 16, digest)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.decode_encrypted_message(content, key)
|
72
|
+
decoded = Base64.strict_decode64(content)
|
73
|
+
iv = decoded[0...16]
|
74
|
+
ciphertext = decoded[16..]
|
75
|
+
|
76
|
+
decipher = OpenSSL::Cipher.new("AES-128-CBC")
|
77
|
+
decipher.decrypt
|
78
|
+
decipher.key = derive_key(key)
|
79
|
+
decipher.iv = iv
|
80
|
+
|
81
|
+
decipher.update(ciphertext) + decipher.final
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|