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,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
|