sekishiki 20260821

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.
data/README.org ADDED
@@ -0,0 +1,84 @@
1
+ #+title: README
2
+ #+author: Sidney PEPO
3
+ #+date: [2026-08-21 Fri]
4
+
5
+ #+begin_center
6
+ #+html: <div align="center">
7
+ #+html: <img src="res/logo/logo.png" alt="Sekishiki logo" height="250">
8
+ #+html: </div>
9
+ #+end_center
10
+
11
+ * Sekishiki (Ruby)
12
+ The Ruby implementation of /Sekishiki: The Secure Purpose-Agnostic Network
13
+ Protocol/.
14
+
15
+ * Why "Sekishiki"?
16
+ This question is answered in [[file:why_sekishiki.org][why_sekishiki.org]], but note that it *contains
17
+ spoilers* from Saint Seiya (aka Knights of the Zodiac), so read it *ONLY* if you
18
+ are okay with that!
19
+
20
+ #+begin_quote
21
+ TL;DR: it is a Saint Seiya reference.
22
+ #+end_quote
23
+
24
+ * Installation
25
+ Install the gem and add to the application's Gemfile by executing:
26
+
27
+ #+begin_src sh
28
+ bundle add sekishiki
29
+ #+end_src
30
+
31
+ If bundler is not being used to manage dependencies, install the gem by
32
+ executing:
33
+
34
+ #+begin_src sh
35
+ gem install sekishiki
36
+ #+end_src
37
+
38
+ * Usage
39
+ :PROPERTIES:
40
+ :CUSTOM_ID: usage
41
+ :END:
42
+ TODO: Write (better) usage instructions here. Since I just wanted to publish
43
+ this Gem as soon as possible, currently, the only available way to learn how to
44
+ use Sekishiki is by reading its test files located at the ~spec~ directory. For
45
+ an actual full usage example, check the ~spec/sekishiki_spec.rb~ test.
46
+
47
+ ** Packet format
48
+ The Sekishiki packet format has the following structure:
49
+
50
+ #+begin_center
51
+ #+html: <div align="center">
52
+ #+html: <img src="res/img/packet_format.png" alt="Sekishiki packet format" width="400">
53
+ #+html: </div>
54
+ #+end_center
55
+
56
+ where
57
+
58
+ - The information inside parenthesis are the size of each field in *bytes*;
59
+
60
+ - The header of Sekishiki packets are 76 bytes long;
61
+
62
+ - Green fields should *always* be filled, since they are required for all
63
+ Sekishiki packets;
64
+
65
+ - The yellow field (~OTP code~) might be left untouched. However, since it is
66
+ what provides protection against [[https://en.wikipedia.org/wiki/Replay_attack][replay attacks]], *its usage is highly
67
+ encouraged*. This Sekishiki implementation ships with built-in features to
68
+ fill it with valid data (check the [[#usage][Usage]] section); and
69
+
70
+ - Blue fields often can be filled with zeros (~\x00~), since they are reserved
71
+ for applications which implements Sekishiki. Such fields are handy for
72
+ transporting feature-specific information or application checks;
73
+
74
+ * Development
75
+ After checking out the repo, run ~bin/setup~ to install dependencies. Then, run
76
+ ~rake spec~ to run the tests. You can also run ~bin/console~ for an interactive
77
+ prompt that will allow you to experiment.
78
+
79
+ To install this gem onto your local machine, run ~bundle exec rake install~.
80
+
81
+ * Contributing
82
+ This section is at TODO state, but the tl;dr is: it is a good practice to first
83
+ write and test your code by creating [[https://rspec.info][RSpec tests]] at the ~spec~ directory, but
84
+ this procedure is not mandatory (just do not submit broken code).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbnacl"
4
+
5
+ require "sekishiki/utils"
6
+
7
+ module SEKISHIKI
8
+ # Wrapper for RbNaCl
9
+ class CRYPTO
10
+ attr_reader :box
11
+
12
+ def initialize(private_key = "", peer_public_key = "")
13
+ reset
14
+ self.private_key = private_key
15
+ self.peer_public_key = peer_public_key
16
+ box_create
17
+ end
18
+
19
+ def reset
20
+ @private_key = ""
21
+ @public_key = ""
22
+ @peer_public_key = ""
23
+
24
+ @box = nil
25
+ end
26
+
27
+ def public_key
28
+ SEKISHIKI.bin2hex @public_key
29
+ end
30
+
31
+ def private_key
32
+ SEKISHIKI.bin2hex @private_key
33
+ end
34
+
35
+ def private_key=(private_key)
36
+ return unless public_key.instance_of?(String) ||
37
+ public_key.instance_of?(RbNaCl::PrivateKey)
38
+
39
+ @private_key = if private_key.instance_of?(String) && !private_key.empty?
40
+ RbNaCl::PrivateKey.new(
41
+ SEKISHIKI.to_bin(private_key, 32)
42
+ )
43
+ elsif private_key.instance_of?(RbNaCl::PrivateKey)
44
+ private_key
45
+ end
46
+
47
+ return unless @private_key.instance_of?(RbNaCl::PrivateKey)
48
+
49
+ @public_key = @private_key.public_key.to_bytes
50
+ @private_key = @private_key.to_bytes
51
+ end
52
+
53
+ def peer_public_key
54
+ SEKISHIKI.bin2hex @peer_public_key
55
+ end
56
+
57
+ def peer_public_key=(public_key)
58
+ return unless public_key.instance_of?(String) ||
59
+ public_key.instance_of?(RbNaCl::PublicKey)
60
+
61
+ @peer_public_key = if public_key.instance_of?(String) &&
62
+ !public_key.empty?
63
+ RbNaCl::PublicKey.new(
64
+ SEKISHIKI.to_bin(public_key, 32)
65
+ )
66
+ elsif public_key.instance_of?(RbNaCl::PublicKey)
67
+ public_key
68
+ end
69
+
70
+ return unless @peer_public_key.instance_of?(RbNaCl::PublicKey)
71
+
72
+ @peer_public_key = @peer_public_key.to_bytes
73
+ end
74
+
75
+ def private_key_create
76
+ @private_key = RbNaCl::PrivateKey.generate
77
+
78
+ @public_key = @private_key.public_key.to_bytes
79
+ @private_key = @private_key.to_bytes
80
+ end
81
+
82
+ def box_create
83
+ return unless @peer_public_key.instance_of?(String) ||
84
+ @private_key.instance_of?(String)
85
+
86
+ @box = !(@peer_public_key.empty? || @private_key.empty?) &&
87
+ RbNaCl::SimpleBox.from_keypair(@peer_public_key, @private_key)
88
+ end
89
+
90
+ def encrypt(plaintext)
91
+ self.class.encrypt @box, plaintext
92
+ end
93
+
94
+ def decrypt(ciphertext)
95
+ self.class.decrypt @box, ciphertext
96
+ end
97
+
98
+ # NOTE: avoid using any of the following class methods! Instead, use their
99
+ # equivalent instance methods
100
+
101
+ def self.encrypt(box, plaintext)
102
+ (box.instance_of?(RbNaCl::SimpleBox) && box.encrypt(plaintext)) || ""
103
+ end
104
+
105
+ def self.decrypt(box, ciphertext)
106
+ (box.instance_of?(RbNaCl::SimpleBox) && box.decrypt(ciphertext)) || ""
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rotp"
4
+
5
+ require "sekishiki/version"
6
+ require "sekishiki/utils"
7
+
8
+ module SEKISHIKI
9
+ # Packet implementation
10
+ class PACKET
11
+ attr_accessor :extension, :payload, :padding
12
+
13
+ attr_reader :otp
14
+
15
+ def initialize(**args)
16
+ reset
17
+ parse args[:pkt]
18
+ create args
19
+ end
20
+
21
+ def reset
22
+ self.version = VERSION # 4
23
+ @flags = SEKISHIKI.data_gen 1, false # 1
24
+
25
+ @otp = nil
26
+ @otp_code = SEKISHIKI.data_gen 3, false # 3
27
+
28
+ @dst = SEKISHIKI.data_gen 32, false # 32
29
+ @src = SEKISHIKI.data_gen 32, false # 32
30
+
31
+ @sz_extension = SEKISHIKI.data_gen 2, false # 2
32
+ @sz_payload = SEKISHIKI.data_gen 2, false # 2
33
+
34
+ @extension = "" # @sz_extension
35
+ @payload = "" # @sz_payload
36
+ @padding = "" # random
37
+ end
38
+
39
+ def parse(pkt)
40
+ return unless pkt.instance_of?(String)
41
+
42
+ self.version = pkt[0..3]
43
+ self.flags = pkt[4]
44
+ self.otp_code = pkt[5..7]
45
+
46
+ self.dst = pkt[8..39]
47
+ self.src = pkt[40..71]
48
+
49
+ self.sz_extension = pkt[72..73]
50
+ self.sz_payload = pkt[74..75]
51
+
52
+ @extension = pkt[76..(76 + sz_extension - 1)]
53
+ @payload = pkt[(76 + sz_extension)..(76 + sz_extension + sz_payload - 1)]
54
+ @padding = pkt[(76 + sz_extension + sz_payload)..]
55
+ end
56
+
57
+ def create(args)
58
+ self.version = args[:version]
59
+ self.flags = args[:flags]
60
+ self.otp = args[:otp_secret]
61
+ self.otp_code = args[:otp_code]
62
+
63
+ self.dst = args[:dst]
64
+ self.src = args[:src]
65
+
66
+ @extension = SEKISHIKI.to_bin args[:extension] if
67
+ args[:extension].instance_of?(String)
68
+ self.sz_extension = @extension.length
69
+
70
+ @payload = SEKISHIKI.to_bin args[:payload] if
71
+ args[:payload].instance_of?(String)
72
+ self.sz_payload = @payload.length
73
+
74
+ @padding = SEKISHIKI.to_bin args[:padding] if
75
+ args[:padding].instance_of?(String)
76
+ end
77
+
78
+ def header
79
+ @version + @flags + @otp_code + @dst + @src + @sz_extension + @sz_payload
80
+ end
81
+
82
+ def packet
83
+ header + @extension + @payload + @padding
84
+ end
85
+
86
+ def pkt
87
+ packet
88
+ end
89
+
90
+ def version
91
+ SEKISHIKI.bin2hex @version
92
+ end
93
+
94
+ def version=(version)
95
+ return unless version.instance_of?(String)
96
+
97
+ @version = SEKISHIKI.to_bin version, 4
98
+ end
99
+
100
+ def flags
101
+ SEKISHIKI.bin2int @flags
102
+ end
103
+
104
+ def flags=(flags)
105
+ return unless flags.instance_of?(String) || flags.instance_of?(Integer)
106
+
107
+ @flags = SEKISHIKI.to_bin flags, 1
108
+ end
109
+
110
+ def otp_code
111
+ format "%06d", SEKISHIKI.bin2hex(@otp_code).to_i
112
+ end
113
+
114
+ def otp_code=(code)
115
+ return unless code.instance_of?(String) || code.instance_of?(Integer)
116
+
117
+ @otp_code = SEKISHIKI.to_bin code, 3
118
+ end
119
+
120
+ def otp_now
121
+ return unless @otp.instance_of?(ROTP::TOTP)
122
+
123
+ self.otp_code = @otp.now
124
+ end
125
+
126
+ def otp_secret
127
+ (@otp.instance_of?(ROTP::TOTP) && @otp.secret) || ""
128
+ end
129
+
130
+ def otp=(secret)
131
+ return unless secret.instance_of?(String) ||
132
+ secret.instance_of?(ROTP::TOTP)
133
+
134
+ @otp = if secret.instance_of?(String) && !secret.empty?
135
+ ROTP::TOTP.new(secret)
136
+ elsif secret.instance_of?(ROTP::TOTP)
137
+ secret
138
+ end
139
+ end
140
+
141
+ def otp_verify
142
+ self.class.otp_verify @otp, otp_code
143
+ end
144
+
145
+ def dst
146
+ SEKISHIKI.bin2hex @dst
147
+ end
148
+
149
+ def dst=(key)
150
+ return unless key.instance_of?(String)
151
+
152
+ @dst = SEKISHIKI.to_bin key, 32
153
+ end
154
+
155
+ def src
156
+ SEKISHIKI.bin2hex @src
157
+ end
158
+
159
+ def src=(key)
160
+ return unless key.instance_of?(String)
161
+
162
+ @src = SEKISHIKI.to_bin key, 32
163
+ end
164
+
165
+ def sz_extension
166
+ SEKISHIKI.bin2int @sz_extension
167
+ end
168
+
169
+ def sz_extension=(size)
170
+ return unless size.instance_of?(String) || size.instance_of?(Integer)
171
+
172
+ @sz_extension = SEKISHIKI.to_bin size, 2
173
+ end
174
+
175
+ def sz_payload
176
+ SEKISHIKI.bin2int @sz_payload
177
+ end
178
+
179
+ def sz_payload=(size)
180
+ return unless size.instance_of?(String) || size.instance_of?(Integer)
181
+
182
+ @sz_payload = SEKISHIKI.to_bin size, 2
183
+ end
184
+
185
+ def padding_zero(amount)
186
+ return unless amount.instance_of?(Integer)
187
+
188
+ @padding = SEKISHIKI.data_gen amount, false
189
+ end
190
+
191
+ def padding_random(amount)
192
+ return unless amount.instance_of?(Integer)
193
+
194
+ @padding = SEKISHIKI.data_gen amount, true
195
+ end
196
+
197
+ # NOTE: avoid using any of the following class methods! Instead, use their
198
+ # equivalent instance methods
199
+
200
+ def self.otp_verify(otp, otp_code)
201
+ (otp.instance_of?(ROTP::TOTP) && otp_code.instance_of?(String) &&
202
+ !otp_code.empty? && otp.verify(otp_code)) || nil
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbnacl"
4
+
5
+ # Utils for data conversion and encoding
6
+ module SEKISHIKI
7
+ def data_gen(amount, random)
8
+ return "" unless amount.instance_of?(Integer)
9
+
10
+ return "\x00".b * amount unless random
11
+
12
+ data = String.new
13
+ (1..amount).each do |_|
14
+ data.insert(-1, rand(0xFF).chr)
15
+ end
16
+ data.b
17
+ end
18
+
19
+ def hex2bin(hex)
20
+ prefix = "0" if hex.instance_of?(String) && hex.length.odd?
21
+ RbNaCl::Util.hex2bin(prefix.to_s + hex)
22
+ end
23
+
24
+ def int2bin(int)
25
+ hex2bin(int.to_s(16))
26
+ end
27
+
28
+ def to_bin(data, length = 0)
29
+ return "" unless data.instance_of?(String) || data.instance_of?(Integer)
30
+
31
+ ret = if data.instance_of?(String)
32
+ if data.encoding == Encoding::ASCII_8BIT
33
+ data
34
+ else
35
+ hex2bin data
36
+ end
37
+ elsif data.instance_of?(Integer)
38
+ int2bin data
39
+ end
40
+
41
+ if length > ret.length
42
+ data_gen(length - ret.length, false) + ret
43
+ elsif length.positive? && length < ret.length
44
+ ret[-length..]
45
+ else
46
+ ret
47
+ end
48
+ end
49
+
50
+ def bin2hex(bin)
51
+ RbNaCl::Util.bin2hex(bin)
52
+ end
53
+
54
+ def bin2int(bin)
55
+ bin2hex(bin).to_i 16
56
+ end
57
+
58
+ module_function :data_gen, :hex2bin, :int2bin, :to_bin, :bin2hex, :bin2int
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEKISHIKI
4
+ VERSION = "20260821"
5
+ end
data/lib/sekishiki.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sekishiki/crypto"
4
+ require "sekishiki/packet"
5
+ require "sekishiki/utils"
6
+ require "sekishiki/version"
7
+
8
+ module SEKISHIKI
9
+ class Error < StandardError; end
10
+ # TODO: add wrappers and/or other needed features/methods. This might be
11
+ # handful for a CLI version of Sekishiki
12
+ end
Binary file