ruby-paseto 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +549 -0
- data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/curve_private_key.rb +22 -0
- data/lib/paseto/asn1/ec_private_key.rb +27 -0
- data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
- data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
- data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
- data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
- data/lib/paseto/asn1/named_curve.rb +17 -0
- data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
- data/lib/paseto/asn1/private_key.rb +17 -0
- data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/public_key.rb +17 -0
- data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
- data/lib/paseto/asn1.rb +101 -0
- data/lib/paseto/asymmetric_key.rb +100 -0
- data/lib/paseto/configuration/box.rb +23 -0
- data/lib/paseto/configuration/decode_configuration.rb +68 -0
- data/lib/paseto/configuration.rb +18 -0
- data/lib/paseto/interface/i_d.rb +23 -0
- data/lib/paseto/interface/key.rb +113 -0
- data/lib/paseto/interface/pbkd.rb +83 -0
- data/lib/paseto/interface/pie.rb +59 -0
- data/lib/paseto/interface/pke.rb +86 -0
- data/lib/paseto/interface/serializer.rb +19 -0
- data/lib/paseto/interface/version.rb +161 -0
- data/lib/paseto/interface/wrapper.rb +20 -0
- data/lib/paseto/operations/i_d.rb +48 -0
- data/lib/paseto/operations/id/i_dv3.rb +20 -0
- data/lib/paseto/operations/id/i_dv4.rb +20 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
- data/lib/paseto/operations/pbkw.rb +73 -0
- data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
- data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
- data/lib/paseto/operations/pke.rb +57 -0
- data/lib/paseto/operations/wrap.rb +29 -0
- data/lib/paseto/paserk.rb +55 -0
- data/lib/paseto/paserk_types.rb +46 -0
- data/lib/paseto/protocol/version3.rb +100 -0
- data/lib/paseto/protocol/version4.rb +99 -0
- data/lib/paseto/result.rb +9 -0
- data/lib/paseto/serializer/optional_json.rb +30 -0
- data/lib/paseto/serializer/raw.rb +23 -0
- data/lib/paseto/sodium/curve_25519.rb +46 -0
- data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
- data/lib/paseto/sodium/stream/base.rb +82 -0
- data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
- data/lib/paseto/sodium.rb +5 -0
- data/lib/paseto/symmetric_key.rb +119 -0
- data/lib/paseto/token.rb +127 -0
- data/lib/paseto/token_types.rb +29 -0
- data/lib/paseto/util.rb +105 -0
- data/lib/paseto/v3/local.rb +63 -0
- data/lib/paseto/v3/public.rb +204 -0
- data/lib/paseto/v4/local.rb +56 -0
- data/lib/paseto/v4/public.rb +169 -0
- data/lib/paseto/validator.rb +154 -0
- data/lib/paseto/verifiers/footer.rb +30 -0
- data/lib/paseto/verifiers/payload.rb +42 -0
- data/lib/paseto/verify.rb +48 -0
- data/lib/paseto/version.rb +6 -0
- data/lib/paseto/versions.rb +25 -0
- data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
- data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
- data/lib/paseto/wrappers/pie.rb +71 -0
- data/lib/paseto.rb +99 -0
- data/paseto.gemspec +58 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
- data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
- data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
- data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
- data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
- data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
- data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
- data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
- data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
- data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
- data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
- data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
- data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
- data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
- data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
- data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
- data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
- data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
- data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
- data/sorbet/rbi/shims/multi_json.rbi +19 -0
- data/sorbet/rbi/shims/openssl.rbi +111 -0
- data/sorbet/rbi/shims/rbnacl.rbi +65 -0
- data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
- data/sorbet/rbi/todo.rbi +7 -0
- data/sorbet/tapioca/config.yml +30 -0
- data/sorbet/tapioca/require.rb +12 -0
- metadata +376 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
module PBKD
|
8
|
+
class PBKDv3
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include Interface::PBKD
|
12
|
+
|
13
|
+
sig { override.returns(Protocol::Version3) }
|
14
|
+
def self.protocol
|
15
|
+
Protocol::Version3.new
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(password: String).void }
|
19
|
+
def initialize(password)
|
20
|
+
@password = password
|
21
|
+
end
|
22
|
+
|
23
|
+
sig do
|
24
|
+
override.params(
|
25
|
+
header: String,
|
26
|
+
pre_key: String,
|
27
|
+
salt: String,
|
28
|
+
nonce: String,
|
29
|
+
edk: String,
|
30
|
+
params: T::Hash[Symbol, Integer]
|
31
|
+
).returns([String, String])
|
32
|
+
end
|
33
|
+
def authenticate(header:, pre_key:, salt:, nonce:, edk:, params:) # rubocop:disable Metrics/ParameterLists
|
34
|
+
iterations = Util.int_to_be32(T.must(params[:iterations]))
|
35
|
+
|
36
|
+
message = "#{salt}#{iterations}#{nonce}#{edk}"
|
37
|
+
|
38
|
+
ak = protocol.digest("#{Operations::PBKW::DOMAIN_SEPARATOR_AUTH}#{pre_key}")
|
39
|
+
tag = protocol.hmac("#{header}.#{message}", key: ak)
|
40
|
+
[message, tag]
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { override.params(salt: String, params: T::Hash[Symbol, Integer]).returns(String) }
|
44
|
+
def pre_key(salt:, params:)
|
45
|
+
iterations = T.must(params[:iterations])
|
46
|
+
protocol.kdf(@password, salt: salt, length: 32, iterations: iterations)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { override.returns(String) }
|
50
|
+
def random_nonce
|
51
|
+
protocol.random(16)
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { override.returns(String) }
|
55
|
+
def random_salt
|
56
|
+
protocol.random(32)
|
57
|
+
end
|
58
|
+
|
59
|
+
sig do
|
60
|
+
override.params(payload: String).returns(
|
61
|
+
{
|
62
|
+
salt: String,
|
63
|
+
nonce: String,
|
64
|
+
edk: String,
|
65
|
+
tag: String,
|
66
|
+
params: T::Hash[Symbol, Integer]
|
67
|
+
}
|
68
|
+
)
|
69
|
+
end
|
70
|
+
def decode(payload)
|
71
|
+
data = Util.decode64(payload)
|
72
|
+
edk_len = data.bytesize - 100
|
73
|
+
iterations = Util.be32_to_int(T.must(data.byteslice(32, 4)))
|
74
|
+
{
|
75
|
+
salt: T.must(data.byteslice(0, 32)),
|
76
|
+
nonce: T.must(data.byteslice(36, 16)),
|
77
|
+
edk: T.must(data.byteslice(52, edk_len)),
|
78
|
+
tag: T.must(data.byteslice(-48, 48)),
|
79
|
+
params: { iterations: iterations }
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
module PBKD
|
8
|
+
class PBKDv4
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include Interface::PBKD
|
12
|
+
|
13
|
+
sig { override.returns(Protocol::Version4) }
|
14
|
+
def self.protocol
|
15
|
+
Protocol::Version4.new
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(password: String).void }
|
19
|
+
def initialize(password)
|
20
|
+
@password = password
|
21
|
+
end
|
22
|
+
|
23
|
+
sig do
|
24
|
+
override.params(
|
25
|
+
header: String,
|
26
|
+
pre_key: String,
|
27
|
+
salt: String,
|
28
|
+
nonce: String,
|
29
|
+
edk: String,
|
30
|
+
params: T::Hash[Symbol, Integer]
|
31
|
+
).returns([String, String])
|
32
|
+
end
|
33
|
+
def authenticate(header:, pre_key:, salt:, nonce:, edk:, params:) # rubocop:disable Metrics/ParameterLists
|
34
|
+
memlimit_int = RbNaCl::PasswordHash::Argon2.memlimit_value(params[:memlimit])
|
35
|
+
opslimit_int = RbNaCl::PasswordHash::Argon2.opslimit_value(params[:opslimit])
|
36
|
+
memlimit = Util.int_to_be64(memlimit_int)
|
37
|
+
opslimit = Util.int_to_be32(opslimit_int)
|
38
|
+
para = Util.int_to_be32(1)
|
39
|
+
|
40
|
+
message = "#{salt}#{memlimit}#{opslimit}#{para}#{nonce}#{edk}"
|
41
|
+
|
42
|
+
ak = protocol.digest("#{Operations::PBKW::DOMAIN_SEPARATOR_AUTH}#{pre_key}", digest_size: 32)
|
43
|
+
tag = protocol.hmac("#{header}.#{message}", key: ak, digest_size: 32)
|
44
|
+
|
45
|
+
[message, tag]
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { override.params(salt: String, params: T::Hash[Symbol, Integer]).returns(String) }
|
49
|
+
def pre_key(salt:, params:)
|
50
|
+
opslimit = T.must(params[:opslimit])
|
51
|
+
memlimit = T.must(params[:memlimit])
|
52
|
+
protocol.kdf(@password, salt: salt, length: 32, opslimit: opslimit, memlimit: memlimit)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { override.returns(String) }
|
56
|
+
def random_nonce
|
57
|
+
protocol.random(24)
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { override.returns(String) }
|
61
|
+
def random_salt
|
62
|
+
protocol.random(16)
|
63
|
+
end
|
64
|
+
|
65
|
+
sig do
|
66
|
+
override.params(payload: String).returns(
|
67
|
+
{
|
68
|
+
salt: String,
|
69
|
+
nonce: String,
|
70
|
+
edk: String,
|
71
|
+
tag: String,
|
72
|
+
params: T::Hash[Symbol, Integer]
|
73
|
+
}
|
74
|
+
)
|
75
|
+
end
|
76
|
+
def decode(payload) # rubocop:disable Metrics/AbcSize
|
77
|
+
data = Util.decode64(payload)
|
78
|
+
edk_len = data.bytesize - 88
|
79
|
+
{
|
80
|
+
salt: T.must(data.byteslice(0, 16)),
|
81
|
+
nonce: T.must(data.byteslice(32, 24)),
|
82
|
+
edk: T.must(data.byteslice(56, edk_len)),
|
83
|
+
tag: T.must(data.byteslice(-32, 32)),
|
84
|
+
params: {
|
85
|
+
memlimit: Util.be64_to_int(T.must(data.byteslice(16, 8))),
|
86
|
+
opslimit: Util.be32_to_int(T.must(data.byteslice(24, 4))),
|
87
|
+
para: Util.be32_to_int(T.must(data.byteslice(28, 4)))
|
88
|
+
}
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
class PBKW
|
8
|
+
DOMAIN_SEPARATOR_ENCRYPT = T.let("\xFF", String)
|
9
|
+
DOMAIN_SEPARATOR_AUTH = T.let("\xFE", String)
|
10
|
+
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(key: Interface::Key, password: String, options: T::Hash[Symbol, T.any(Integer, Symbol)]).returns(String) }
|
14
|
+
def self.pbkw(key, password, options = {})
|
15
|
+
new(key.protocol, password).encode(key, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(version: Interface::Version, password: String).void }
|
19
|
+
def initialize(version, password)
|
20
|
+
@coder = T.let(version.pbkw(password), Interface::PBKD)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(key: Interface::Key, options: T::Hash[Symbol, T.any(Integer, Symbol)]).returns(String) }
|
24
|
+
def encode(key, options)
|
25
|
+
raise LucidityError unless key.protocol == @coder.protocol
|
26
|
+
|
27
|
+
opts = default_options.merge(options)
|
28
|
+
|
29
|
+
h = key.pbkw_header
|
30
|
+
salt = @coder.random_salt
|
31
|
+
nonce = @coder.random_nonce
|
32
|
+
|
33
|
+
pre_key = @coder.pre_key(salt: salt, params: opts)
|
34
|
+
|
35
|
+
edk = @coder.crypt(payload: key.to_bytes, key: pre_key, nonce: nonce)
|
36
|
+
|
37
|
+
message, t = @coder.authenticate(header: h, pre_key: pre_key, salt: salt, nonce: nonce, edk: edk, params: opts)
|
38
|
+
|
39
|
+
data = Util.encode64("#{message}#{t}")
|
40
|
+
"#{h}.#{data}"
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(paserk: String).returns(Interface::Key) }
|
44
|
+
def decode(paserk)
|
45
|
+
paserk.split('.') => [version, type, data]
|
46
|
+
raise LucidityError unless version == @coder.paserk_version
|
47
|
+
|
48
|
+
header = "#{version}.#{type}"
|
49
|
+
|
50
|
+
@coder.decode(data) => {salt:, nonce:, edk:, tag:, params:}
|
51
|
+
|
52
|
+
pre_key = @coder.pre_key(salt: salt, params: params)
|
53
|
+
|
54
|
+
_, t2 = @coder.authenticate(header: header, pre_key: pre_key, salt: salt, nonce: nonce, edk: edk, params: params)
|
55
|
+
raise InvalidAuthenticator unless Util.constant_compare(t2, tag)
|
56
|
+
|
57
|
+
ptk = @coder.crypt(payload: edk, key: pre_key, nonce: nonce)
|
58
|
+
PaserkTypes.deserialize(header).generate(ptk)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
sig { returns({ iterations: Integer, memlimit: Symbol, opslimit: Symbol }) }
|
64
|
+
def default_options
|
65
|
+
{
|
66
|
+
iterations: 100_000,
|
67
|
+
memlimit: :interactive,
|
68
|
+
opslimit: :interactive
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
class PKE
|
8
|
+
class PKEv3
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include Interface::PKE
|
12
|
+
|
13
|
+
sig { override.params(esk: OpenSSL::PKey::EC).returns(String) }
|
14
|
+
def self.epk_bytes_from_esk(esk)
|
15
|
+
esk.public_key.to_octet_string(:compressed)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig(:final) { override.returns(OpenSSL::PKey::EC) }
|
19
|
+
def self.generate_ephemeral_key
|
20
|
+
OpenSSL::PKey::EC.generate('secp384r1')
|
21
|
+
end
|
22
|
+
|
23
|
+
sig(:final) { override.returns(String) }
|
24
|
+
def self.header
|
25
|
+
'k3.seal.'
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { override.returns(Protocol::Version3) }
|
29
|
+
def self.protocol
|
30
|
+
Protocol::Version3.new
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { override.params(encoded_data: String).returns([String, OpenSSL::PKey::EC::Point, String]) }
|
34
|
+
def self.split(encoded_data)
|
35
|
+
data = Util.decode64(encoded_data)
|
36
|
+
|
37
|
+
t = T.must(data.slice(0, 48))
|
38
|
+
|
39
|
+
epk_bytes = T.must(data.slice(48, 49))
|
40
|
+
epk = OpenSSL::PKey::EC::Point.new(OpenSSL::PKey::EC::Group.new('secp384r1'), epk_bytes)
|
41
|
+
|
42
|
+
edk = T.must(data.slice(97, 32))
|
43
|
+
|
44
|
+
[t, epk, edk]
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(sealing_key: AsymmetricKey).void }
|
48
|
+
def initialize(sealing_key)
|
49
|
+
raise LucidityError unless sealing_key.is_a? V3::Public
|
50
|
+
|
51
|
+
@sealing_key = T.let(sealing_key, V3::Public)
|
52
|
+
@pk = T.let(@sealing_key.public_bytes, String)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { override.params(message: String, ek: String, n: String).returns(SymmetricKey) }
|
56
|
+
def decrypt(message:, ek:, n:)
|
57
|
+
pdk = protocol.crypt(key: ek, nonce: n, payload: message)
|
58
|
+
V3::Local.new(ikm: pdk)
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { override.params(xk: String, epk: OpenSSL::PKey::EC::Point).returns({ ek: String, n: String }) }
|
62
|
+
def derive_ek_n(xk:, epk:)
|
63
|
+
epk_bytes = epk.to_octet_string(:compressed)
|
64
|
+
|
65
|
+
x = protocol.digest("#{DOMAIN_SEPARATOR_ENCRYPT}#{header}#{xk}#{epk_bytes}#{@pk}")
|
66
|
+
|
67
|
+
ek = T.must(x[0, 32])
|
68
|
+
n = T.must(x[32, 16])
|
69
|
+
|
70
|
+
{ ek: ek, n: n }
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { override.params(xk: String, epk: OpenSSL::PKey::EC::Point).returns(String) }
|
74
|
+
def derive_ak(xk:, epk:)
|
75
|
+
epk_bytes = epk.to_octet_string(:compressed)
|
76
|
+
protocol.digest("#{DOMAIN_SEPARATOR_AUTH}#{header}#{xk}#{epk_bytes}#{@pk}")
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { override.params(message: String, ek: String, n: String).returns(String) }
|
80
|
+
def encrypt(message:, ek:, n:)
|
81
|
+
protocol.crypt(payload: message, key: ek, nonce: n)
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { override.params(ak: String, epk: OpenSSL::PKey::EC::Point, edk: String).returns(String) }
|
85
|
+
def tag(ak:, epk:, edk:)
|
86
|
+
epk_bytes = epk.to_octet_string(:compressed)
|
87
|
+
protocol.hmac("#{header}#{epk_bytes}#{edk}", key: ak)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
sig { override.returns(Paseto::V3::Public) }
|
93
|
+
attr_reader :sealing_key
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
class PKE
|
8
|
+
class PKEv4
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include Interface::PKE
|
12
|
+
|
13
|
+
sig { override.params(esk: RbNaCl::PrivateKey).returns(String) }
|
14
|
+
def self.epk_bytes_from_esk(esk)
|
15
|
+
esk.public_key.to_bytes
|
16
|
+
end
|
17
|
+
|
18
|
+
sig(:final) { override.returns(RbNaCl::PrivateKey) }
|
19
|
+
def self.generate_ephemeral_key
|
20
|
+
RbNaCl::PrivateKey.generate
|
21
|
+
end
|
22
|
+
|
23
|
+
sig(:final) { override.returns(String) }
|
24
|
+
def self.header
|
25
|
+
'k4.seal.'
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { override.returns(Protocol::Version4) }
|
29
|
+
def self.protocol
|
30
|
+
Protocol::Version4.new
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { override.params(encoded_data: String).returns([String, RbNaCl::PublicKey, String]) }
|
34
|
+
def self.split(encoded_data)
|
35
|
+
data = Util.decode64(encoded_data)
|
36
|
+
|
37
|
+
t = T.must(data.slice(0, 32))
|
38
|
+
|
39
|
+
epk_bytes = T.must(data.slice(32, 32))
|
40
|
+
epk = RbNaCl::PublicKey.new(epk_bytes)
|
41
|
+
|
42
|
+
edk = T.must(data.slice(64, 32))
|
43
|
+
|
44
|
+
[t, epk, edk]
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(sealing_key: AsymmetricKey).void }
|
48
|
+
def initialize(sealing_key)
|
49
|
+
raise LucidityError unless sealing_key.is_a? V4::Public
|
50
|
+
|
51
|
+
@sealing_key = T.let(sealing_key, V4::Public)
|
52
|
+
@pk = T.let(@sealing_key.x25519_public_key, RbNaCl::PublicKey)
|
53
|
+
@pk_bytes = T.let(@pk.to_bytes, String)
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { override.params(message: String, ek: String, n: String).returns(SymmetricKey) }
|
57
|
+
def decrypt(message:, ek:, n:)
|
58
|
+
pdk = protocol.crypt(payload: message, key: ek, nonce: n)
|
59
|
+
V4::Local.new(ikm: pdk)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { override.params(xk: String, epk: RbNaCl::PublicKey).returns({ ek: String, n: String }) }
|
63
|
+
def derive_ek_n(xk:, epk:)
|
64
|
+
ek = protocol.digest(
|
65
|
+
"#{DOMAIN_SEPARATOR_ENCRYPT}#{header}#{xk}#{epk.to_bytes}#{@pk_bytes}",
|
66
|
+
digest_size: 32
|
67
|
+
)
|
68
|
+
n = protocol.digest("#{epk.to_bytes}#{@pk_bytes}", digest_size: 24)
|
69
|
+
|
70
|
+
{ ek: ek, n: n }
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { override.params(xk: String, epk: RbNaCl::PublicKey).returns(String) }
|
74
|
+
def derive_ak(xk:, epk:)
|
75
|
+
protocol.digest([DOMAIN_SEPARATOR_AUTH, header, xk, epk.to_bytes, @pk_bytes].join, digest_size: 32)
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { override.params(message: String, ek: String, n: String).returns(String) }
|
79
|
+
def encrypt(message:, ek:, n:)
|
80
|
+
protocol.crypt(payload: message, key: ek, nonce: n)
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { override.params(ak: String, epk: RbNaCl::PublicKey, edk: String).returns(String) }
|
84
|
+
def tag(ak:, epk:, edk:)
|
85
|
+
protocol.hmac("#{header}#{epk.to_bytes}#{edk}", key: ak, digest_size: 32)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
sig { override.returns(Paseto::V4::Public) }
|
91
|
+
attr_reader :sealing_key
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
class PKE
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(sealing_key: AsymmetricKey).void }
|
11
|
+
def initialize(sealing_key)
|
12
|
+
@sealing_key = sealing_key
|
13
|
+
@coder = T.let(@sealing_key.pke, Paseto::Interface::PKE)
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { params(key: SymmetricKey).returns(String) }
|
17
|
+
def seal(key)
|
18
|
+
raise LucidityError unless key.protocol == @sealing_key.protocol
|
19
|
+
|
20
|
+
esk = @coder.generate_ephemeral_key
|
21
|
+
epk = esk.public_key
|
22
|
+
|
23
|
+
xk = @sealing_key.ecdh(esk)
|
24
|
+
|
25
|
+
@coder.derive_ek_n(xk: xk, epk: epk) => {ek:, n:}
|
26
|
+
|
27
|
+
edk = @coder.encrypt(message: key.to_bytes, ek: ek, n: n)
|
28
|
+
|
29
|
+
ak = @coder.derive_ak(xk: xk, epk: epk)
|
30
|
+
t = @coder.tag(ak: ak, epk: epk, edk: edk)
|
31
|
+
|
32
|
+
epk_bytes = @coder.epk_bytes_from_esk(esk)
|
33
|
+
data = Util.encode64("#{t}#{epk_bytes}#{edk}")
|
34
|
+
"#{@coder.header}#{data}"
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { params(paserk: String).returns(Interface::Key) }
|
38
|
+
def unseal(paserk)
|
39
|
+
paserk.split('.') => [version, type, encoded_data]
|
40
|
+
raise LucidityError unless version == @sealing_key.paserk_version
|
41
|
+
raise LucidityError unless type == 'seal'
|
42
|
+
|
43
|
+
t, epk, edk = @coder.split(encoded_data)
|
44
|
+
|
45
|
+
xk = @sealing_key.ecdh(epk)
|
46
|
+
|
47
|
+
ak = @coder.derive_ak(xk: xk, epk: epk)
|
48
|
+
t2 = @coder.tag(ak: ak, epk: epk, edk: edk)
|
49
|
+
raise InvalidAuthenticator unless Util.constant_compare(t, t2)
|
50
|
+
|
51
|
+
@coder.derive_ek_n(xk: xk, epk: epk) => {ek:, n:}
|
52
|
+
|
53
|
+
@coder.decrypt(message: edk, ek: ek, n: n)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Paseto
|
6
|
+
module Operations
|
7
|
+
class Wrap
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
DOMAIN_SEPARATOR_AUTH = "\x81"
|
11
|
+
DOMAIN_SEPARATOR_ENCRYPT = "\x80"
|
12
|
+
|
13
|
+
sig { params(wrapping_key: SymmetricKey, paserk: [String, String, String, String]).returns(Interface::Key) }
|
14
|
+
def self.unwrap(wrapping_key, paserk)
|
15
|
+
case paserk
|
16
|
+
in [_, _, String => protocol, _] if protocol == 'pie'
|
17
|
+
Wrappers::PIE.new(wrapping_key).decode(paserk)
|
18
|
+
else
|
19
|
+
raise UnknownProtocol, protocol
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(key: Interface::Key, wrapping_key: SymmetricKey, nonce: T.nilable(String)).returns(String) }
|
24
|
+
def self.wrap(key, wrapping_key:, nonce: nil)
|
25
|
+
Wrappers::PIE.new(wrapping_key).encode(key, nonce: nonce)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Paseto
|
5
|
+
module Paserk
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
8
|
+
|
9
|
+
requires_ancestor { Kernel }
|
10
|
+
|
11
|
+
sig do
|
12
|
+
params(
|
13
|
+
paserk: String,
|
14
|
+
wrapping_key: T.nilable(SymmetricKey),
|
15
|
+
password: T.nilable(String),
|
16
|
+
unsealing_key: T.nilable(AsymmetricKey)
|
17
|
+
).returns(T.untyped)
|
18
|
+
end
|
19
|
+
def self.from_paserk(paserk:, wrapping_key: nil, password: nil, unsealing_key: nil)
|
20
|
+
parts = paserk.split('.')
|
21
|
+
case parts
|
22
|
+
in [String => version, String => type, String => protocol, String => data] if wrapping_key
|
23
|
+
Operations::Wrap.unwrap(T.must(wrapping_key), [version, type, protocol, data])
|
24
|
+
|
25
|
+
in [String => version, String => type, String => data] if password
|
26
|
+
version = Versions.deserialize(version).instance
|
27
|
+
Operations::PBKW.new(version, T.must(password)).decode(paserk)
|
28
|
+
|
29
|
+
in [String => version, String => type, String => data] if unsealing_key
|
30
|
+
Operations::PKE.new(T.must(unsealing_key)).unseal(paserk)
|
31
|
+
|
32
|
+
in [String => version, String => type, String => data] if %w[local secret public].include?(type)
|
33
|
+
PaserkTypes.deserialize(paserk.rpartition('.').first).generate(Util.decode64(data))
|
34
|
+
|
35
|
+
else
|
36
|
+
raise UnknownOperation
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(key: Interface::Key, wrapping_key: SymmetricKey, nonce: T.nilable(String)).returns(String) }
|
41
|
+
def self.wrap(key:, wrapping_key:, nonce: nil)
|
42
|
+
Operations::Wrap.wrap(key, wrapping_key: wrapping_key, nonce: nonce)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(key: Interface::Key, password: String, options: T::Hash[Symbol, T.any(Integer, Symbol)]).returns(String) }
|
46
|
+
def self.pbkw(key:, password:, options: {})
|
47
|
+
Operations::PBKW.pbkw(key, password, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(key: SymmetricKey, sealing_key: AsymmetricKey).returns(String) }
|
51
|
+
def self.seal(key:, sealing_key:)
|
52
|
+
Operations::PKE.new(sealing_key).seal(key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Paseto
|
5
|
+
class PaserkTypes < T::Enum
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
enums do
|
9
|
+
K3Local = new('k3.local')
|
10
|
+
K3Secret = new('k3.secret')
|
11
|
+
K3Public = new('k3.public')
|
12
|
+
K3LocalWrap = new('k3.local-wrap')
|
13
|
+
K3SecretWrap = new('k3.secret-wrap')
|
14
|
+
K3LocalPBKW = new('k3.local-pw')
|
15
|
+
K3SecretPBKW = new('k3.secret-pw')
|
16
|
+
|
17
|
+
K4Local = new('k4.local')
|
18
|
+
K4Secret = new('k4.secret')
|
19
|
+
K4Public = new('k4.public')
|
20
|
+
K4LocalWrap = new('k4.local-wrap')
|
21
|
+
K4SecretWrap = new('k4.secret-wrap')
|
22
|
+
K4LocalPBKW = new('k4.local-pw')
|
23
|
+
K4SecretPBKW = new('k4.secret-pw')
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(input: String).returns(Interface::Key) }
|
27
|
+
def generate(input) # rubocop:disable Metrics/MethodLength
|
28
|
+
case self
|
29
|
+
in K3LocalWrap | K3LocalPBKW | K3Local if input.bytesize == 32
|
30
|
+
V3::Local.new(ikm: input)
|
31
|
+
in K3SecretWrap | K3SecretPBKW | K3Secret if input.bytesize == 48
|
32
|
+
V3::Public.from_scalar_bytes(input)
|
33
|
+
in K3Public
|
34
|
+
V3::Public.from_public_bytes(input)
|
35
|
+
in K4LocalWrap | K4LocalPBKW | K4Local if Paseto.rbnacl? && input.bytesize == 32
|
36
|
+
V4::Local.new(ikm: input)
|
37
|
+
in K4SecretWrap | K4SecretPBKW | K4Secret if Paseto.rbnacl? && input.bytesize == 64
|
38
|
+
V4::Public.from_keypair(input)
|
39
|
+
in K4Public
|
40
|
+
V4::Public.from_public_bytes(input)
|
41
|
+
else
|
42
|
+
raise InvalidKeyPair
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|