Class: Pedicel::EC

Inherits:
Base
  • Object
show all
Defined in:
lib/pedicel/ec.rb

Constant Summary

Constants inherited from Base

Base::SUPPORTED_VERSIONS

Instance Attribute Summary

Attributes inherited from Base

#config

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#application_data, #decrypt_aes, #encrypted_data, extract_certificates, #initialize, #private_key_class, #signature, #symmetric_algorithm, #transaction_id, #valid_signature?, verify_root_certificate, #verify_signature, verify_signed_time, verify_x509_chain, #version

Constructor Details

This class inherits a constructor from Pedicel::Base

Class Method Details

.merchant_id(certificate:, mid_oid: ) ⇒ Object

Raises:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/pedicel/ec.rb', line 116

def self.merchant_id(certificate:, mid_oid: Pedicel::DEFAULT_CONFIG[:oid_merchant_identifier_field])
  begin
    cert = OpenSSL::X509::Certificate.new(certificate)
  rescue => e
    raise CertificateError, "invalid PEM format of certificate: #{e.message}"
  end

  merchant_id_hex =
    cert
    .extensions
    .find { |x| x.oid == mid_oid }
    &.value # Hex encoded Merchant ID plus perhaps extra non-hex chars.
    &.delete('^[0-9a-fA-F]') # Remove non-hex chars.

  raise CertificateError, 'no merchant identifier in certificate' unless merchant_id_hex

  [merchant_id_hex].pack('H*')
end

.symmetric_key(merchant_id:, shared_secret:) ⇒ Object

Raises:

  • (ArgumentError)


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/pedicel/ec.rb', line 71

def self.symmetric_key(merchant_id:, shared_secret:)
  raise ArgumentError, 'merchant_id must be a SHA256' unless merchant_id.is_a?(String) && merchant_id.length == 32
  raise ArgumentError, 'shared_secret must be a string' unless shared_secret.is_a?(String)

  # http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf
  # Section 5.8.1.1, The Single-Step KDF Specification.
  #
  # With slight adjustments:
  # > 1. Set `reps = ceil(keydatalen/hashlen)`
  # > 2. If `reps > (2^32 - 1)`, then return an error indicator without
  # >    performing the remaining actions.
  # > 3. Initialize a 32-bit, big-endian bit string `counter` as 00000001 base
  # >    16 (i.e. 0x00000001).
  # > 4. If `counter || Z || OtherInfo` is more than `max_H_inputlen` bits
  # >    long, then return an error indicator without performing the remaining
  # >    actions.
  # > 5. For `i = 1` to `reps` by `1`, do the following:
  # >      5.1  Compute `K(i) = H(counter || Z || OtherInfo)`.
  # >      5.2  Increment `counter` (modulo `2^32`), treating it as an
  # >           unsigned 32-bit integer.
  # > 6. Let `K_Last` be set to `K(reps)` if `keydatalen / hashlen` is an
  # >    integer; otherwise, let `K_Last` be set to the `keydatalen mod
  # >    hashlen` leftmost bits of `K(reps)`.
  # > 7. Return `K(1) || K(2) || ... || K(reps-1) || K_Last`.
  #
  # Digest::SHA256 will do the calculations when we throw Z and OtherInfo
  # into the digest.

  sha256 = Digest::SHA256.new

  # Step 3
  sha256 << "\x00\x00\x00\x01"

  # Z
  sha256 << shared_secret

  # OtherInfo
  # https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html
  sha256 << "\x0d" + 'id-aes256-GCM' # AlgorithmID
  sha256 << 'Apple' # PartyUInfo
  sha256 << merchant_id # PartyVInfo

  sha256.digest
end

Instance Method Details

#decrypt(symmetric_key: nil, merchant_id: nil, certificate: nil, private_key: nil, ca_certificate_pem: @config[:trusted_ca_pem], now: Time.now) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/pedicel/ec.rb', line 11

def decrypt(symmetric_key: nil, merchant_id: nil, certificate: nil, private_key: nil,
            ca_certificate_pem: @config[:trusted_ca_pem], now: Time.now)
  # Check for necessary parameters.
  unless symmetric_key || ((merchant_id || certificate) && private_key)
    raise ArgumentError, 'missing parameters'
  end

  # Check for uniqueness among the supplied parameters used directly here.
  if symmetric_key && (merchant_id || certificate || private_key)
    raise ArgumentError, "leave out other parameters when supplying 'symmetric_key'"
  end

  verify_signature(ca_certificate_pem: ca_certificate_pem, now: now)

  symmetric_key ||= symmetric_key(private_key: private_key,
                                  merchant_id: merchant_id,
                                  certificate: certificate)

  decrypt_aes(key: symmetric_key)
end

#ephemeral_public_keyObject



5
6
7
8
9
# File 'lib/pedicel/ec.rb', line 5

def ephemeral_public_key
  @token[:header].transform_keys!(&:to_sym)

  Base64.decode64(@token[:header][:ephemeralPublicKey])
end

#shared_secret(private_key:) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pedicel/ec.rb', line 50

def shared_secret(private_key:)
  begin
    privkey = OpenSSL::PKey::EC.new(private_key)
  rescue => e
    raise EcKeyError, "invalid PEM format of private key for EC: #{e.message}"
  end

  begin
    pubkey = OpenSSL::PKey::EC.new(ephemeral_public_key).public_key
  rescue => e
    raise EcKeyError, "invalid ephemeralPublicKey (from token) for EC: #{e.message}"
  end

  unless privkey.group == pubkey.group
    raise EcKeyError, "private_key curve '%s' differs from token ephemeralPublicKey curve '%s'" %
                      [privkey.group.curve_name, pubkey.group.curve_name]
  end

  privkey.dh_compute_key(pubkey)
end

#symmetric_key(private_key: nil, merchant_id: nil, certificate: nil) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/pedicel/ec.rb', line 32

def symmetric_key(private_key: nil, merchant_id: nil, certificate: nil)
  # Check for necessary parameters.
  unless private_key && (merchant_id || certificate)
    raise ArgumentError, 'missing parameters'
  end

  # Check for uniqueness among the supplied parameters.
  if merchant_id && certificate
    raise ArgumentError, "leave out 'certificate' when supplying 'merchant_id'"
  end

  shared_secret = shared_secret(private_key: private_key)

  merchant_id ||= self.class.merchant_id(certificate: certificate, mid_oid: @config[:oid_merchant_identifier_field])

  self.class.symmetric_key(shared_secret: shared_secret, merchant_id: merchant_id)
end