ruby-paseto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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,100 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Protocol
|
|
7
|
+
class Version3
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
include Interface::Version
|
|
12
|
+
|
|
13
|
+
sig(:final) { override.params(key: String, nonce: String, payload: String).returns(String) }
|
|
14
|
+
def self.crypt(key:, nonce:, payload:)
|
|
15
|
+
cipher = OpenSSL::Cipher.new('aes-256-ctr')
|
|
16
|
+
cipher.key = key
|
|
17
|
+
cipher.iv = nonce
|
|
18
|
+
cipher.update(payload) + cipher.final
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig(:final) { override.params(data: String, digest_size: Integer).returns(String) }
|
|
22
|
+
def self.digest(data, digest_size:)
|
|
23
|
+
T.must(OpenSSL::Digest.digest('SHA384', data).byteslice(0, digest_size))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig(:final) { override.returns(Integer) }
|
|
27
|
+
def self.digest_bytes
|
|
28
|
+
48
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig(:final) { override.params(data: String, key: String, digest_size: Integer).returns(String) }
|
|
32
|
+
def self.hmac(data, key:, digest_size:)
|
|
33
|
+
T.must(OpenSSL::HMAC.digest('SHA384', key, data).byteslice(0, digest_size))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig(:final) { override.returns(T.class_of(Operations::ID::IDv3)) }
|
|
37
|
+
def self.id
|
|
38
|
+
Operations::ID::IDv3
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig(:final) do
|
|
42
|
+
override.params(
|
|
43
|
+
password: String,
|
|
44
|
+
salt: String,
|
|
45
|
+
length: Integer,
|
|
46
|
+
parameters: Integer
|
|
47
|
+
).returns(String)
|
|
48
|
+
end
|
|
49
|
+
def self.kdf(password, salt:, length:, **parameters)
|
|
50
|
+
OpenSSL::KDF.pbkdf2_hmac(
|
|
51
|
+
password,
|
|
52
|
+
salt: salt,
|
|
53
|
+
length: length,
|
|
54
|
+
iterations: T.must(parameters[:iterations]),
|
|
55
|
+
hash: 'SHA384'
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig(:final) { override.returns(String) }
|
|
60
|
+
def self.paserk_version
|
|
61
|
+
'k3'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig(:final) { override.returns(String) }
|
|
65
|
+
def self.pbkd_local_header
|
|
66
|
+
'k3.local-pw'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig(:final) { override.returns(String) }
|
|
70
|
+
def self.pbkd_secret_header
|
|
71
|
+
'k3.secret-pw'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig(:final) { override.params(password: String).returns(Operations::PBKD::PBKDv3) }
|
|
75
|
+
def self.pbkw(password)
|
|
76
|
+
Operations::PBKD::PBKDv3.new(password)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig(:final) { override.params(key: SymmetricKey).returns(Wrappers::PIE::PieV3) }
|
|
80
|
+
def self.pie(key)
|
|
81
|
+
Wrappers::PIE::PieV3.new(key)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig(:final) { override.params(key: AsymmetricKey).returns(Operations::PKE::PKEv3) }
|
|
85
|
+
def self.pke(key)
|
|
86
|
+
Operations::PKE::PKEv3.new(key)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig(:final) { override.params(size: Integer).returns(String) }
|
|
90
|
+
def self.random(size)
|
|
91
|
+
SecureRandom.random_bytes(size)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig(:final) { override.returns(String) }
|
|
95
|
+
def self.version
|
|
96
|
+
'v3'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Protocol
|
|
6
|
+
class Version4
|
|
7
|
+
extend T::Sig
|
|
8
|
+
extend T::Helpers
|
|
9
|
+
|
|
10
|
+
include Interface::Version
|
|
11
|
+
|
|
12
|
+
sig(:final) { override.params(key: String, nonce: String, payload: String).returns(String) }
|
|
13
|
+
def self.crypt(key:, nonce:, payload:)
|
|
14
|
+
Paseto::Sodium::Stream::XChaCha20Xor.new(key).encrypt(nonce, payload)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig(:final) { override.params(data: String, digest_size: Integer).returns(String) }
|
|
18
|
+
def self.digest(data, digest_size:)
|
|
19
|
+
RbNaCl::Hash.blake2b(data, digest_size: digest_size)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig(:final) { override.returns(Integer) }
|
|
23
|
+
def self.digest_bytes
|
|
24
|
+
32
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig(:final) { override.params(data: String, key: String, digest_size: Integer).returns(String) }
|
|
28
|
+
def self.hmac(data, key:, digest_size: 32)
|
|
29
|
+
RbNaCl::Hash.blake2b(data, key: key, digest_size: digest_size)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig(:final) { override.returns(T.class_of(Operations::ID::IDv4)) }
|
|
33
|
+
def self.id
|
|
34
|
+
Operations::ID::IDv4
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig(:final) do
|
|
38
|
+
override.params(
|
|
39
|
+
password: String,
|
|
40
|
+
salt: String,
|
|
41
|
+
length: Integer,
|
|
42
|
+
parameters: T.any(Symbol, Integer)
|
|
43
|
+
).returns(String)
|
|
44
|
+
end
|
|
45
|
+
def self.kdf(password, salt:, length:, **parameters)
|
|
46
|
+
memlimit = RbNaCl::PasswordHash::Argon2.memlimit_value(parameters[:memlimit])
|
|
47
|
+
opslimit = RbNaCl::PasswordHash::Argon2.opslimit_value(parameters[:opslimit])
|
|
48
|
+
|
|
49
|
+
RbNaCl::PasswordHash.argon2id(
|
|
50
|
+
password,
|
|
51
|
+
salt,
|
|
52
|
+
opslimit,
|
|
53
|
+
memlimit,
|
|
54
|
+
length
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig(:final) { override.returns(String) }
|
|
59
|
+
def self.paserk_version
|
|
60
|
+
'k4'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig(:final) { override.returns(String) }
|
|
64
|
+
def self.pbkd_local_header
|
|
65
|
+
'k4.local-pw'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig(:final) { override.returns(String) }
|
|
69
|
+
def self.pbkd_secret_header
|
|
70
|
+
'k4.secret-pw'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig(:final) { override.params(password: String).returns(Operations::PBKD::PBKDv4) }
|
|
74
|
+
def self.pbkw(password)
|
|
75
|
+
Operations::PBKD::PBKDv4.new(password)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig(:final) { override.params(key: SymmetricKey).returns(Wrappers::PIE::PieV4) }
|
|
79
|
+
def self.pie(key)
|
|
80
|
+
Wrappers::PIE::PieV4.new(key)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig(:final) { override.params(key: AsymmetricKey).returns(Operations::PKE::PKEv4) }
|
|
84
|
+
def self.pke(key)
|
|
85
|
+
Operations::PKE::PKEv4.new(key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig(:final) { override.params(size: Integer).returns(String) }
|
|
89
|
+
def self.random(size)
|
|
90
|
+
RbNaCl::Random.random_bytes(size)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
sig(:final) { override.returns(String) }
|
|
94
|
+
def self.version
|
|
95
|
+
'v4'
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Serializer
|
|
6
|
+
module OptionalJson
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
extend Interface::Serializer
|
|
10
|
+
|
|
11
|
+
sig { override.params(val: String, options: T::Hash[T.untyped, T.untyped]).returns(T.untyped) }
|
|
12
|
+
def self.deserialize(val, options)
|
|
13
|
+
obj = MultiJson.load(val, options)
|
|
14
|
+
case obj
|
|
15
|
+
when Hash then obj
|
|
16
|
+
else val
|
|
17
|
+
end
|
|
18
|
+
rescue MultiJson::ParseError
|
|
19
|
+
val
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(val: T.untyped, options: T::Hash[T.untyped, T.untyped]).returns(String) }
|
|
23
|
+
def self.serialize(val, options)
|
|
24
|
+
return val unless val.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
MultiJson.dump(val, options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Serializer
|
|
6
|
+
module Raw
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
extend Interface::Serializer
|
|
10
|
+
|
|
11
|
+
sig(:final) do
|
|
12
|
+
override.params(
|
|
13
|
+
val: String,
|
|
14
|
+
_options: T::Hash[T.untyped, T.untyped]
|
|
15
|
+
).returns(T.any(String, T::Hash[String, T.untyped]))
|
|
16
|
+
end
|
|
17
|
+
def self.deserialize(val, _options) = val
|
|
18
|
+
|
|
19
|
+
sig(:final) { override.params(val: T.untyped, _options: T.untyped).returns(String) }
|
|
20
|
+
def self.serialize(val, _options) = val
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Sodium
|
|
6
|
+
class Curve25519
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
extend RbNaCl::Sodium
|
|
10
|
+
|
|
11
|
+
sodium_type :sign
|
|
12
|
+
sodium_primitive :ed25519
|
|
13
|
+
|
|
14
|
+
sodium_function :to_x25519_private_key,
|
|
15
|
+
:crypto_sign_ed25519_sk_to_curve25519,
|
|
16
|
+
%i[pointer pointer]
|
|
17
|
+
|
|
18
|
+
sodium_function :to_x25519_public_key,
|
|
19
|
+
:crypto_sign_ed25519_pk_to_curve25519,
|
|
20
|
+
%i[pointer pointer]
|
|
21
|
+
|
|
22
|
+
sig { params(key: V4::Public).void }
|
|
23
|
+
def initialize(key)
|
|
24
|
+
@key = key
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(RbNaCl::PrivateKey) }
|
|
28
|
+
def to_x25519_private_key
|
|
29
|
+
buffer = RbNaCl::Util.zeros(RbNaCl::PrivateKey::BYTES)
|
|
30
|
+
success = self.class.to_x25519_private_key(buffer, @key.to_bytes)
|
|
31
|
+
raise CryptoError, 'Ed25519->X25519 sk failure' unless success
|
|
32
|
+
|
|
33
|
+
RbNaCl::PrivateKey.new(buffer)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(RbNaCl::PublicKey) }
|
|
37
|
+
def to_x25519_public_key
|
|
38
|
+
buffer = RbNaCl::Util.zeros(RbNaCl::PublicKey::BYTES)
|
|
39
|
+
success = self.class.to_x25519_public_key(buffer, @key.public_bytes)
|
|
40
|
+
raise CryptoError, 'Ed25519->X25519 pk failure' unless success
|
|
41
|
+
|
|
42
|
+
RbNaCl::PublicKey.new(buffer)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
module Sodium
|
|
6
|
+
module SafeEd25519Loader
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
include Kernel
|
|
10
|
+
|
|
11
|
+
sig(:final) { params(keypair: String).returns(RbNaCl::SigningKey) }
|
|
12
|
+
def self.from_keypair(keypair)
|
|
13
|
+
RbNaCl::SigningKey.new(keypair[0, 32]).tap do |key|
|
|
14
|
+
raise InvalidKeyPair, 'public key does not match private' unless keypair == key.keypair_bytes
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Sodium
|
|
7
|
+
module Stream
|
|
8
|
+
# Abstract base class for Stream ciphers
|
|
9
|
+
class Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
extend T::Helpers
|
|
12
|
+
|
|
13
|
+
abstract!
|
|
14
|
+
|
|
15
|
+
# Number of bytes in a valid key
|
|
16
|
+
KEYBYTES = 0
|
|
17
|
+
|
|
18
|
+
# Number of bytes in a valid nonce
|
|
19
|
+
NONCEBYTES = 0
|
|
20
|
+
|
|
21
|
+
MESSAGEBYTES_MAX = 0
|
|
22
|
+
|
|
23
|
+
sig { returns(Integer) }
|
|
24
|
+
def self.nonce_bytes
|
|
25
|
+
const_get(:NONCEBYTES)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { returns(Integer) }
|
|
29
|
+
def self.key_bytes
|
|
30
|
+
const_get(:KEYBYTES)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Create a new Stream.
|
|
34
|
+
#
|
|
35
|
+
# Sets up Stream with a secret key for encrypting and decrypting messages.
|
|
36
|
+
sig { params(key: String).void }
|
|
37
|
+
def initialize(key)
|
|
38
|
+
RbNaCl::Util.check_length(key, key_bytes, 'Key')
|
|
39
|
+
@key = key
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(nonce: String, message: T.nilable(String)).returns(String) }
|
|
43
|
+
def encrypt(nonce, message)
|
|
44
|
+
RbNaCl::Util.check_length(nonce, nonce_bytes, 'Nonce')
|
|
45
|
+
|
|
46
|
+
ciphertext = RbNaCl::Util.zeros(data_len(message))
|
|
47
|
+
|
|
48
|
+
success = do_encrypt(ciphertext, nonce, message)
|
|
49
|
+
raise CryptoError, 'Encryption failed' unless success
|
|
50
|
+
|
|
51
|
+
ciphertext
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { returns(Integer) }
|
|
55
|
+
def nonce_bytes
|
|
56
|
+
self.class.nonce_bytes
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { returns(Integer) }
|
|
60
|
+
def key_bytes
|
|
61
|
+
self.class.key_bytes
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Symmetric encryption key for a cipher instance
|
|
67
|
+
sig { returns(String) }
|
|
68
|
+
attr_reader :key
|
|
69
|
+
|
|
70
|
+
sig { abstract.params(ciphertext: String, nonce: String, message: T.nilable(String)).returns(T::Boolean) }
|
|
71
|
+
def do_encrypt(ciphertext, nonce, message); end
|
|
72
|
+
|
|
73
|
+
sig { params(message: T.nilable(String)).returns(Integer) }
|
|
74
|
+
def data_len(message)
|
|
75
|
+
return 0 unless message
|
|
76
|
+
|
|
77
|
+
message.bytesize
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
# typed: false
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Paseto
|
|
6
|
+
module Sodium
|
|
7
|
+
module Stream
|
|
8
|
+
class XChaCha20Xor < Paseto::Sodium::Stream::Base
|
|
9
|
+
extend RbNaCl::Sodium
|
|
10
|
+
sodium_type :stream
|
|
11
|
+
|
|
12
|
+
sodium_primitive :xchacha20
|
|
13
|
+
|
|
14
|
+
sodium_constant :KEYBYTES
|
|
15
|
+
sodium_constant :NONCEBYTES
|
|
16
|
+
sodium_constant :MESSAGEBYTES_MAX
|
|
17
|
+
|
|
18
|
+
sodium_function :stream_xchacha20_xor,
|
|
19
|
+
:crypto_stream_xchacha20_xor,
|
|
20
|
+
%i[pointer pointer ulong_long pointer pointer]
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
sig { override.params(ciphertext: String, nonce: String, message: T.nilable(String)).returns(T::Boolean) }
|
|
25
|
+
def do_encrypt(ciphertext, nonce, message)
|
|
26
|
+
self.class.stream_xchacha20_xor(ciphertext, message, data_len(message), nonce, key)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
class SymmetricKey < Interface::Key
|
|
6
|
+
extend T::Sig
|
|
7
|
+
extend T::Helpers
|
|
8
|
+
|
|
9
|
+
abstract!
|
|
10
|
+
|
|
11
|
+
sig(:final) { returns(String) }
|
|
12
|
+
attr_reader :key, :lid, :paserk
|
|
13
|
+
|
|
14
|
+
sig { params(ikm: String).void }
|
|
15
|
+
def initialize(ikm)
|
|
16
|
+
raise ArgumentError, 'ikm must be 32 bytes' unless ikm.bytesize == 32
|
|
17
|
+
|
|
18
|
+
@key = T.let(ikm.freeze, String)
|
|
19
|
+
@paserk = T.let("#{paserk_version}.#{purpose}.#{Util.encode64(key)}".freeze, String)
|
|
20
|
+
@lid = T.let(Operations::ID.lid(self).freeze, String)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Encrypts and authenticates `message` with optional binding input `implicit_assertion`, returning a `Token`.
|
|
24
|
+
# If `footer` is provided, it is included as authenticated data in the reuslting `Token``.
|
|
25
|
+
# `n` must not be used outside of tests.
|
|
26
|
+
sig(:final) { params(message: String, footer: String, implicit_assertion: String, n: T.nilable(String)).returns(Token) }
|
|
27
|
+
def encrypt(message:, footer: '', implicit_assertion: '', n: nil) # rubocop:disable Naming/MethodParameterName
|
|
28
|
+
n ||= SecureRandom.random_bytes(32)
|
|
29
|
+
|
|
30
|
+
ek, n2, ak = calc_keys(n)
|
|
31
|
+
|
|
32
|
+
c = protocol.crypt(payload: message, key: ek, nonce: n2)
|
|
33
|
+
|
|
34
|
+
Util.pre_auth_encode(pae_header, n, c, footer, implicit_assertion)
|
|
35
|
+
.then { |pre_auth| protocol.hmac(pre_auth, key: ak) }
|
|
36
|
+
.then { |t| "#{n}#{c}#{t}" }
|
|
37
|
+
.then { |payload| Token.new(payload: payload, version: version, purpose: purpose, footer: footer) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Verify and decrypt an encrypted Token, with an optional string `implicit_assertion`, and return the plaintext.
|
|
41
|
+
# If `token` includes a footer, it is treated as authenticated data to be verified but not returned.
|
|
42
|
+
# `token` must be a `v4.local` type Token.
|
|
43
|
+
sig(:final) { params(token: Token, implicit_assertion: String).returns(String) }
|
|
44
|
+
def decrypt(token:, implicit_assertion: '')
|
|
45
|
+
raise LucidityError unless header == token.header
|
|
46
|
+
|
|
47
|
+
n, c, t = split_payload(token.raw_payload)
|
|
48
|
+
|
|
49
|
+
ek, n2, ak = calc_keys(n)
|
|
50
|
+
|
|
51
|
+
pre_auth = Util.pre_auth_encode(pae_header, n, c, token.raw_footer, implicit_assertion)
|
|
52
|
+
t2 = protocol.hmac(pre_auth, key: ak)
|
|
53
|
+
raise InvalidAuthenticator unless Util.constant_compare(t, t2)
|
|
54
|
+
|
|
55
|
+
protocol.crypt(payload: c, key: ek, nonce: n2).encode(Encoding::UTF_8)
|
|
56
|
+
rescue Encoding::UndefinedConversionError
|
|
57
|
+
raise ParseError, 'invalid payload encoding'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig(:final) do
|
|
61
|
+
override.params(
|
|
62
|
+
payload: T::Hash[String, T.untyped],
|
|
63
|
+
footer: String,
|
|
64
|
+
implicit_assertion: String,
|
|
65
|
+
options: T.nilable(T.any(String, Integer, Symbol, T::Boolean))
|
|
66
|
+
).returns(String)
|
|
67
|
+
end
|
|
68
|
+
def encode!(payload, footer: '', implicit_assertion: '', **options)
|
|
69
|
+
n = T.cast(options.delete(:nonce), T.nilable(String))
|
|
70
|
+
MultiJson.dump(payload, options)
|
|
71
|
+
.then { |message| encrypt(message: message, footer: footer, implicit_assertion: implicit_assertion, n: n) }
|
|
72
|
+
.then(&:to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig(:final) do
|
|
76
|
+
override.params(
|
|
77
|
+
payload: String,
|
|
78
|
+
implicit_assertion: String,
|
|
79
|
+
options: T.nilable(T.any(Proc, String, Integer, Symbol, T::Boolean))
|
|
80
|
+
).returns(Result)
|
|
81
|
+
end
|
|
82
|
+
def decode!(payload, implicit_assertion: '', **options)
|
|
83
|
+
token = Token.parse(payload)
|
|
84
|
+
|
|
85
|
+
decrypt(token: token, implicit_assertion: implicit_assertion)
|
|
86
|
+
.then { |json| MultiJson.load(json, **options) }
|
|
87
|
+
.then { |claims| Result.new(claims: claims, footer: token.footer) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig(:final) { override.returns(String) }
|
|
91
|
+
def id = @lid
|
|
92
|
+
|
|
93
|
+
sig(:final) { override.returns(String) }
|
|
94
|
+
def pbkw_header = protocol.pbkd_local_header
|
|
95
|
+
|
|
96
|
+
sig(:final) { returns(Interface::PIE) }
|
|
97
|
+
def pie = protocol.pie(self)
|
|
98
|
+
|
|
99
|
+
sig(:final) { override.returns(String) }
|
|
100
|
+
def purpose = 'local'
|
|
101
|
+
|
|
102
|
+
sig(:final) { override.returns(String) }
|
|
103
|
+
def to_bytes = key
|
|
104
|
+
|
|
105
|
+
sig(:final) { params(paserk: String).returns(Interface::Key) }
|
|
106
|
+
def unwrap(paserk) = Paserk.from_paserk(paserk: paserk, wrapping_key: self)
|
|
107
|
+
|
|
108
|
+
sig(:final) { params(key: Interface::Key, nonce: T.nilable(String)).returns(String) }
|
|
109
|
+
def wrap(key, nonce: nil) = Paserk.wrap(key: key, wrapping_key: self, nonce: nonce)
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
sig { abstract.params(nonce: String).returns([String, String, String]) }
|
|
114
|
+
def calc_keys(nonce); end
|
|
115
|
+
|
|
116
|
+
sig { abstract.params(payload: String).returns([String, String, String]) }
|
|
117
|
+
def split_payload(payload); end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/paseto/token.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Paseto
|
|
5
|
+
class Token
|
|
6
|
+
extend T::Sig
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
sig { returns(String) }
|
|
10
|
+
attr_reader :version, :purpose, :raw_payload, :raw_footer
|
|
11
|
+
|
|
12
|
+
sig { returns(T.any(String, T::Hash[String, T.untyped])) }
|
|
13
|
+
attr_reader :footer
|
|
14
|
+
|
|
15
|
+
sig { returns(T.class_of(Interface::Key)) }
|
|
16
|
+
attr_reader :type
|
|
17
|
+
|
|
18
|
+
sig do
|
|
19
|
+
params(
|
|
20
|
+
paseto: String,
|
|
21
|
+
options: T.nilable(T.any(Proc, String, Integer, Symbol, T::Boolean))
|
|
22
|
+
).returns(Token)
|
|
23
|
+
end
|
|
24
|
+
def self.parse(paseto, **options)
|
|
25
|
+
case paseto.split('.')
|
|
26
|
+
in [String => version, String => purpose, String => payload, String => footer]
|
|
27
|
+
nil
|
|
28
|
+
in [String => version, String => purpose, String => payload]
|
|
29
|
+
footer = ''
|
|
30
|
+
else
|
|
31
|
+
raise ParseError, 'not a valid token'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
payload = Util.decode64(payload)
|
|
35
|
+
Util.decode64(footer)
|
|
36
|
+
.then { |f| serializer.deserialize(f, options) }
|
|
37
|
+
.then { |f| new(version: version, purpose: purpose, payload: payload, footer: f) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(Paseto::Interface::Serializer) }
|
|
41
|
+
def self.serializer
|
|
42
|
+
Paseto.config.decode.footer_serializer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig do
|
|
46
|
+
params(
|
|
47
|
+
payload: String,
|
|
48
|
+
purpose: String,
|
|
49
|
+
version: String,
|
|
50
|
+
footer: T.any(String, T::Hash[String, T.untyped]),
|
|
51
|
+
options: T.nilable(T.any(Proc, String, Integer, Symbol, T::Boolean))
|
|
52
|
+
).void
|
|
53
|
+
end
|
|
54
|
+
def initialize(payload:, purpose:, version:, footer: '', **options) # rubocop:disable Metrics/AbcSize
|
|
55
|
+
raw_footer = serializer.serialize(footer, options)
|
|
56
|
+
encoded_footer = ".#{Util.encode64(raw_footer)}" unless raw_footer.empty?
|
|
57
|
+
|
|
58
|
+
paseto = Util.encode64(payload)
|
|
59
|
+
.then { |data| "#{data}#{encoded_footer}" }
|
|
60
|
+
.then { |data| "#{version}.#{purpose}.#{data}" }
|
|
61
|
+
.then(&:freeze)
|
|
62
|
+
|
|
63
|
+
@version = T.let(version.freeze, String)
|
|
64
|
+
@purpose = T.let(purpose.freeze, String)
|
|
65
|
+
@raw_payload = T.let(payload.freeze, String)
|
|
66
|
+
@type = T.let(validate_header, T.class_of(Interface::Key))
|
|
67
|
+
@footer = T.let(footer, T.any(String, T::Hash[String, T.untyped]))
|
|
68
|
+
@raw_footer = T.let(raw_footer, String)
|
|
69
|
+
@str = T.let(paseto, String)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig do
|
|
73
|
+
params(
|
|
74
|
+
key: Interface::Key,
|
|
75
|
+
implicit_assertion: String,
|
|
76
|
+
options: T.nilable(T.any(Proc, String, Integer, Symbol, T::Boolean))
|
|
77
|
+
).returns(T::Hash[String, T.untyped])
|
|
78
|
+
end
|
|
79
|
+
def decode!(key, implicit_assertion: '', **options)
|
|
80
|
+
return @result.claims if @result
|
|
81
|
+
|
|
82
|
+
key.decode(@str, implicit_assertion: implicit_assertion, **options)
|
|
83
|
+
.then { |result| @result = T.let(result, T.nilable(Result)) }
|
|
84
|
+
.then(&:claims)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { returns(String) }
|
|
88
|
+
def header
|
|
89
|
+
"#{version}.#{purpose}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { returns(String) }
|
|
93
|
+
def inspect
|
|
94
|
+
to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
98
|
+
def payload
|
|
99
|
+
return @result.claims if @result
|
|
100
|
+
|
|
101
|
+
raise ParseError, 'token not yet decoded, call #decode! first'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig { returns(String) }
|
|
105
|
+
def to_s = @str
|
|
106
|
+
|
|
107
|
+
sig { params(other: T.any(Token, String)).returns(T.nilable(Integer)) }
|
|
108
|
+
def <=>(other)
|
|
109
|
+
to_s <=> other.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
sig { returns(Paseto::Interface::Serializer) }
|
|
115
|
+
def serializer = self.class.serializer
|
|
116
|
+
|
|
117
|
+
sig { returns(T.class_of(Interface::Key)) }
|
|
118
|
+
def validate_header
|
|
119
|
+
type = TokenTypes.deserialize(header).key_klass
|
|
120
|
+
return type if type
|
|
121
|
+
|
|
122
|
+
raise UnsupportedToken, header
|
|
123
|
+
rescue KeyError
|
|
124
|
+
raise UnsupportedToken, header
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|