ruby-paseto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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