number_station 0.2.0 → 0.3.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 +4 -4
- data/README.asciidoc +81 -3
- data/Rakefile +58 -4
- data/lib/number_station/GLaDOS_espeak.rb +69 -26
- data/lib/number_station/cli.rb +1441 -138
- data/lib/number_station/config_reader.rb +91 -14
- data/lib/number_station/decrypt_message.rb +75 -31
- data/lib/number_station/encrypt_message.rb +112 -41
- data/lib/number_station/examine_pads.rb +171 -0
- data/lib/number_station/make_onetime_pad.rb +58 -23
- data/lib/number_station/phonetic_conversion.rb +38 -49
- data/lib/number_station/version.rb +1 -1
- data/lib/number_station.rb +36 -6
- data/number_station.gemspec +3 -3
- data/resources/conf.yaml +415 -0
- data/resources/repeat_message.txt +1 -0
- metadata +20 -18
- data/resources/conf.json +0 -14
|
@@ -21,37 +21,72 @@
|
|
|
21
21
|
require "securerandom"
|
|
22
22
|
require "json"
|
|
23
23
|
require "time"
|
|
24
|
+
require "date"
|
|
24
25
|
|
|
25
26
|
module NumberStation
|
|
26
|
-
def self.make_otp(pad_path, length, num_pads)
|
|
27
|
+
def self.make_otp(pad_path, length, num_pads, agent_name = nil)
|
|
27
28
|
path = pad_path || Dir.pwd
|
|
28
|
-
len = length ||
|
|
29
|
-
num = num_pads ||
|
|
29
|
+
len = length || 500
|
|
30
|
+
num = num_pads || 500
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
id = rand(0..99999).to_s.rjust(5, "0")
|
|
34
|
-
file_name = File.join(path, "one_time_pad_#{id}.json")
|
|
35
|
-
NumberStation.log.debug "file_name: #{file_name}"
|
|
32
|
+
# Round length up to nearest multiple of 5
|
|
33
|
+
len = round_up_to_multiple_of_5(len.to_i)
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
# Generate date-based filename with uniqueness component
|
|
36
|
+
date_str = Date.today.strftime("%Y-%m-%d")
|
|
37
|
+
filename_base = if agent_name && !agent_name.empty?
|
|
38
|
+
"#{agent_name}-#{date_str}"
|
|
39
|
+
else
|
|
40
|
+
"one_time_pad-#{date_str}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Ensure uniqueness: if file exists, add a counter
|
|
44
|
+
filename = File.join(path, "#{filename_base}.json")
|
|
45
|
+
counter = 1
|
|
46
|
+
while File.exist?(filename)
|
|
47
|
+
filename = File.join(path, "#{filename_base}-#{counter.to_s.rjust(3, '0')}.json")
|
|
48
|
+
counter += 1
|
|
49
|
+
# Safety check to prevent infinite loop
|
|
50
|
+
raise StandardError, "Too many pad files with same date prefix: #{filename_base}" if counter > 999
|
|
43
51
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
|
|
53
|
+
# Generate a unique pad_id for internal use (using epoch timestamp + random)
|
|
54
|
+
pad_id = generate_pad_id
|
|
55
|
+
|
|
56
|
+
pads = generate_pads(num.to_i, len)
|
|
57
|
+
pad_data = {
|
|
58
|
+
id: pad_id,
|
|
59
|
+
pads: pads
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
File.write(filename, pad_data.to_json)
|
|
63
|
+
NumberStation.log.debug "Created one-time pad: #{filename}"
|
|
64
|
+
filename
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def self.round_up_to_multiple_of_5(number)
|
|
70
|
+
# Round up to nearest multiple of 5
|
|
71
|
+
# Examples: 3 -> 5, 7 -> 10, 12 -> 15, 15 -> 15, 500 -> 500
|
|
72
|
+
((number + 4) / 5) * 5
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.generate_pad_id
|
|
76
|
+
# Generate a unique ID using epoch timestamp and random component
|
|
77
|
+
# This ensures uniqueness even if multiple pads are created in the same second
|
|
78
|
+
"#{Time.now.to_i}-#{rand(1000..9999)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.generate_pads(num_pads, length)
|
|
82
|
+
pads = {}
|
|
83
|
+
0.upto(num_pads - 1) do |i|
|
|
84
|
+
pads[i] = {
|
|
85
|
+
"key" => SecureRandom.hex(length),
|
|
86
|
+
"epoch_date" => nil,
|
|
87
|
+
"consumed" => false
|
|
88
|
+
}
|
|
55
89
|
end
|
|
90
|
+
pads
|
|
56
91
|
end
|
|
57
92
|
end
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
module NumberStation
|
|
23
23
|
|
|
24
|
-
ALPHABET ={
|
|
24
|
+
ALPHABET = {
|
|
25
25
|
'0' => "zero",
|
|
26
26
|
'1' => "one",
|
|
27
27
|
'2' => "two",
|
|
@@ -58,80 +58,69 @@ module NumberStation
|
|
|
58
58
|
'x' => "xray",
|
|
59
59
|
'y' => "yankee",
|
|
60
60
|
'z' => "zulu"
|
|
61
|
-
}
|
|
61
|
+
}.freeze
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return NumberStation::ALPHABET[c] + ' ' || ' '
|
|
67
|
-
rescue Exception => e
|
|
68
|
-
return ' '
|
|
69
|
-
end
|
|
63
|
+
def self.lookup_phonetic(char)
|
|
64
|
+
phonetic = ALPHABET[char.downcase]
|
|
65
|
+
phonetic ? phonetic : nil
|
|
70
66
|
end
|
|
71
67
|
|
|
72
68
|
|
|
73
69
|
def self.espeak_word_template(word)
|
|
74
|
-
|
|
70
|
+
pitch = "#{random_sign}#{rand(0..200)}"
|
|
71
|
+
"<prosody pitch=\"#{pitch}\">#{word}</prosody>"
|
|
75
72
|
end
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return rand(0..1) == 0 ? "-" : "+"
|
|
74
|
+
def self.random_sign
|
|
75
|
+
rand(0..1) == 0 ? "-" : "+"
|
|
80
76
|
end
|
|
81
77
|
|
|
82
|
-
|
|
83
78
|
def self.generate_sentence(message)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
words = message.split(" ")
|
|
80
|
+
prosody_words = words.map { |word| espeak_word_template(word) }.join
|
|
81
|
+
"<speak version=\"1.0\" xmlns=\"\" xmlns:xsi=\"\" xsi:schemaLocation=\"\" xml:lang=\"\"><voice gender=\"female\">#{prosody_words}</voice></speak>"
|
|
87
82
|
end
|
|
88
83
|
|
|
89
|
-
|
|
90
84
|
def self.write_espeak_template_file(filename, sentence)
|
|
91
|
-
|
|
92
|
-
f.write(sentence)
|
|
93
|
-
f.close
|
|
85
|
+
File.write(filename, sentence)
|
|
94
86
|
end
|
|
95
87
|
|
|
96
88
|
|
|
97
89
|
def self.call_espeak(input_file_path, output_file_path)
|
|
98
|
-
|
|
99
|
-
cmd = "espeak -ven+f3 -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
|
|
100
|
-
else
|
|
101
|
-
cmd = "espeak -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
unless NumberStation.command?('espeak') || NumberStation.command?('ffmpeg')
|
|
90
|
+
unless NumberStation.command?('espeak') && NumberStation.command?('ffmpeg')
|
|
105
91
|
NumberStation.log.error "number_station requires the espeak and ffmpeg utilities are installed in order to output an mp3 file."
|
|
106
|
-
|
|
107
|
-
`#{cmd}`
|
|
92
|
+
return
|
|
108
93
|
end
|
|
109
|
-
end
|
|
110
94
|
|
|
95
|
+
# Use GLaDOS voice settings by default
|
|
96
|
+
voice_flag = "-ven+f3"
|
|
97
|
+
cmd = "espeak #{voice_flag} -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
|
|
98
|
+
`#{cmd}`
|
|
99
|
+
end
|
|
111
100
|
|
|
112
101
|
def self.write_mp3(message, output_file_path)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
NumberStation.call_espeak(filename, output_file_path)
|
|
102
|
+
# Use temporary file for espeak template
|
|
103
|
+
template_file = "/tmp/espeak_tmp.xml"
|
|
104
|
+
# Generate GLaDOS-style sentence
|
|
105
|
+
sentence = generate_sentence(message)
|
|
106
|
+
|
|
107
|
+
write_espeak_template_file(template_file, sentence)
|
|
108
|
+
call_espeak(template_file, output_file_path)
|
|
121
109
|
end
|
|
122
110
|
|
|
123
|
-
|
|
124
111
|
def self.to_phonetic(file_name)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
112
|
+
raw_message = File.read(file_name)
|
|
113
|
+
# Remove all whitespace from input
|
|
114
|
+
cleaned_message = raw_message.gsub(/[\s\n\r]/, '')
|
|
115
|
+
|
|
116
|
+
# Process in groups of 5 characters
|
|
117
|
+
groups = cleaned_message.chars.each_slice(5).map do |group|
|
|
118
|
+
# Convert each character in the group to phonetic
|
|
119
|
+
group.map { |char| lookup_phonetic(char) }.compact.join(' ')
|
|
133
120
|
end
|
|
134
|
-
|
|
121
|
+
|
|
122
|
+
# Join groups with double space for readability
|
|
123
|
+
groups.join(' ')
|
|
135
124
|
end
|
|
136
125
|
|
|
137
126
|
|
data/lib/number_station.rb
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
You should have received a copy of the GNU General Public License
|
|
19
19
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
20
|
=end
|
|
21
|
-
require 'pastel'
|
|
22
21
|
require 'json'
|
|
23
22
|
require 'logger'
|
|
24
23
|
require 'number_station/cli'
|
|
@@ -27,11 +26,12 @@ require 'number_station/encrypt_message'
|
|
|
27
26
|
require 'number_station/decrypt_message'
|
|
28
27
|
require 'number_station/make_onetime_pad'
|
|
29
28
|
require 'number_station/phonetic_conversion'
|
|
29
|
+
require 'number_station/examine_pads'
|
|
30
|
+
require 'number_station/GLaDOS_espeak'
|
|
30
31
|
require 'number_station/version'
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
module NumberStation
|
|
34
|
-
|
|
35
35
|
def self.command?(name)
|
|
36
36
|
`which #{name}`
|
|
37
37
|
$?.success?
|
|
@@ -41,16 +41,46 @@ module NumberStation
|
|
|
41
41
|
@log = log
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def self.log
|
|
45
|
-
|
|
44
|
+
def self.log
|
|
45
|
+
@log
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def self.set_data(data)
|
|
49
49
|
@data = data
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def self.data
|
|
53
|
-
|
|
52
|
+
def self.data
|
|
53
|
+
@data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.agent_list
|
|
57
|
+
return [] unless @data && @data["agent_list"]
|
|
58
|
+
|
|
59
|
+
# Handle backward compatibility: if agent_list contains strings, convert them
|
|
60
|
+
agents = @data["agent_list"]
|
|
61
|
+
if agents.is_a?(Array) && agents.first.is_a?(String)
|
|
62
|
+
# Old format: array of strings, convert to hashes
|
|
63
|
+
agents.map do |name|
|
|
64
|
+
{
|
|
65
|
+
"name" => name,
|
|
66
|
+
"location" => nil,
|
|
67
|
+
"handler_codeword" => nil,
|
|
68
|
+
"start_date" => nil,
|
|
69
|
+
"end_date" => nil,
|
|
70
|
+
"active" => false
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
# New format: array of hashes
|
|
75
|
+
agents
|
|
76
|
+
end
|
|
54
77
|
end
|
|
55
78
|
|
|
79
|
+
def self.find_agent_by_name(name)
|
|
80
|
+
agent_list.find { |agent| agent["name"] == name }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.active_agents
|
|
84
|
+
agent_list.select { |agent| agent["active"] == true }
|
|
85
|
+
end
|
|
56
86
|
end
|
data/number_station.gemspec
CHANGED
|
@@ -53,8 +53,8 @@ Gem::Specification.new do |spec|
|
|
|
53
53
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
54
54
|
spec.require_paths = ["lib"]
|
|
55
55
|
|
|
56
|
-
spec.add_dependency "
|
|
57
|
-
spec.add_dependency "thor"
|
|
56
|
+
spec.add_dependency "thor", "~> 1.5"
|
|
58
57
|
|
|
59
|
-
spec.add_development_dependency "
|
|
58
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
59
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
60
60
|
end
|