lanet 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +39 -21
- data/Gemfile.lock +1 -1
- data/README.md +86 -2
- data/index.html +424 -223
- data/lib/lanet/cli.rb +110 -62
- data/lib/lanet/encryptor.rb +94 -16
- data/lib/lanet/scanner.rb +101 -135
- data/lib/lanet/signer.rb +47 -0
- data/lib/lanet/version.rb +1 -1
- metadata +3 -2
data/lib/lanet/cli.rb
CHANGED
@@ -14,27 +14,55 @@ module Lanet
|
|
14
14
|
false
|
15
15
|
end
|
16
16
|
|
17
|
-
desc "send
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
desc "send", "Send a message to a specific target"
|
18
|
+
method_option :target, type: :string, required: true, desc: "Target IP address"
|
19
|
+
method_option :message, type: :string, required: true, desc: "Message to send"
|
20
|
+
method_option :key, type: :string, desc: "Encryption key (optional)"
|
21
|
+
method_option :private_key_file, type: :string, desc: "Path to private key file for signing (optional)"
|
22
|
+
method_option :port, type: :numeric, default: 5000, desc: "Port number"
|
22
23
|
def send
|
23
|
-
sender = Sender.new(options[:port])
|
24
|
-
|
24
|
+
sender = Lanet::Sender.new(options[:port])
|
25
|
+
|
26
|
+
private_key = nil
|
27
|
+
if options[:private_key_file]
|
28
|
+
begin
|
29
|
+
private_key = File.read(options[:private_key_file])
|
30
|
+
puts "Message will be digitally signed"
|
31
|
+
rescue StandardError => e
|
32
|
+
puts "Error reading private key file: #{e.message}"
|
33
|
+
return
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
message = Lanet::Encryptor.prepare_message(options[:message], options[:key], private_key)
|
38
|
+
|
25
39
|
sender.send_to(options[:target], message)
|
26
40
|
puts "Message sent to #{options[:target]}"
|
27
41
|
end
|
28
42
|
|
29
|
-
desc "broadcast
|
30
|
-
|
31
|
-
|
32
|
-
|
43
|
+
desc "broadcast", "Broadcast a message to all devices on the network"
|
44
|
+
method_option :message, type: :string, required: true, desc: "Message to broadcast"
|
45
|
+
method_option :key, type: :string, desc: "Encryption key (optional)"
|
46
|
+
method_option :private_key_file, type: :string, desc: "Path to private key file for signing (optional)"
|
47
|
+
method_option :port, type: :numeric, default: 5000, desc: "Port number"
|
33
48
|
def broadcast
|
34
|
-
sender = Sender.new(options[:port])
|
35
|
-
|
49
|
+
sender = Lanet::Sender.new(options[:port])
|
50
|
+
|
51
|
+
private_key = nil
|
52
|
+
if options[:private_key_file]
|
53
|
+
begin
|
54
|
+
private_key = File.read(options[:private_key_file])
|
55
|
+
puts "Message will be digitally signed"
|
56
|
+
rescue StandardError => e
|
57
|
+
puts "Error reading private key file: #{e.message}"
|
58
|
+
return
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
message = Lanet::Encryptor.prepare_message(options[:message], options[:key], private_key)
|
63
|
+
|
36
64
|
sender.broadcast(message)
|
37
|
-
puts "Message broadcasted"
|
65
|
+
puts "Message broadcasted to the network"
|
38
66
|
end
|
39
67
|
|
40
68
|
desc "scan --range CIDR [--timeout TIMEOUT] [--threads THREADS] [--verbose]",
|
@@ -77,64 +105,84 @@ module Lanet
|
|
77
105
|
end
|
78
106
|
end
|
79
107
|
|
80
|
-
desc "listen
|
81
|
-
|
82
|
-
|
108
|
+
desc "listen", "Listen for incoming messages"
|
109
|
+
method_option :encryption_key, type: :string, desc: "Encryption key for decrypting messages (optional)"
|
110
|
+
method_option :public_key_file, type: :string, desc: "Path to public key file for signature verification (optional)"
|
111
|
+
method_option :port, type: :numeric, default: 5000, desc: "Port to listen on"
|
83
112
|
def listen
|
84
|
-
receiver = Receiver.new(options[:port])
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
113
|
+
receiver = Lanet::Receiver.new(options[:port])
|
114
|
+
|
115
|
+
public_key = nil
|
116
|
+
if options[:public_key_file]
|
117
|
+
begin
|
118
|
+
public_key = File.read(options[:public_key_file])
|
119
|
+
puts "Digital signature verification enabled"
|
120
|
+
rescue StandardError => e
|
121
|
+
puts "Error reading public key file: #{e.message}"
|
122
|
+
return
|
123
|
+
end
|
89
124
|
end
|
90
|
-
end
|
91
125
|
|
92
|
-
|
93
|
-
|
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
|
126
|
+
puts "Listening for messages on port #{options[:port]}..."
|
127
|
+
puts "Press Ctrl+C to stop"
|
110
128
|
|
111
|
-
|
129
|
+
receiver.listen do |data, sender_ip|
|
130
|
+
result = Lanet::Encryptor.process_message(data, options[:encryption_key], public_key)
|
112
131
|
|
113
|
-
|
114
|
-
|
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)
|
132
|
+
puts "\nMessage from #{sender_ip}:"
|
133
|
+
puts "Content: #{result[:content]}"
|
124
134
|
|
125
|
-
if
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
# Real-time output for multiple hosts
|
133
|
-
pinger.ping_hosts(hosts, true, options[:continuous])
|
135
|
+
if result.key?(:verified)
|
136
|
+
verification_status = if result[:verified]
|
137
|
+
"VERIFIED"
|
138
|
+
else
|
139
|
+
"NOT VERIFIED: #{result[:verification_status]}"
|
140
|
+
end
|
141
|
+
puts "Signature: #{verification_status}"
|
134
142
|
end
|
143
|
+
|
144
|
+
puts "-" * 40
|
135
145
|
end
|
136
146
|
end
|
137
147
|
|
148
|
+
desc "ping", "Ping a host to check connectivity"
|
149
|
+
method_option :host, type: :string, desc: "Host to ping"
|
150
|
+
method_option :hosts, type: :string, desc: "Comma-separated list of hosts to ping"
|
151
|
+
method_option :timeout, type: :numeric, default: 1, desc: "Timeout in seconds"
|
152
|
+
method_option :count, type: :numeric, default: 4, desc: "Number of pings"
|
153
|
+
method_option :continuous, type: :boolean, default: false, desc: "Ping continuously until interrupted"
|
154
|
+
method_option :quiet, type: :boolean, default: false, desc: "Show only summary"
|
155
|
+
def ping(single_host = nil)
|
156
|
+
# This is a placeholder for the ping implementation
|
157
|
+
target_host = single_host || options[:host]
|
158
|
+
puts "Pinging #{target_host || options[:hosts]}..."
|
159
|
+
puts "Ping functionality not implemented yet"
|
160
|
+
end
|
161
|
+
|
162
|
+
desc "keygen", "Generate key pair for digital signatures"
|
163
|
+
method_option :bits, type: :numeric, default: 2048, desc: "Key size in bits"
|
164
|
+
method_option :output, type: :string, default: ".", desc: "Output directory"
|
165
|
+
def keygen
|
166
|
+
key_pair = Lanet::Signer.generate_key_pair(options[:bits])
|
167
|
+
|
168
|
+
private_key_file = File.join(options[:output], "lanet_private.key")
|
169
|
+
public_key_file = File.join(options[:output], "lanet_public.key")
|
170
|
+
|
171
|
+
File.write(private_key_file, key_pair[:private_key])
|
172
|
+
File.write(public_key_file, key_pair[:public_key])
|
173
|
+
|
174
|
+
puts "Key pair generated!"
|
175
|
+
puts "Private key saved to: #{private_key_file}"
|
176
|
+
puts "Public key saved to: #{public_key_file}"
|
177
|
+
puts "\nIMPORTANT: Keep your private key secure and never share it."
|
178
|
+
puts "Share your public key with others who need to verify your messages."
|
179
|
+
end
|
180
|
+
|
181
|
+
desc "version", "Display the version of Lanet"
|
182
|
+
def version
|
183
|
+
puts "Lanet version #{Lanet::VERSION}"
|
184
|
+
end
|
185
|
+
|
138
186
|
private
|
139
187
|
|
140
188
|
def display_ping_details(host, result)
|
data/lib/lanet/encryptor.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "openssl"
|
4
4
|
require "digest"
|
5
5
|
require "base64"
|
6
|
+
require_relative "signer"
|
6
7
|
|
7
8
|
module Lanet
|
8
9
|
class Encryptor
|
@@ -10,16 +11,50 @@ module Lanet
|
|
10
11
|
CIPHER_ALGORITHM = "AES-256-CBC"
|
11
12
|
ENCRYPTED_PREFIX = "E"
|
12
13
|
PLAINTEXT_PREFIX = "P"
|
14
|
+
SIGNED_ENCRYPTED_PREFIX = "SE"
|
15
|
+
SIGNED_PLAINTEXT_PREFIX = "SP"
|
13
16
|
IV_SIZE = 16
|
17
|
+
SIGNATURE_DELIMITER = "||SIG||"
|
18
|
+
MAX_KEY_LENGTH = 64
|
14
19
|
|
15
20
|
# Error class for encryption/decryption failures
|
16
21
|
class Error < StandardError; end
|
17
22
|
|
18
|
-
#
|
23
|
+
# Prepares a message with encryption and/or signing
|
19
24
|
# @param message [String] the message to prepare
|
20
|
-
# @param
|
25
|
+
# @param encryption_key [String, nil] encryption key or nil for plaintext
|
26
|
+
# @param private_key [String, nil] PEM-encoded private key for signing or nil for unsigned
|
21
27
|
# @return [String] prepared message with appropriate prefix
|
22
|
-
def self.prepare_message(message,
|
28
|
+
def self.prepare_message(message, encryption_key, private_key = nil)
|
29
|
+
if private_key.nil? || private_key.empty?
|
30
|
+
prepare_unsigned_message(message, encryption_key)
|
31
|
+
else
|
32
|
+
# Sign the message
|
33
|
+
signature = Signer.sign(message.to_s, private_key)
|
34
|
+
message_with_signature = "#{message}#{SIGNATURE_DELIMITER}#{signature}"
|
35
|
+
|
36
|
+
return "#{SIGNED_PLAINTEXT_PREFIX}#{message_with_signature}" if encryption_key.nil? || encryption_key.empty?
|
37
|
+
|
38
|
+
# Signed but not encrypted
|
39
|
+
|
40
|
+
# Signed and encrypted
|
41
|
+
begin
|
42
|
+
cipher = OpenSSL::Cipher.new("AES-128-CBC")
|
43
|
+
cipher.encrypt
|
44
|
+
cipher.key = derive_key(encryption_key)
|
45
|
+
iv = cipher.random_iv
|
46
|
+
encrypted = cipher.update(message_with_signature) + cipher.final
|
47
|
+
encoded = Base64.strict_encode64(iv + encrypted)
|
48
|
+
"#{SIGNED_ENCRYPTED_PREFIX}#{encoded}"
|
49
|
+
rescue StandardError => e
|
50
|
+
raise Error, "Encryption failed: #{e.message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Original prepare_message renamed
|
57
|
+
def self.prepare_unsigned_message(message, key)
|
23
58
|
return PLAINTEXT_PREFIX + message.to_s if key.nil? || key.empty?
|
24
59
|
|
25
60
|
begin
|
@@ -35,35 +70,78 @@ module Lanet
|
|
35
70
|
end
|
36
71
|
end
|
37
72
|
|
38
|
-
# Processes a message, decrypting if necessary
|
73
|
+
# Processes a message, decrypting and verifying if necessary
|
39
74
|
# @param data [String] the data to process
|
40
|
-
# @param
|
41
|
-
# @
|
42
|
-
|
43
|
-
|
75
|
+
# @param encryption_key [String, nil] decryption key or nil
|
76
|
+
# @param public_key [String, nil] PEM-encoded public key for verification or nil
|
77
|
+
# @return [Hash] processed message with content and verification status
|
78
|
+
def self.process_message(data, encryption_key = nil, public_key = nil)
|
79
|
+
return { content: "[Empty message]", verified: false } if data.nil? || data.empty?
|
44
80
|
|
45
|
-
prefix = data[0]
|
46
|
-
|
81
|
+
prefix = data[0..0] # First character for simple prefixes
|
82
|
+
prefix = data[0..1] if data.length > 1 && %w[SE SP].include?(data[0..1]) # Two characters for complex prefixes
|
83
|
+
content = data[prefix.length..]
|
47
84
|
|
48
85
|
case prefix
|
49
86
|
when ENCRYPTED_PREFIX
|
50
|
-
if
|
51
|
-
"[Encrypted message received, but no key provided]"
|
87
|
+
if encryption_key.nil? || encryption_key.strip.empty?
|
88
|
+
{ content: "[Encrypted message received, but no key provided]", verified: false }
|
52
89
|
else
|
53
90
|
begin
|
54
|
-
decode_encrypted_message(content,
|
91
|
+
decrypted = decode_encrypted_message(content, encryption_key)
|
92
|
+
{ content: decrypted, verified: false }
|
55
93
|
rescue StandardError => e
|
56
|
-
"Decryption failed: #{e.message}"
|
94
|
+
{ content: "Decryption failed: #{e.message}", verified: false }
|
57
95
|
end
|
58
96
|
end
|
59
97
|
when PLAINTEXT_PREFIX
|
60
|
-
content
|
98
|
+
{ content: content, verified: false }
|
99
|
+
when SIGNED_ENCRYPTED_PREFIX
|
100
|
+
if encryption_key.nil? || encryption_key.strip.empty?
|
101
|
+
{ content: "[Signed encrypted message received, but no encryption key provided]", verified: false }
|
102
|
+
else
|
103
|
+
begin
|
104
|
+
decrypted = decode_encrypted_message(content, encryption_key)
|
105
|
+
process_signed_content(decrypted, public_key)
|
106
|
+
rescue StandardError => e
|
107
|
+
{ content: "Processing signed encrypted message failed: #{e.message}", verified: false }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
when SIGNED_PLAINTEXT_PREFIX
|
111
|
+
process_signed_content(content, public_key)
|
61
112
|
else
|
62
|
-
"[Invalid message format]"
|
113
|
+
{ content: "[Invalid message format]", verified: false }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Process content that contains a signature
|
118
|
+
def self.process_signed_content(content, public_key)
|
119
|
+
if content.include?(SIGNATURE_DELIMITER)
|
120
|
+
message, signature = content.split(SIGNATURE_DELIMITER, 2)
|
121
|
+
|
122
|
+
if public_key.nil? || public_key.strip.empty?
|
123
|
+
{ content: message, verified: false, verification_status: "No public key provided for verification" }
|
124
|
+
else
|
125
|
+
begin
|
126
|
+
verified = Signer.verify(message, signature, public_key)
|
127
|
+
{ content: message, verified: verified,
|
128
|
+
verification_status: verified ? "Verified" : "Signature verification failed" }
|
129
|
+
rescue StandardError => e
|
130
|
+
{ content: message, verified: false, verification_status: "Verification error: #{e.message}" }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
else
|
134
|
+
{ content: content, verified: false, verification_status: "No signature found" }
|
63
135
|
end
|
64
136
|
end
|
65
137
|
|
66
138
|
def self.derive_key(key)
|
139
|
+
# Add validation to reject keys that are too long
|
140
|
+
if key && key.length > MAX_KEY_LENGTH
|
141
|
+
raise Error,
|
142
|
+
"Encryption key is too long (maximum #{MAX_KEY_LENGTH} characters)"
|
143
|
+
end
|
144
|
+
|
67
145
|
digest = OpenSSL::Digest.new("SHA256")
|
68
146
|
OpenSSL::PKCS5.pbkdf2_hmac(key, "salt", 1000, 16, digest)
|
69
147
|
end
|