• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payrollhero / webhook_system / #911

23 Apr 2025 10:22AM UTC coverage: 8.615% (-79.8%) from 88.393%
#911

push

web-flow
Merge f6cb66e39 into 523bc8de4

2 of 3 new or added lines in 2 files covered. (66.67%)

270 existing lines in 6 files now uncovered.

28 of 325 relevant lines covered (8.62%)

0.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/webhook_system/encoder.rb
1
# frozen_string_literal: true
2

UNCOV
3
require 'base64'
×
4

UNCOV
5
module WebhookSystem
×
6

7
  # Class in charge of encoding and decoding encrypted payload
UNCOV
8
  module Encoder
×
9
    # Given a secret string, encode the passed payload to json
10
    # encrypt it, base64 encode that, and wrap it in its own json wrapper
11
    #
12
    # @param [String] secret_string some secret string
13
    # @param [Object#to_json] payload Any object that responds to to_json
14
    # @return [String] The encoded string payload (its a JSON string)
UNCOV
15
    def self.encode(secret_string, payload, format:)
×
UNCOV
16
      response_hash = Payload.encode(payload, secret: secret_string, format: format)
×
UNCOV
17
      payload_string = JSON.generate(response_hash)
×
UNCOV
18
      signature = hub_signature(payload_string, secret_string)
×
UNCOV
19
      [payload_string, { 'X-Hub-Signature' => signature, 'Content-Type' => content_type_for_format(format) }]
×
UNCOV
20
    end
×
21

22
    # Given a secret string, and an encrypted payload, unwrap it, bas64 decode it
23
    # decrypt it, and JSON decode it
24
    #
25
    # @param [String] secret_string some secret string
26
    # @param [String] payload_string String as returned from #encode
27
    # @return [Object] return the JSON decode of the encrypted payload
UNCOV
28
    def self.decode(secret_string, payload_string, headers = {})
×
UNCOV
29
      signature = headers['X-Hub-Signature']
×
UNCOV
30
      format = format_for_content_type(headers.fetch('Content-Type'))
×
31

UNCOV
32
      payload_signature = hub_signature(payload_string, secret_string)
×
UNCOV
33
      raise DecodingError, 'signature mismatch' if signature && signature != payload_signature
×
34

UNCOV
35
      Payload.decode(payload_string, secret: secret_string, format: format)
×
UNCOV
36
    end
×
37

UNCOV
38
    class << self
×
UNCOV
39
      private
×
40

UNCOV
41
      def content_type_format_map
×
UNCOV
42
        {
×
UNCOV
43
          'base64+aes256' => 'application/json; base64+aes256',
×
UNCOV
44
          'json' => 'application/json',
×
UNCOV
45
        }
×
UNCOV
46
      end
×
47

UNCOV
48
      def format_for_content_type(content_type)
×
UNCOV
49
        content_type_format_map.invert.fetch(content_type)
×
UNCOV
50
      end
×
51

UNCOV
52
      def content_type_for_format(format)
×
UNCOV
53
        content_type_format_map.fetch(format)
×
UNCOV
54
      end
×
55

UNCOV
56
      def hub_signature(payload_string, secret)
×
UNCOV
57
        "sha1=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, payload_string)}"
×
UNCOV
58
      end
×
UNCOV
59
    end
×
UNCOV
60
  end
×
61

UNCOV
62
  module Payload
×
UNCOV
63
    class << self
×
UNCOV
64
      def encode(payload, secret:, format:)
×
UNCOV
65
        case format
×
UNCOV
66
        when 'base64+aes256'
×
UNCOV
67
          encode_aes(payload, secret)
×
UNCOV
68
        when 'json'
×
UNCOV
69
          payload
×
UNCOV
70
        else
×
71
          raise ArgumentError, "don't know how to handle: #{payload['format']} payload"
×
UNCOV
72
        end
×
UNCOV
73
      end
×
74

UNCOV
75
      def decode(response_body, secret:, format:)
×
UNCOV
76
        payload = JSON.parse(response_body)
×
77

UNCOV
78
        case format
×
UNCOV
79
        when 'base64+aes256'
×
UNCOV
80
          decode_aes(payload, secret)
×
UNCOV
81
        when 'json'
×
UNCOV
82
          payload
×
UNCOV
83
        else
×
84
          raise ArgumentError, "don't know how to handle: #{payload['format']} payload"
×
UNCOV
85
        end
×
UNCOV
86
      end
×
87

UNCOV
88
      private
×
89

UNCOV
90
      def encode_aes(payload, secret)
×
UNCOV
91
        cipher = OpenSSL::Cipher.new('aes-256-cbc')
×
UNCOV
92
        cipher.encrypt
×
UNCOV
93
        iv = cipher.random_iv
×
UNCOV
94
        cipher.key = key_from_secret(iv, secret)
×
UNCOV
95
        encoded = cipher.update(payload.to_json) + cipher.final
×
96

UNCOV
97
        {
×
UNCOV
98
          format: 'base64+aes256',
×
UNCOV
99
          payload: Base64.encode64(encoded),
×
UNCOV
100
          iv: Base64.encode64(iv),
×
UNCOV
101
        }
×
UNCOV
102
      end
×
103

UNCOV
104
      def decode_aes(payload, secret)
×
UNCOV
105
        encoded = Base64.decode64(payload['payload'])
×
UNCOV
106
        iv = Base64.decode64(payload['iv'])
×
107

UNCOV
108
        cipher = OpenSSL::Cipher.new('aes-256-cbc')
×
UNCOV
109
        cipher.decrypt
×
UNCOV
110
        cipher.iv = iv
×
UNCOV
111
        cipher.key = key_from_secret(iv, secret)
×
UNCOV
112
        decoded = cipher.update(encoded) + cipher.final
×
113

UNCOV
114
        JSON.parse(decoded)
×
UNCOV
115
      rescue OpenSSL::Cipher::CipherError
×
116
        raise DecodingError, 'Decoding Failed, probably mismatched secret'
×
UNCOV
117
      end
×
118

UNCOV
119
      def key_from_secret(iv, secret_string)
×
UNCOV
120
        OpenSSL::PKCS5.pbkdf2_hmac(secret_string, iv, 100_000, 256 / 8, 'SHA256')
×
UNCOV
121
      end
×
UNCOV
122
    end
×
UNCOV
123
  end
×
UNCOV
124
end
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc