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.
- checksums.yaml +7 -0
- data/COPYING +661 -0
- data/README.org +84 -0
- data/Rakefile +12 -0
- data/lib/sekishiki/crypto.rb +109 -0
- data/lib/sekishiki/packet.rb +205 -0
- data/lib/sekishiki/utils.rb +59 -0
- data/lib/sekishiki/version.rb +5 -0
- data/lib/sekishiki.rb +12 -0
- data/res/img/packet_format.png +0 -0
- data/res/img/packet_format.svg +4 -0
- data/res/logo/logo.png +0 -0
- data/res/logo/sekishiki.svg +477 -0
- data/sig/crypto.rbs +44 -0
- data/sig/packet.rbs +99 -0
- data/sig/sekishiki.rbs +3 -0
- data/sig/utils.rbs +14 -0
- data/sig/version.rbs +3 -0
- data/why_sekishiki.org +45 -0
- metadata +91 -0
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,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
|
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
|