ml_kem 0.1.0 → 0.1.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/lib/ml_kem/cli.rb +147 -0
- data/lib/ml_kem/constants.rb +96 -0
- data/lib/ml_kem/core/k_pke.rb +171 -0
- data/lib/ml_kem/core/ml_kem_internal.rb +84 -0
- data/lib/ml_kem/crypto/hash_functions.rb +74 -0
- data/lib/ml_kem/crypto/symmetric_primitives.rb +67 -0
- data/lib/ml_kem/math/byte_operations.rb +121 -0
- data/lib/ml_kem/math/ntt.rb +123 -0
- data/lib/ml_kem/math/polynomial.rb +80 -0
- data/lib/ml_kem/math/sampling.rb +89 -0
- data/lib/ml_kem/version.rb +1 -1
- metadata +34 -12
- data/Rakefile +0 -8
- data/sig/ml_kem.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 291966b70102237cc2d2c36410dffafb2aef96f16381f0c4f25bb52b54e2666d
|
4
|
+
data.tar.gz: a12f02cda9894cd988451e38a6eebb4bb6cd0b889f1ae261a8fd64fc5cf3882d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b06d18dbf5a67425607e6cfadb21681d19ed0cee9efa8ad516e67874487fec7fd4ed93e079bbf923457824e309d4af8da9396019e8473c257210032c0b944d9
|
7
|
+
data.tar.gz: b683231323c1a52032acf6d084cf2e5ce2b1b508109b58e01ee340139091f16ba73915b7f2bbfc01a786922c7c65e32c7085ff9122b87426609467d577c1a259
|
data/lib/ml_kem/cli.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'ml_kem'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module MLKEM
|
8
|
+
# Command Line Interface (CLI) for ML-KEM key encapsulation mechanism.
|
9
|
+
# Supports key generation, encapsulation, and decapsulation via Thor commands.
|
10
|
+
#
|
11
|
+
# @example Generate keys
|
12
|
+
# $ mlkem keygen -p pub.pem -s priv.pem
|
13
|
+
#
|
14
|
+
# @example Encapsulate
|
15
|
+
# $ mlkem encaps -p pub.pem -c ct.txt -k secret.key
|
16
|
+
#
|
17
|
+
# @example Decapsulate
|
18
|
+
# $ mlkem decaps -s priv.pem -c ct.txt -k secret.key
|
19
|
+
#
|
20
|
+
# @since 0.1.0
|
21
|
+
class CLI < Thor
|
22
|
+
# Ensures the CLI exits with a non-zero status code on failure
|
23
|
+
def self.exit_on_failure?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Global option for selecting the ML-KEM variant
|
28
|
+
class_option :variant,
|
29
|
+
aliases: '-v',
|
30
|
+
type: :string,
|
31
|
+
default: 'ml_kem_768',
|
32
|
+
desc: 'Select the ML-KEM variant (ml_kem_512, ml_kem_768, ml_kem_1024)'
|
33
|
+
|
34
|
+
desc "keygen", "Generate a key pair"
|
35
|
+
option :pk, aliases: "-p", type: :string, default: "public_key.pem", desc: "Output file for the public key"
|
36
|
+
option :sk, aliases: "-s", type: :string, default: "private_key.pem", desc: "Output file for the private key"
|
37
|
+
#
|
38
|
+
# Generates a public/private key pair and stores them in PEM format.
|
39
|
+
#
|
40
|
+
# @option options [String] :pk Output path for the public key.
|
41
|
+
# @option options [String] :sk Output path for the private key.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# $ mlkem keygen -p pub.pem -s priv.pem
|
45
|
+
def keygen
|
46
|
+
mlkem = create_mlkem_instance
|
47
|
+
public_key, private_key = mlkem.keygen
|
48
|
+
|
49
|
+
File.write(options[:pk], encode_pem(public_key, "PUBLIC KEY"))
|
50
|
+
File.write(options[:sk], encode_pem(private_key, "PRIVATE KEY"))
|
51
|
+
|
52
|
+
puts "Keys generated:\n Public Key: #{options[:pk]}\n Private Key: #{options[:sk]}"
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "encaps", "Encapsulate a secret using the public key"
|
56
|
+
option :pk, aliases: "-p", type: :string, required: true, desc: "Input file containing the public key (for encapsulation)"
|
57
|
+
option :sharedkey, aliases: "-k", type: :string, default: "shared_secret.key", desc: "Output file for the shared secret"
|
58
|
+
option :ciphertext, aliases: "-c", type: :string, default: "ciphertext.txt", desc: "Output file for the ciphertext"
|
59
|
+
#
|
60
|
+
# Encapsulates a shared secret using the given public key.
|
61
|
+
#
|
62
|
+
# @option options [String] :pk Path to PEM-encoded public key file.
|
63
|
+
# @option options [String] :sharedkey Output path for the base64-encoded shared secret.
|
64
|
+
# @option options [String] :ciphertext Output path for the base64-encoded ciphertext.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# $ mlkem encaps -p pub.pem -c ct.txt -k secret.key
|
68
|
+
def encaps
|
69
|
+
mlkem = create_mlkem_instance
|
70
|
+
public_key_pem = File.read(options[:pk])
|
71
|
+
public_key = decode_pem(public_key_pem)
|
72
|
+
|
73
|
+
secret, ciphertext = mlkem.encaps(public_key)
|
74
|
+
|
75
|
+
File.write(options[:ciphertext], Base64.strict_encode64(ciphertext))
|
76
|
+
File.write(options[:sharedkey], Base64.strict_encode64(secret))
|
77
|
+
|
78
|
+
puts "Encapsulation complete:\n Ciphertext: #{options[:ciphertext]}\n Shared Secret: #{options[:sharedkey]}"
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "decaps", "Decapsulate a ciphertext using the private key"
|
82
|
+
option :sk, aliases: "-s", type: :string, required: true, desc: "Input file containing the private key (for decapsulation)"
|
83
|
+
option :ciphertext, aliases: "-c", type: :string, required: true, desc: "Ciphertext file to decapsulate"
|
84
|
+
option :sharedkey, aliases: "-k", type: :string, default: "shared_secret.key", desc: "Output file for the shared secret"
|
85
|
+
#
|
86
|
+
# Decapsulates a ciphertext to recover the shared secret using the private key.
|
87
|
+
#
|
88
|
+
# @option options [String] :sk Path to PEM-encoded private key file.
|
89
|
+
# @option options [String] :ciphertext Path to base64-encoded ciphertext file.
|
90
|
+
# @option options [String] :sharedkey Output path for the base64-encoded shared secret.
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# $ mlkem decaps -s priv.pem -c ct.txt -k secret.key
|
94
|
+
def decaps
|
95
|
+
mlkem = create_mlkem_instance
|
96
|
+
private_key_pem = File.read(options[:sk])
|
97
|
+
private_key = decode_pem(private_key_pem)
|
98
|
+
|
99
|
+
ciphertext_base64 = File.read(options[:ciphertext])
|
100
|
+
ciphertext = Base64.decode64(ciphertext_base64)
|
101
|
+
|
102
|
+
secret = mlkem.decaps(private_key, ciphertext)
|
103
|
+
|
104
|
+
File.write(options[:sharedkey], Base64.strict_encode64(secret))
|
105
|
+
|
106
|
+
puts "Decapsulation complete. Shared secret written to #{options[:sharedkey]}"
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Initializes the ML-KEM instance using the provided variant.
|
112
|
+
#
|
113
|
+
# @return [MLKEM::MLKEM] Instance for encryption operations.
|
114
|
+
# @raise [ArgumentError] if the variant is not recognized.
|
115
|
+
def create_mlkem_instance
|
116
|
+
variant = options[:variant].to_sym
|
117
|
+
valid_variants = [:ml_kem_512, :ml_kem_768, :ml_kem_1024]
|
118
|
+
|
119
|
+
unless valid_variants.include?(variant)
|
120
|
+
raise ArgumentError, "Invalid variant: #{options[:variant]}. Valid options are: #{valid_variants.join(', ')}"
|
121
|
+
end
|
122
|
+
|
123
|
+
::MLKEM::MLKEM.new(variant: variant)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Decodes a PEM string into binary data.
|
127
|
+
#
|
128
|
+
# @param [String] pem_str The PEM-encoded key string.
|
129
|
+
# @return [String] Raw binary key data.
|
130
|
+
def decode_pem(pem_str)
|
131
|
+
base64_body = pem_str.lines.reject { |line|
|
132
|
+
line =~ /-----BEGIN|-----END/ || line.strip.empty?
|
133
|
+
}.join
|
134
|
+
Base64.decode64(base64_body)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Encodes binary data into PEM format.
|
138
|
+
#
|
139
|
+
# @param [String] bin_str The binary data.
|
140
|
+
# @param [String] label The PEM label (e.g., "PUBLIC KEY").
|
141
|
+
# @return [String] PEM-formatted string.
|
142
|
+
def encode_pem(bin_str, label)
|
143
|
+
encoded = Base64.strict_encode64(bin_str).scan(/.{1,64}/).join("\n")
|
144
|
+
"-----BEGIN #{label}-----\n#{encoded}\n-----END #{label}-----\n"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module MLKEM
|
5
|
+
# MLKEM::Constants module holds all static cryptographic parameters and tables
|
6
|
+
# used throughout the ML-KEM implementation.
|
7
|
+
#
|
8
|
+
# These include:
|
9
|
+
# - Approved parameter sets for ML-KEM (Table 2 of the specification)
|
10
|
+
# - Modulus (Q)
|
11
|
+
# - Polynomial degree (N)
|
12
|
+
# - Precomputed values for the Number-Theoretic Transform (NTT)
|
13
|
+
#
|
14
|
+
# @see https://csrc.nist.gov/pubs/fips/203/final NIST FIPS 203 - ML-KEM Specification
|
15
|
+
#
|
16
|
+
# @since 0.1.0
|
17
|
+
module Constants
|
18
|
+
##
|
19
|
+
# Approved parameter sets for ML-KEM.
|
20
|
+
# These are tuples of the form [k, eta1, eta2, du, dv] where:
|
21
|
+
# - k : number of polynomials in the public key
|
22
|
+
# - eta1 : noise sampling parameter for secret key
|
23
|
+
# - eta2 : noise sampling parameter for encryption
|
24
|
+
# - du : number of bits revealed in the ciphertext part u
|
25
|
+
# - dv : number of bits revealed in the ciphertext part v
|
26
|
+
#
|
27
|
+
# @return [Hash{String => Array<Integer>}]
|
28
|
+
PARAM_SETS = {
|
29
|
+
'ML-KEM-512' => [2, 3, 2, 10, 4],
|
30
|
+
'ML-KEM-768' => [3, 2, 2, 10, 4],
|
31
|
+
'ML-KEM-1024' => [4, 2, 2, 11, 5]
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
##
|
35
|
+
# Modulus used for all arithmetic operations in the ring Z_q.
|
36
|
+
#
|
37
|
+
# @return [Integer]
|
38
|
+
Q = 3329
|
39
|
+
|
40
|
+
##
|
41
|
+
# Degree of the polynomials used in ML-KEM, i.e., number of coefficients.
|
42
|
+
#
|
43
|
+
# @return [Integer]
|
44
|
+
N = 256
|
45
|
+
|
46
|
+
##
|
47
|
+
# Precomputed roots of unity for the Number-Theoretic Transform (NTT),
|
48
|
+
# as required by Appendix A of the ML-KEM specification.
|
49
|
+
#
|
50
|
+
# These are used for forward NTT operations.
|
51
|
+
#
|
52
|
+
# @return [Array<Integer>]
|
53
|
+
ZETA_NTT = [
|
54
|
+
1, 1729, 2580, 3289, 2642, 630, 1897, 848,
|
55
|
+
1062, 1919, 193, 797, 2786, 3260, 569, 1746,
|
56
|
+
296, 2447, 1339, 1476, 3046, 56, 2240, 1333,
|
57
|
+
1426, 2094, 535, 2882, 2393, 2879, 1974, 821,
|
58
|
+
289, 331, 3253, 1756, 1197, 2304, 2277, 2055,
|
59
|
+
650, 1977, 2513, 632, 2865, 33, 1320, 1915,
|
60
|
+
2319, 1435, 807, 452, 1438, 2868, 1534, 2402,
|
61
|
+
2647, 2617, 1481, 648, 2474, 3110, 1227, 910,
|
62
|
+
17, 2761, 583, 2649, 1637, 723, 2288, 1100,
|
63
|
+
1409, 2662, 3281, 233, 756, 2156, 3015, 3050,
|
64
|
+
1703, 1651, 2789, 1789, 1847, 952, 1461, 2687,
|
65
|
+
939, 2308, 2437, 2388, 733, 2337, 268, 641,
|
66
|
+
1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
|
67
|
+
1063, 319, 2773, 757, 2099, 561, 2466, 2594,
|
68
|
+
2804, 1092, 403, 1026, 1143, 2150, 2775, 886,
|
69
|
+
1722, 1212, 1874, 1029, 2110, 2935, 885, 2154
|
70
|
+
].freeze
|
71
|
+
|
72
|
+
##
|
73
|
+
# Precomputed values used in point-wise multiplication during the NTT.
|
74
|
+
# These include both positive and negative roots of unity.
|
75
|
+
#
|
76
|
+
# @return [Array<Integer>]
|
77
|
+
ZETA_MUL = [
|
78
|
+
17, -17, 2761, -2761, 583, -583, 2649, -2649,
|
79
|
+
1637, -1637, 723, -723, 2288, -2288, 1100, -1100,
|
80
|
+
1409, -1409, 2662, -2662, 3281, -3281, 233, -233,
|
81
|
+
756, -756, 2156, -2156, 3015, -3015, 3050, -3050,
|
82
|
+
1703, -1703, 1651, -1651, 2789, -2789, 1789, -1789,
|
83
|
+
1847, -1847, 952, -952, 1461, -1461, 2687, -2687,
|
84
|
+
939, -939, 2308, -2308, 2437, -2437, 2388, -2388,
|
85
|
+
733, -733, 2337, -2337, 268, -268, 641, -641,
|
86
|
+
1584, -1584, 2298, -2298, 2037, -2037, 3220, -3220,
|
87
|
+
375, -375, 2549, -2549, 2090, -2090, 1645, -1645,
|
88
|
+
1063, -1063, 319, -319, 2773, -2773, 757, -757,
|
89
|
+
2099, -2099, 561, -561, 2466, -2466, 2594, -2594,
|
90
|
+
2804, -2804, 1092, -1092, 403, -403, 1026, -1026,
|
91
|
+
1143, -1143, 2150, -2150, 2775, -2775, 886, -886,
|
92
|
+
1722, -1722, 1212, -1212, 1874, -1874, 1029, -1029,
|
93
|
+
2110, -2110, 2935, -2935, 885, -885, 2154, -2154
|
94
|
+
].freeze
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MLKEM
|
4
|
+
# Core module containing the Public Key Encryption (K-PKE) implementation and the ML-KEM internal logic in two separate classes.
|
5
|
+
#
|
6
|
+
# @since 0.1.0
|
7
|
+
module Core
|
8
|
+
# Implements the Public Key Encryption (K-PKE) as specified in ML-KEM (Kyber).
|
9
|
+
# Provides methods for key generation, encryption, and decryption using lattice-based NTT arithmetic.
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
class KPKE
|
13
|
+
# Initializes the KPKE engine with given parameters.
|
14
|
+
#
|
15
|
+
# @param [Integer] k Security level (2, 3, or 4 for ML-KEM-512/768/1024)
|
16
|
+
# @param [Integer] eta1 Noise parameter for secret and error
|
17
|
+
# @param [Integer] eta2 Noise parameter for encryption errors
|
18
|
+
# @param [Integer] du Compression bits for u
|
19
|
+
# @param [Integer] dv Compression bits for v
|
20
|
+
# @param [Integer] q Prime modulus
|
21
|
+
def initialize(k, eta1, eta2, du, dv, q = Constants::Q)
|
22
|
+
@k = k
|
23
|
+
@eta1 = eta1
|
24
|
+
@eta2 = eta2
|
25
|
+
@du = du
|
26
|
+
@dv = dv
|
27
|
+
@q = q
|
28
|
+
|
29
|
+
@poly_ops = Math::Polynomial.new(@q)
|
30
|
+
@ntt_ops = Math::NTT.new(@q)
|
31
|
+
@sampling = Math::Sampling.new(@q)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Key generation algorithm (Algorithm 13).
|
35
|
+
#
|
36
|
+
# @param [String] d Random seed (32 bytes)
|
37
|
+
# @return [Array<String>] [public_key, secret_key] both as binary strings
|
38
|
+
def keygen(d)
|
39
|
+
rho, sig = Crypto::SymmetricPrimitives.g(d + [@k].pack('C'))
|
40
|
+
n = 0
|
41
|
+
|
42
|
+
a = Array.new(@k) { Array.new(@k) }
|
43
|
+
@k.times do |i|
|
44
|
+
@k.times do |j|
|
45
|
+
a[i][j] = @sampling.sample_ntt(rho + [j, i].pack('CC'))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
s = Array.new(@k)
|
50
|
+
e = Array.new(@k)
|
51
|
+
@k.times do |i|
|
52
|
+
s[i] = @sampling.sample_poly_cbd(@eta1, Crypto::SymmetricPrimitives.prf(@eta1, sig, n))
|
53
|
+
n += 1
|
54
|
+
e[i] = @sampling.sample_poly_cbd(@eta1, Crypto::SymmetricPrimitives.prf(@eta1, sig, n))
|
55
|
+
n += 1
|
56
|
+
end
|
57
|
+
|
58
|
+
s.map! { |v| @ntt_ops.ntt(v) }
|
59
|
+
e.map! { |v| @ntt_ops.ntt(v) }
|
60
|
+
|
61
|
+
t = e.dup
|
62
|
+
@k.times do |i|
|
63
|
+
@k.times do |j|
|
64
|
+
t[i] = @poly_ops.add(t[i], @ntt_ops.multiply_ntts(a[i][j], s[j]))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ek_pke = Math::ByteOperations.byte_encode(12, t, @q) + rho
|
69
|
+
dk_pke = Math::ByteOperations.byte_encode(12, s, @q)
|
70
|
+
|
71
|
+
[ek_pke, dk_pke]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Encryption algorithm (Algorithm 14).
|
75
|
+
#
|
76
|
+
# @param [String] ek_pke Public key
|
77
|
+
# @param [String] m Message (32 bytes of encoded bits)
|
78
|
+
# @param [String] r Randomness (32-byte seed)
|
79
|
+
# @return [String] Ciphertext
|
80
|
+
def encrypt(ek_pke, m, r)
|
81
|
+
n = 0
|
82
|
+
|
83
|
+
t = Array.new(@k)
|
84
|
+
@k.times do |i|
|
85
|
+
t[i] = Math::ByteOperations.byte_decode(12, ek_pke[(384 * i)...(384 * (i + 1))], @q)
|
86
|
+
end
|
87
|
+
rho = ek_pke[(384 * @k)...(384 * @k + 32)]
|
88
|
+
|
89
|
+
a = Array.new(@k) { Array.new(@k) }
|
90
|
+
@k.times do |i|
|
91
|
+
@k.times do |j|
|
92
|
+
a[i][j] = @sampling.sample_ntt(rho + [j, i].pack('CC'))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
y = Array.new(@k)
|
97
|
+
e1 = Array.new(@k)
|
98
|
+
@k.times do |i|
|
99
|
+
y[i] = @sampling.sample_poly_cbd(@eta1, Crypto::SymmetricPrimitives.prf(@eta1, r, n))
|
100
|
+
n += 1
|
101
|
+
e1[i] = @sampling.sample_poly_cbd(@eta2, Crypto::SymmetricPrimitives.prf(@eta2, r, n))
|
102
|
+
n += 1
|
103
|
+
end
|
104
|
+
e2 = @sampling.sample_poly_cbd(@eta2, Crypto::SymmetricPrimitives.prf(@eta2, r, n))
|
105
|
+
|
106
|
+
y.map! { |v| @ntt_ops.ntt(v) }
|
107
|
+
|
108
|
+
u = Array.new(@k) { Array.new(256, 0) }
|
109
|
+
@k.times do |i|
|
110
|
+
@k.times do |j|
|
111
|
+
u[i] = @poly_ops.add(u[i], @ntt_ops.multiply_ntts(a[j][i], y[j]))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
@k.times do |i|
|
116
|
+
u[i] = @ntt_ops.inverse_ntt(u[i])
|
117
|
+
u[i] = @poly_ops.add(u[i], e1[i])
|
118
|
+
end
|
119
|
+
|
120
|
+
mu = @poly_ops.decompress(1, Math::ByteOperations.byte_decode(1, m, @q))
|
121
|
+
|
122
|
+
v = Array.new(256, 0)
|
123
|
+
@k.times do |i|
|
124
|
+
v = @poly_ops.add(v, @ntt_ops.multiply_ntts(t[i], y[i]))
|
125
|
+
end
|
126
|
+
v = @ntt_ops.inverse_ntt(v)
|
127
|
+
v = @poly_ops.add(v, e2)
|
128
|
+
v = @poly_ops.add(v, mu)
|
129
|
+
|
130
|
+
c1 = ''
|
131
|
+
@k.times do |i|
|
132
|
+
c1 += Math::ByteOperations.byte_encode(@du, @poly_ops.compress(@du, u[i]), @q)
|
133
|
+
end
|
134
|
+
c2 = Math::ByteOperations.byte_encode(@dv, @poly_ops.compress(@dv, v), @q)
|
135
|
+
|
136
|
+
c1 + c2
|
137
|
+
end
|
138
|
+
|
139
|
+
# Decryption algorithm (Algorithm 15).
|
140
|
+
#
|
141
|
+
# @param [String] dk_pke Secret key
|
142
|
+
# @param [String] c Ciphertext
|
143
|
+
# @return [String] Recovered message (32-byte encoded bitstring)
|
144
|
+
def decrypt(dk_pke, c)
|
145
|
+
c1 = c[0...(32 * @du * @k)]
|
146
|
+
c2 = c[(32 * @du * @k)...(32 * (@du * @k + @dv))]
|
147
|
+
|
148
|
+
up = Array.new(@k)
|
149
|
+
@k.times do |i|
|
150
|
+
up[i] = @poly_ops.decompress(@du,
|
151
|
+
Math::ByteOperations.byte_decode(@du, c1[(32 * @du * i)...(32 * @du * (i + 1))], @q))
|
152
|
+
end
|
153
|
+
|
154
|
+
vp = @poly_ops.decompress(@dv, Math::ByteOperations.byte_decode(@dv, c2, @q))
|
155
|
+
|
156
|
+
s = Array.new(@k)
|
157
|
+
@k.times do |i|
|
158
|
+
s[i] = Math::ByteOperations.byte_decode(12, dk_pke[(384 * i)...(384 * (i + 1))], @q)
|
159
|
+
end
|
160
|
+
|
161
|
+
w = Array.new(256, 0)
|
162
|
+
@k.times do |i|
|
163
|
+
w = @poly_ops.add(w, @ntt_ops.multiply_ntts(s[i], @ntt_ops.ntt(up[i])))
|
164
|
+
end
|
165
|
+
w = @poly_ops.subtract(vp, @ntt_ops.inverse_ntt(w))
|
166
|
+
|
167
|
+
Math::ByteOperations.byte_encode(1, @poly_ops.compress(1, w), @q)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MLKEM
|
4
|
+
module Core
|
5
|
+
# Internal implementation of ML-KEM (Kyber) encapsulation and decapsulation algorithms.
|
6
|
+
# Wraps K-PKE logic and adds hashing and key derivation per ML-KEM specification.
|
7
|
+
#
|
8
|
+
# Algorithms implemented:
|
9
|
+
# - Algorithm 16: ML-KEM.KeyGen_internal
|
10
|
+
# - Algorithm 17: ML-KEM.Encaps_internal
|
11
|
+
# - Algorithm 18: ML-KEM.Decaps_internal
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
class MLKEMInternal
|
15
|
+
# Constructs the internal ML-KEM engine.
|
16
|
+
#
|
17
|
+
# @param [Integer] k Security level (1, 3, or 5)
|
18
|
+
# @param [Integer] eta1 Noise parameter η₁
|
19
|
+
# @param [Integer] eta2 Noise parameter η₂
|
20
|
+
# @param [Integer] du Compression bits for u
|
21
|
+
# @param [Integer] dv Compression bits for v
|
22
|
+
def initialize(k, eta1, eta2, du, dv)
|
23
|
+
@k = k
|
24
|
+
@eta1 = eta1
|
25
|
+
@eta2 = eta2
|
26
|
+
@du = du
|
27
|
+
@dv = dv
|
28
|
+
@q = Constants::Q
|
29
|
+
@kpke = KPKE.new(@k, @eta1, @eta2, @du, @dv, @q)
|
30
|
+
end
|
31
|
+
|
32
|
+
# ML-KEM Key Generation (Algorithm 16).
|
33
|
+
#
|
34
|
+
# @param [String] d A 32-byte seed for public key derivation.
|
35
|
+
# @param [String] z A 32-byte random secret value.
|
36
|
+
# @return [Array<String>] [ek, dk] Public and private key pair.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# ek, dk = kem.keygen_internal(seed, z)
|
40
|
+
def keygen_internal(d, z)
|
41
|
+
ek_pke, dk_pke = @kpke.keygen(d)
|
42
|
+
ek = ek_pke
|
43
|
+
dk = dk_pke + ek + Crypto::SymmetricPrimitives.h(ek) + z
|
44
|
+
[ek, dk]
|
45
|
+
end
|
46
|
+
|
47
|
+
# ML-KEM Encapsulation (Algorithm 17).
|
48
|
+
#
|
49
|
+
# @param [String] ek Public key.
|
50
|
+
# @param [String] m A 32-byte message (uniformly random).
|
51
|
+
# @return [Array<String>] [k, c] Shared secret and ciphertext.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# k, c = kem.encaps_internal(ek, m)
|
55
|
+
def encaps_internal(ek, m)
|
56
|
+
k, r = Crypto::SymmetricPrimitives.g(m + Crypto::SymmetricPrimitives.h(ek))
|
57
|
+
c = @kpke.encrypt(ek, m, r)
|
58
|
+
[k, c]
|
59
|
+
end
|
60
|
+
|
61
|
+
# ML-KEM Decapsulation (Algorithm 18).
|
62
|
+
#
|
63
|
+
# @param [String] dk Private key.
|
64
|
+
# @param [String] c Ciphertext.
|
65
|
+
# @return [String] Shared secret (32 bytes).
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# k = kem.decaps_internal(dk, c)
|
69
|
+
def decaps_internal(dk, c)
|
70
|
+
dk_pke = dk[0...(384 * @k)]
|
71
|
+
ek_pke = dk[(384 * @k)...(768 * @k + 32)]
|
72
|
+
h_val = dk[(768 * @k + 32)...(768 * @k + 64)]
|
73
|
+
z = dk[(768 * @k + 64)...(768 * @k + 96)]
|
74
|
+
|
75
|
+
mp = @kpke.decrypt(dk_pke, c)
|
76
|
+
kp, rp = Crypto::SymmetricPrimitives.g(mp + h_val)
|
77
|
+
kk = Crypto::SymmetricPrimitives.j(z + c)
|
78
|
+
cp = @kpke.encrypt(ek_pke, mp, rp)
|
79
|
+
|
80
|
+
c == cp ? kp : kk
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sha3'
|
4
|
+
|
5
|
+
module MLKEM
|
6
|
+
# Module containing an adjusted version of the Kyber hash functions
|
7
|
+
# used for cryptographic operations in ML-KEM.
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
module Crypto
|
11
|
+
# Provides cryptographic hash functions used in ML-KEM (Kyber),
|
12
|
+
# including SHAKE128, SHAKE256, SHA3-256, and SHA3-512.
|
13
|
+
#
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
class HashFunctions
|
17
|
+
class << self
|
18
|
+
# Computes the SHAKE128 extendable-output function (XOF).
|
19
|
+
#
|
20
|
+
# @param [String] data Input byte string.
|
21
|
+
# @param [Integer] length Number of output bytes to produce.
|
22
|
+
# @return [String] XOF output of the specified length.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# out = HashFunctions.shake128("seed", 32)
|
26
|
+
def shake128(data, length)
|
27
|
+
shake = SHA3::Digest::SHAKE_128.new
|
28
|
+
shake << data
|
29
|
+
shake.squeeze(length)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Computes the SHAKE256 extendable-output function (XOF).
|
33
|
+
#
|
34
|
+
# @param [String] data Input byte string.
|
35
|
+
# @param [Integer] length Number of output bytes to produce.
|
36
|
+
# @return [String] XOF output of the specified length.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# out = HashFunctions.shake256("key", 64)
|
40
|
+
def shake256(data, length)
|
41
|
+
shake = SHA3::Digest::SHAKE_256.new
|
42
|
+
shake << data
|
43
|
+
shake.squeeze(length)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Computes the SHA3-256 hash of the given data.
|
47
|
+
#
|
48
|
+
# @param [String] data Input byte string.
|
49
|
+
# @return [String] 32-byte hash digest.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# digest = HashFunctions.sha3_256("message")
|
53
|
+
def sha3_256(data)
|
54
|
+
digest = SHA3::Digest.new(:sha3_256)
|
55
|
+
digest.update(data)
|
56
|
+
digest.digest
|
57
|
+
end
|
58
|
+
|
59
|
+
# Computes the SHA3-512 hash of the given data.
|
60
|
+
#
|
61
|
+
# @param [String] data Input byte string.
|
62
|
+
# @return [String] 64-byte hash digest.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# digest = HashFunctions.sha3_512("message")
|
66
|
+
def sha3_512(data)
|
67
|
+
digest = SHA3::Digest.new(:sha3_512)
|
68
|
+
digest.update(data)
|
69
|
+
digest.digest
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sha3'
|
4
|
+
|
5
|
+
module MLKEM
|
6
|
+
module Crypto
|
7
|
+
# Provides symmetric cryptographic primitives as defined by ML-KEM (Kyber),
|
8
|
+
# including hash functions h, g, j and a pseudorandom function (PRF).
|
9
|
+
#
|
10
|
+
# These are used for key derivation, random generation, and message hashing.
|
11
|
+
#
|
12
|
+
# @since 0.1.0
|
13
|
+
class SymmetricPrimitives
|
14
|
+
class << self
|
15
|
+
# Hash function h: SHA3-256
|
16
|
+
#
|
17
|
+
# @param [String] x Input data to hash.
|
18
|
+
# @return [String] 32-byte digest.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# digest = SymmetricPrimitives.h("message")
|
22
|
+
def h(x)
|
23
|
+
HashFunctions.sha3_256(x)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Hash function g: SHA3-512, split into two 32-byte outputs.
|
27
|
+
#
|
28
|
+
# @param [String] x Input data to hash.
|
29
|
+
# @return [Array<String>] An array containing two 32-byte strings.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# g1, g2 = SymmetricPrimitives.g("seed")
|
33
|
+
def g(x)
|
34
|
+
hash = HashFunctions.sha3_512(x)
|
35
|
+
[hash[0...32], hash[32...64]]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Hash function j: SHAKE256 with 32-byte output.
|
39
|
+
#
|
40
|
+
# @param [String] s Input data to hash.
|
41
|
+
# @return [String] 32-byte XOF output.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# result = SymmetricPrimitives.j("domain")
|
45
|
+
def j(s)
|
46
|
+
HashFunctions.shake256(s, 32)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Pseudorandom Function (PRF)
|
50
|
+
#
|
51
|
+
# Uses SHAKE256 to expand `s || b` into `eta * 64` bytes of pseudorandom data.
|
52
|
+
#
|
53
|
+
# @param [Integer] eta Security parameter (e.g., 2 or 3).
|
54
|
+
# @param [String] s Secret seed.
|
55
|
+
# @param [Integer] b A single byte to include in domain separation.
|
56
|
+
# @return [String] Pseudorandom byte string of length `eta * 64`.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# prf_output = SymmetricPrimitives.prf(3, "key", 0x01)
|
60
|
+
def prf(eta, s, b)
|
61
|
+
input = s + [b].pack('C*')
|
62
|
+
HashFunctions.shake256(input, eta * 64)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MLKEM
|
4
|
+
# Math module containing all the math related operations to NTT, byte operations, polynomial and sampling.
|
5
|
+
#
|
6
|
+
# @since 0.1.0
|
7
|
+
module Math
|
8
|
+
# Byte and bit manipulation operations used in the ML-KEM standard (2024).
|
9
|
+
#
|
10
|
+
# Provides methods to convert between bit arrays and byte strings,
|
11
|
+
# and to encode and decode arrays according to given parameters.
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
class ByteOperations
|
15
|
+
# Converts an array of bits into a byte string.
|
16
|
+
#
|
17
|
+
# Implements Algorithm 3, BitsToBytes(b).
|
18
|
+
#
|
19
|
+
# @param [Array<Integer>] b An array of bits (0 or 1).
|
20
|
+
# @return [String] The resulting byte string.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# bits = [1,0,1,0,1,0,1,0]
|
24
|
+
# ByteOperations.bits_to_bytes(bits) # => "\x55" (byte representation)
|
25
|
+
def self.bits_to_bytes(b)
|
26
|
+
l = b.length
|
27
|
+
a = Array.new(l / 8, 0)
|
28
|
+
(0...l).step(8) do |i|
|
29
|
+
x = 0
|
30
|
+
8.times do |j|
|
31
|
+
x += b[i + j] << j
|
32
|
+
end
|
33
|
+
a[i / 8] = x
|
34
|
+
end
|
35
|
+
a.pack('C*')
|
36
|
+
end
|
37
|
+
|
38
|
+
# Converts a byte string into an array of bits.
|
39
|
+
#
|
40
|
+
# Implements Algorithm 4, BytesToBits(B).
|
41
|
+
#
|
42
|
+
# @param [String] b A byte string.
|
43
|
+
# @return [Array<Integer>] The resulting array of bits (0 or 1).
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# bytes = "\x55"
|
47
|
+
# ByteOperations.bytes_to_bits(bytes) # => [1,0,1,0,1,0,1,0]
|
48
|
+
def self.bytes_to_bits(b)
|
49
|
+
l = b.length
|
50
|
+
a = Array.new(8 * l, 0)
|
51
|
+
(0...(8 * l)).step(8) do |i|
|
52
|
+
x = b.bytes[i / 8]
|
53
|
+
8.times do |j|
|
54
|
+
a[i + j] = (x >> j) & 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
a
|
58
|
+
end
|
59
|
+
|
60
|
+
# Encodes an array of values into a byte string using dimension d and modulus q.
|
61
|
+
#
|
62
|
+
# Implements Algorithm 5, ByteEncode_d(F).
|
63
|
+
#
|
64
|
+
# @param [Integer] d Number of bits per value.
|
65
|
+
# @param [Array<Integer>, Array<Array<Integer>>] f Array of values or array of arrays to encode.
|
66
|
+
# @param [Integer] q Modulus for values (kept for compatibility; unused internally).
|
67
|
+
# @return [String] The encoded byte string.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# d = 8
|
71
|
+
# f = Array.new(256) { |i| i }
|
72
|
+
# ByteOperations.byte_encode(d, f, 256) # => encoded string
|
73
|
+
#
|
74
|
+
# @example Encoding multiple arrays:
|
75
|
+
# f_multi = [Array.new(256, 1), Array.new(256, 2)]
|
76
|
+
# ByteOperations.byte_encode(d, f_multi, 256) # => concatenated encoded string
|
77
|
+
def self.byte_encode(d, f, q)
|
78
|
+
if f.first.is_a?(Array)
|
79
|
+
return f.map { |x| byte_encode(d, x, q) }.join
|
80
|
+
end
|
81
|
+
|
82
|
+
b = Array.new(256 * d, 0)
|
83
|
+
|
84
|
+
256.times do |i|
|
85
|
+
a = f[i]
|
86
|
+
d.times do |j|
|
87
|
+
b[d * i + j] = (a >> j) & 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
bits_to_bytes(b)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Decodes a byte string into an array of values using dimension d and modulus q.
|
94
|
+
#
|
95
|
+
# Implements Algorithm 6, ByteDecode_d(B).
|
96
|
+
#
|
97
|
+
# @param [Integer] d Number of bits per value.
|
98
|
+
# @param [String] b Encoded byte string.
|
99
|
+
# @param [Integer] q Modulus for values.
|
100
|
+
# @return [Array<Integer>] The decoded array of values.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# d = 8
|
104
|
+
# encoded = ByteOperations.byte_encode(d, Array.new(256, 42), 256)
|
105
|
+
# ByteOperations.byte_decode(d, encoded, 256) # => Array with value 42 repeated
|
106
|
+
def self.byte_decode(d, b, q)
|
107
|
+
bits_array = bytes_to_bits(b)
|
108
|
+
f = Array.new(256, 0)
|
109
|
+
|
110
|
+
256.times do |i|
|
111
|
+
a = 0
|
112
|
+
d.times do |j|
|
113
|
+
a += bits_array[d * i + j] << j
|
114
|
+
end
|
115
|
+
f[i] = a % q
|
116
|
+
end
|
117
|
+
f
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MLKEM
|
4
|
+
module Math
|
5
|
+
# Implements the Number Theoretic Transform (NTT) and related operations
|
6
|
+
# as defined in the ML-KEM standard (2024), used for polynomial arithmetic
|
7
|
+
# in the ring Z_q[X]/(X^256 + 1).
|
8
|
+
#
|
9
|
+
# Includes forward and inverse NTT transforms, pointwise multiplication,
|
10
|
+
# and the base case multiplication algorithm.
|
11
|
+
#
|
12
|
+
# @since 0.1.0
|
13
|
+
class NTT
|
14
|
+
# Initializes an NTT instance with a modulus `q`.
|
15
|
+
#
|
16
|
+
# @param [Integer] q The modulus for all modular arithmetic.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# ntt = NTT.new
|
20
|
+
def initialize(q = Constants::Q)
|
21
|
+
@q = q
|
22
|
+
end
|
23
|
+
|
24
|
+
# Applies the forward Number Theoretic Transform to a polynomial.
|
25
|
+
#
|
26
|
+
# Implements Algorithm 9, NTT(f).
|
27
|
+
#
|
28
|
+
# @param [Array<Integer>] f A 256-element array representing the polynomial.
|
29
|
+
# @return [Array<Integer>] The transformed polynomial.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# transformed = ntt.ntt(original_poly)
|
33
|
+
def ntt(f)
|
34
|
+
f = f.dup
|
35
|
+
i = 1
|
36
|
+
le = 128
|
37
|
+
|
38
|
+
while le >= 2
|
39
|
+
(0...256).step(2 * le) do |st|
|
40
|
+
ze = Constants::ZETA_NTT[i]
|
41
|
+
i += 1
|
42
|
+
(st...(st + le)).each do |j|
|
43
|
+
t = (ze * f[j + le]) % @q
|
44
|
+
f[j + le] = (f[j] - t) % @q
|
45
|
+
f[j] = (f[j] + t) % @q
|
46
|
+
end
|
47
|
+
end
|
48
|
+
le /= 2
|
49
|
+
end
|
50
|
+
|
51
|
+
f
|
52
|
+
end
|
53
|
+
|
54
|
+
# Applies the inverse Number Theoretic Transform to a polynomial.
|
55
|
+
#
|
56
|
+
# Implements Algorithm 10, NTT^<sup>-1</sup>(f).
|
57
|
+
#
|
58
|
+
# @param [Array<Integer>] f A 256-element array in the NTT domain.
|
59
|
+
# @return [Array<Integer>] The inverse-transformed polynomial.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# original = ntt.inverse_ntt(transformed_poly)
|
63
|
+
def inverse_ntt(f)
|
64
|
+
f = f.dup
|
65
|
+
i = 127
|
66
|
+
le = 2
|
67
|
+
|
68
|
+
while le <= 128
|
69
|
+
(0...256).step(2 * le) do |st|
|
70
|
+
ze = Constants::ZETA_NTT[i]
|
71
|
+
i -= 1
|
72
|
+
(st...(st + le)).each do |j|
|
73
|
+
t = f[j]
|
74
|
+
f[j] = (t + f[j + le]) % @q
|
75
|
+
f[j + le] = (ze * (f[j + le] - t)) % @q
|
76
|
+
end
|
77
|
+
end
|
78
|
+
le *= 2
|
79
|
+
end
|
80
|
+
|
81
|
+
f.map { |x| (x * 3303) % @q }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Performs pointwise multiplication in the NTT domain.
|
85
|
+
#
|
86
|
+
# Implements Algorithm 11, MultiplyNTTs(~f, ~g).
|
87
|
+
#
|
88
|
+
# @param [Array<Integer>] f First polynomial in NTT form.
|
89
|
+
# @param [Array<Integer>] g Second polynomial in NTT form.
|
90
|
+
# @return [Array<Integer>] Result of the pointwise multiplication.
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# product = ntt.multiply_ntts(ntt_f, ntt_g)
|
94
|
+
def multiply_ntts(f, g)
|
95
|
+
h = []
|
96
|
+
(0...256).step(2) do |ii|
|
97
|
+
h.concat(base_case_multiply(f[ii], f[ii + 1], g[ii], g[ii + 1],
|
98
|
+
Constants::ZETA_MUL[ii / 2]))
|
99
|
+
end
|
100
|
+
h
|
101
|
+
end
|
102
|
+
|
103
|
+
# Performs the base case multiplication for two polynomial coefficient pairs.
|
104
|
+
#
|
105
|
+
# Implements Algorithm 12, BaseCaseMultiply(a0, a1, b0, b1, gamma).
|
106
|
+
#
|
107
|
+
# @param [Integer] a0 Coefficient a_0
|
108
|
+
# @param [Integer] a1 Coefficient a_1
|
109
|
+
# @param [Integer] b0 Coefficient b_0
|
110
|
+
# @param [Integer] b1 Coefficient b_1
|
111
|
+
# @param [Integer] gam Precomputed gamma value
|
112
|
+
# @return [Array<Integer>] The resulting coefficients [c0, c1]
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# ntt.base_case_multiply(3, 4, 5, 6, gamma) # => [expected_c0, expected_c1]
|
116
|
+
def base_case_multiply(a0, a1, b0, b1, gam)
|
117
|
+
c0 = (a0 * b0 + a1 * b1 * gam) % @q
|
118
|
+
c1 = (a0 * b1 + a1 * b0) % @q
|
119
|
+
[c0, c1]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MLKEM
|
4
|
+
module Math
|
5
|
+
# Implements polynomial arithmetic and compression/decompression
|
6
|
+
# operations used in ML-KEM (Kyber) cryptographic schemes.
|
7
|
+
#
|
8
|
+
# Provides basic modular operations such as addition and subtraction
|
9
|
+
# as well as lossy compression methods used to reduce bandwidth.
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
class Polynomial
|
13
|
+
# Initializes a Polynomial instance with a modulus q.
|
14
|
+
#
|
15
|
+
# @param [Integer] q The modulus used for polynomial arithmetic.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# poly = Polynomial.new
|
19
|
+
def initialize(q = Constants::Q)
|
20
|
+
@q = q
|
21
|
+
end
|
22
|
+
|
23
|
+
# Adds two polynomials coefficient-wise modulo q.
|
24
|
+
#
|
25
|
+
# @param [Array<Integer>] f First polynomial.
|
26
|
+
# @param [Array<Integer>] g Second polynomial.
|
27
|
+
# @return [Array<Integer>] Resulting polynomial (f + g) mod q.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# result = poly.add([1, 2], [3, 4]) # => [4, 6]
|
31
|
+
def add(f, g)
|
32
|
+
f.zip(g).map { |a, b| (a + b) % @q }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Subtracts one polynomial from another coefficient-wise modulo q.
|
36
|
+
#
|
37
|
+
# @param [Array<Integer>] f Minuend polynomial.
|
38
|
+
# @param [Array<Integer>] g Subtrahend polynomial.
|
39
|
+
# @return [Array<Integer>] Resulting polynomial (f - g) mod q.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# result = poly.subtract([5, 3], [2, 1]) # => [3, 2]
|
43
|
+
def subtract(f, g)
|
44
|
+
f.zip(g).map { |a, b| (a - b) % @q }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Compresses the coefficients of a polynomial to `d` bits.
|
48
|
+
#
|
49
|
+
# Lossy operation used to reduce size during transmission.
|
50
|
+
#
|
51
|
+
# @param [Integer] d Number of bits to compress to.
|
52
|
+
# @param [Array<Integer>] xv Polynomial coefficients.
|
53
|
+
# @return [Array<Integer>] Compressed coefficients.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# compressed = poly.compress(4, [0, 1000, 2000])
|
57
|
+
def compress(d, xv)
|
58
|
+
xv.map do |x|
|
59
|
+
(((x << d) + (@q - 1) / 2) / @q) % (1 << d)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Decompresses `d`-bit values back into approximate polynomial coefficients.
|
64
|
+
#
|
65
|
+
# Inverse of `#compress`, though lossy.
|
66
|
+
#
|
67
|
+
# @param [Integer] d Number of bits the coefficients were compressed to.
|
68
|
+
# @param [Array<Integer>] yv Compressed coefficients.
|
69
|
+
# @return [Array<Integer>] Approximate original coefficients.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# decompressed = poly.decompress(4, [0, 5, 10])
|
73
|
+
def decompress(d, yv)
|
74
|
+
yv.map do |y|
|
75
|
+
(@q * y + (1 << (d - 1))) >> d
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sha3'
|
4
|
+
|
5
|
+
module MLKEM
|
6
|
+
module Math
|
7
|
+
# Implements sampling algorithms used in ML-KEM (Kyber),
|
8
|
+
# including NTT-domain sampling and centered binomial distribution (CBD).
|
9
|
+
#
|
10
|
+
# These routines are used to generate polynomial coefficients
|
11
|
+
# from uniformly random byte arrays or XOF outputs.
|
12
|
+
#
|
13
|
+
# @since 0.1.0
|
14
|
+
class Sampling
|
15
|
+
# Initializes the Sampling object with a modulus `q`.
|
16
|
+
#
|
17
|
+
# @param [Integer] q The modulus used in coefficient arithmetic.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# sampling = Sampling.new
|
21
|
+
def initialize(q = Constants::Q)
|
22
|
+
@q = q
|
23
|
+
end
|
24
|
+
|
25
|
+
# Samples a polynomial in the NTT domain from a byte string using SHAKE128.
|
26
|
+
#
|
27
|
+
# Implements Algorithm 7, SampleNTT(B).
|
28
|
+
#
|
29
|
+
# This uses rejection sampling to select values uniformly < q from SHAKE128 output.
|
30
|
+
#
|
31
|
+
# @param [String] b Input byte string to expand into polynomial coefficients.
|
32
|
+
# @return [Array<Integer>] A 256-element array of sampled coefficients < q.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# sampled = sampling.sample_ntt(seed_bytes)
|
36
|
+
def sample_ntt(b)
|
37
|
+
xof_data = Crypto::HashFunctions.shake128(b, 1024)
|
38
|
+
j = 0
|
39
|
+
a = []
|
40
|
+
i = 0
|
41
|
+
|
42
|
+
while j < 256 && i < xof_data.length - 2
|
43
|
+
c = xof_data.bytes[i, 3]
|
44
|
+
d1 = c[0] + 256 * (c[1] % 16)
|
45
|
+
d2 = (c[1] / 16) + 16 * c[2]
|
46
|
+
|
47
|
+
if d1 < @q
|
48
|
+
a << d1
|
49
|
+
j += 1
|
50
|
+
end
|
51
|
+
|
52
|
+
if d2 < @q && j < 256
|
53
|
+
a << d2
|
54
|
+
j += 1
|
55
|
+
end
|
56
|
+
|
57
|
+
i += 3
|
58
|
+
end
|
59
|
+
|
60
|
+
a
|
61
|
+
end
|
62
|
+
|
63
|
+
# Samples a polynomial using centered binomial distribution (CBD).
|
64
|
+
#
|
65
|
+
# Implements Algorithm 8, SamplePolyCBD_eta(B).
|
66
|
+
#
|
67
|
+
# The result is a noise polynomial used in ML-KEM.
|
68
|
+
#
|
69
|
+
# @param [Integer] eta CBD parameter (usually 2 or 3).
|
70
|
+
# @param [String] b Byte string used as input entropy.
|
71
|
+
# @return [Array<Integer>] A 256-element array of polynomial coefficients modulo q.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# noise = sampling.sample_poly_cbd(3, seed_bytes)
|
75
|
+
def sample_poly_cbd(eta, b)
|
76
|
+
b_bits = Math::ByteOperations.bytes_to_bits(b)
|
77
|
+
f = Array.new(256, 0)
|
78
|
+
|
79
|
+
256.times do |i|
|
80
|
+
x = b_bits[(2 * i * eta)...((2 * i + 1) * eta)].sum
|
81
|
+
y = b_bits[((2 * i + 1) * eta)...((2 * i + 2) * eta)].sum
|
82
|
+
f[i] = (x - y) % @q
|
83
|
+
end
|
84
|
+
|
85
|
+
f
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/ml_kem/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ml_kem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- MarioRgzLpz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sha3
|
@@ -30,56 +30,70 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 1.3.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 1.3.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: yard
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.9.37
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.9.37
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: minitest
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 5.25.5
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 5.25.5
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 13.3.0
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 13.3.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: irb
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.15.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.15.2
|
83
97
|
description: A Ruby gem providing an implementation of the ML-KEM (formerly Kyber)
|
84
98
|
key-encapsulation mechanism for post-quantum cryptography standards.
|
85
99
|
email:
|
@@ -91,11 +105,19 @@ extra_rdoc_files: []
|
|
91
105
|
files:
|
92
106
|
- LICENSE.txt
|
93
107
|
- README.md
|
94
|
-
- Rakefile
|
95
108
|
- exe/mlkem
|
96
109
|
- lib/ml_kem.rb
|
110
|
+
- lib/ml_kem/cli.rb
|
111
|
+
- lib/ml_kem/constants.rb
|
112
|
+
- lib/ml_kem/core/k_pke.rb
|
113
|
+
- lib/ml_kem/core/ml_kem_internal.rb
|
114
|
+
- lib/ml_kem/crypto/hash_functions.rb
|
115
|
+
- lib/ml_kem/crypto/symmetric_primitives.rb
|
116
|
+
- lib/ml_kem/math/byte_operations.rb
|
117
|
+
- lib/ml_kem/math/ntt.rb
|
118
|
+
- lib/ml_kem/math/polynomial.rb
|
119
|
+
- lib/ml_kem/math/sampling.rb
|
97
120
|
- lib/ml_kem/version.rb
|
98
|
-
- sig/ml_kem.rbs
|
99
121
|
homepage: https://github.com/MarioRgzLpz/ml_kem
|
100
122
|
licenses:
|
101
123
|
- MIT
|
data/Rakefile
DELETED
data/sig/ml_kem.rbs
DELETED