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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +549 -0
  6. data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
  7. data/lib/paseto/asn1/curve_private_key.rb +22 -0
  8. data/lib/paseto/asn1/ec_private_key.rb +27 -0
  9. data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
  10. data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
  11. data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
  12. data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
  13. data/lib/paseto/asn1/named_curve.rb +17 -0
  14. data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
  15. data/lib/paseto/asn1/private_key.rb +17 -0
  16. data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
  17. data/lib/paseto/asn1/public_key.rb +17 -0
  18. data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
  19. data/lib/paseto/asn1.rb +101 -0
  20. data/lib/paseto/asymmetric_key.rb +100 -0
  21. data/lib/paseto/configuration/box.rb +23 -0
  22. data/lib/paseto/configuration/decode_configuration.rb +68 -0
  23. data/lib/paseto/configuration.rb +18 -0
  24. data/lib/paseto/interface/i_d.rb +23 -0
  25. data/lib/paseto/interface/key.rb +113 -0
  26. data/lib/paseto/interface/pbkd.rb +83 -0
  27. data/lib/paseto/interface/pie.rb +59 -0
  28. data/lib/paseto/interface/pke.rb +86 -0
  29. data/lib/paseto/interface/serializer.rb +19 -0
  30. data/lib/paseto/interface/version.rb +161 -0
  31. data/lib/paseto/interface/wrapper.rb +20 -0
  32. data/lib/paseto/operations/i_d.rb +48 -0
  33. data/lib/paseto/operations/id/i_dv3.rb +20 -0
  34. data/lib/paseto/operations/id/i_dv4.rb +20 -0
  35. data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
  36. data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
  37. data/lib/paseto/operations/pbkw.rb +73 -0
  38. data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
  39. data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
  40. data/lib/paseto/operations/pke.rb +57 -0
  41. data/lib/paseto/operations/wrap.rb +29 -0
  42. data/lib/paseto/paserk.rb +55 -0
  43. data/lib/paseto/paserk_types.rb +46 -0
  44. data/lib/paseto/protocol/version3.rb +100 -0
  45. data/lib/paseto/protocol/version4.rb +99 -0
  46. data/lib/paseto/result.rb +9 -0
  47. data/lib/paseto/serializer/optional_json.rb +30 -0
  48. data/lib/paseto/serializer/raw.rb +23 -0
  49. data/lib/paseto/sodium/curve_25519.rb +46 -0
  50. data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
  51. data/lib/paseto/sodium/stream/base.rb +82 -0
  52. data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
  53. data/lib/paseto/sodium.rb +5 -0
  54. data/lib/paseto/symmetric_key.rb +119 -0
  55. data/lib/paseto/token.rb +127 -0
  56. data/lib/paseto/token_types.rb +29 -0
  57. data/lib/paseto/util.rb +105 -0
  58. data/lib/paseto/v3/local.rb +63 -0
  59. data/lib/paseto/v3/public.rb +204 -0
  60. data/lib/paseto/v4/local.rb +56 -0
  61. data/lib/paseto/v4/public.rb +169 -0
  62. data/lib/paseto/validator.rb +154 -0
  63. data/lib/paseto/verifiers/footer.rb +30 -0
  64. data/lib/paseto/verifiers/payload.rb +42 -0
  65. data/lib/paseto/verify.rb +48 -0
  66. data/lib/paseto/version.rb +6 -0
  67. data/lib/paseto/versions.rb +25 -0
  68. data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
  69. data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
  70. data/lib/paseto/wrappers/pie.rb +71 -0
  71. data/lib/paseto.rb +99 -0
  72. data/paseto.gemspec +58 -0
  73. data/sorbet/config +3 -0
  74. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  75. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  76. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  77. data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
  78. data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
  79. data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
  80. data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
  81. data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
  82. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
  83. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  84. data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
  85. data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
  86. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  87. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
  88. data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
  89. data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
  90. data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
  91. data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
  92. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  93. data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
  94. data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
  95. data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
  96. data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
  97. data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
  98. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
  99. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
  100. data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
  101. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
  102. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  103. data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
  104. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
  105. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
  106. data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
  107. data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
  108. data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
  109. data/sorbet/rbi/shims/multi_json.rbi +19 -0
  110. data/sorbet/rbi/shims/openssl.rbi +111 -0
  111. data/sorbet/rbi/shims/rbnacl.rbi +65 -0
  112. data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
  113. data/sorbet/rbi/todo.rbi +7 -0
  114. data/sorbet/tapioca/config.yml +30 -0
  115. data/sorbet/tapioca/require.rb +12 -0
  116. 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,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Paseto
5
+ class Result < T::Struct
6
+ prop :claims, T::Hash[String, T.untyped]
7
+ prop :footer, T.nilable(T.any(String, T::Hash[String, T.untyped])), default: nil
8
+ end
9
+ 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,5 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'sodium/stream/base'
5
+ require_relative 'sodium/stream/x_cha_cha20_xor'
@@ -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
@@ -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