atpay_tokens 0.0.7

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 629a0137006e92782fffe3b0012381272b1f9dbf
4
+ data.tar.gz: d06442cc7083d78c21d230b1494026404b63381a
5
+ SHA512:
6
+ metadata.gz: 58779323c57e6b2ca635d38a69cf6748912dc890c788724e559bcb9ae2ccb8132933a44d80c795b8d8c6e466c80d837be0c56469ed190458e647e79bd99c5621
7
+ data.tar.gz: a187a35f082ce3f9a7a96bb656dc24f8af3345427f88f428491ab43c3735fef64dfa4b9935478622e2c4ed9051742d6a7e89004ff66c9a82358f37ca90e31f9d
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.un~
2
+ .bundle
3
+ /vendor
4
+ coverage
5
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+
3
+ before_script: "LD_LIBRARY_PATH=lib bundle exec rake ci"
4
+ script: "LD_LIBRARY_PATH=lib bundle exec rspec spec"
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rspec'
6
+ gem 'rspec-mocks'
7
+ gem 'rbnacl', :github => "cryptosphere/rbnacl", :ref => "907bf0c07dc058ad0efbdef47ebc431444c2f696"
8
+ gem 'ruby-prof'
9
+ gem 'simplecov', :require => false, :group => :test
10
+ gem 'bundler'
11
+ gem 'rake'
12
+ gem 'coveralls', require: false
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @Pay API Client
2
+
3
+ [![Build Status](https://travis-ci.org/atpay/atpay-client.png)](https://travis-ci.org/atpay/atpay-client)
4
+
5
+
6
+ Client interface for the @Pay API and key generation for
7
+ performance optimization. This library is designed for advanced
8
+ implementation of the @Pay API, with the primary purpose
9
+ of enhancing performance for high-traffic, heavily utilized
10
+ platforms.
11
+
12
+ Interfaces here are implemented after receiving OAuth 2.0
13
+ privileges for the partner/user record. You cannot authenticate
14
+ directly to the API with this library at this moment.
15
+
16
+ ## Installation
17
+
18
+ Add the 'atpay-client' gem to your Gemfile:
19
+
20
+ ```ruby
21
+ #Gemfile
22
+
23
+ gem 'atpay', :github => "atpay/atpay-client"
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ With the `keys` scope, authenticate with OAuth, and make a request
29
+ to the 'keys' endpoint (see the api documentation at
30
+ https://developer.atpay.com) to receive the partner_id,
31
+ public key and private key.
32
+
33
+ Apply these values to your configuration
34
+
35
+ ```ruby
36
+ session = AtPay::Session.new({
37
+ :environment => :sandbox, # Either :sandbox or :production
38
+ :partner_id => 1234, # Integer value partner id
39
+ :public_key => "XXX", # Provided public key
40
+ :private_key => "YYY" # Provided private key
41
+ })
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ In order for an @Pay user to make a purchase via email, they'll
47
+ need to send a specially crafted key to @Pay's address. You can
48
+ either use the OAuth API endpoints to generate buttons and keys,
49
+ or you can generate the keys yourself. In a high traffic
50
+ environment you'll want to generate keys locally.
51
+
52
+ Let's assume you have a member with an @Pay account, and you
53
+ would like to include a $20.00 purchase in an email to them:
54
+
55
+ ```ruby
56
+ @key = session.security_key(:amount => 20.00, :email => "test@example.com")
57
+ ```
58
+
59
+ Now, include the `@key` in a mailto link within the email
60
+ addressed to transaction@payments.atpay.com. Your user will
61
+ make the purchase by clicking the mailto and sending the
62
+ email.
63
+
64
+ ## Expiration
65
+
66
+ Keys will expire by default after two weeks. To extend or
67
+ shorten the expiration time of your offer, just use the
68
+ expires option and provide a unix timestamp representing the
69
+ desired expiration:
70
+
71
+ ```ruby
72
+ @key = session.security_key({
73
+ :amount => 20.00,
74
+ :email => "test@example.com",
75
+ :expires => (Time.now.to_i + (3600 * 5)) # Expire in 5 hours
76
+ })
77
+ ```
78
+
79
+ ## Key Groups
80
+
81
+ You can generate groups of key values for a user that will automatically
82
+ invalidate all members of the group when one key is processed. This
83
+ is useful when sending out multiple keys via email when only one key should ever
84
+ be processed:
85
+
86
+ ```ruby
87
+ @keys = session.security_key({
88
+ :amount => [20.00, 30.00, 40.00],
89
+ :email => "test@example.com"
90
+ })
91
+
92
+ # returns array length == 3
93
+ ```
94
+
95
+ ## User Data
96
+
97
+ You can pass in arbitrary data that will be returned to you upon the successful parsing of a token in @Pay's system. There is a limit of 2500 characters on this argument. It is expected to be a string beyond that any formatting should be returned as it was received.
98
+
99
+ ```ruby
100
+ @key = session.security_key({
101
+ :amount => 20.00,
102
+ :email => 'email@address',
103
+ :user_data => "{ sku: '82', cid: '3', notes: 'expedited' } "
104
+ })
105
+ ```
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "rake/clean"
2
+ require "rbnacl/rake_tasks"
3
+
4
+ file "lib/libsodium.so" => :build_libsodium do
5
+ cp $LIBSODIUM_PATH, "lib/libsodium.so"
6
+ end
7
+
8
+ task "ci:sodium" => "lib/libsodium.so"
9
+
10
+ task :ci => %w(ci:sodium spec)
11
+
12
+
13
+ CLEAN.add "lib/libsodium.*"
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'atpay_tokens'
3
+ s.version = '0.0.7'
4
+ s.date = '2013-07-25'
5
+ s.summary = "@Pay Token Generator"
6
+ s.description = "Client interface for the @Pay API, key generation for performance optimization"
7
+ s.authors = ["James Kassemi", "Glen Holcomb"]
8
+ s.email = 'james@atpay.com'
9
+ s.files = `git ls-files`.split($/)
10
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
11
+ s.homepage = "https://atpay.com"
12
+ s.add_dependency "rbnacl"
13
+ s.add_runtime_dependency 'ffi'
14
+ end
@@ -0,0 +1,216 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'atpay'
4
+ require 'yaml'
5
+
6
+
7
+ class Arguments
8
+ attr_reader :values
9
+
10
+ def initialize
11
+ @values = YAML.load_file(File.expand_path('../../config/credentials.yml', __FILE__))
12
+ end
13
+ end
14
+
15
+ module Usage
16
+ USAGE = <<-EOS
17
+
18
+ The @Pay Client will generate @Pay Tokens for you. You can use this tool to generate both email
19
+ and site tokens. There are quite a few arguments but only a handfull are required:
20
+
21
+
22
+ Examples:
23
+ ruby atpay-client.rb email_token targets bob@bob.com amount 8.0 expires 1454673223
24
+ ruby atpay-client.rb email_token targets bob@bob.com group "8.0 9.5 23.28"
25
+ ruby atpay-client.rb email_token cards OGQ3OWE0OWNhMFFTL4mMpQA= amount 12.0
26
+ ruby atpay-client.rb email_token cards OGQ3OWE0OWNhMFFTL4mMpQA= targets bob@bob.com amount 12.0
27
+
28
+ ruby atpay-client.rb site_token cards OGQ3OWE0OWNhMFFTL4mMpQA= amount 5.0 user_agent "curl/7.9.8" lang "en-US,en;q=0.8" charset "utf8" addr 173.163.242.213
29
+
30
+
31
+ Required:
32
+ site_token|email_token You must specify the type of token you wish to generate
33
+ cards|targets You must at least specify a card or email address. For a site token
34
+ you must provide a card. If you are building multiple tokens ensure
35
+ that you provide a card token for each email and that the order is
36
+ correct.
37
+ amount|group You also need to specify either a single amount or a group of amounts.
38
+
39
+
40
+ Required (for site token):
41
+ user_agent The HTTP_USER_AGENT from the client's request header.
42
+ lang The HTTP_ACCEPT_LANGUAGE from the client's request header.
43
+ charset The HTTP_ACCEPT_CHARSET from the client's request header.
44
+ addr The IP address corresponding to the requesting source (The user's IP)
45
+
46
+
47
+ Optional:
48
+ expires This is the expiration date. This should be an integer value of
49
+ seconds since epoch.
50
+
51
+ EOS
52
+ end
53
+
54
+
55
+ class ClientRunner
56
+ OPTIONS = %w(expires group amount)
57
+ HEADERS = %w(HTTP_USER_AGENT HTTP_ACCEPT_LANGUAGE HTTP_ACCEPT_CHARSET)
58
+ EMAIL = /[\w\d\.]+@[\w\.]+[\w]+/
59
+ CARD = /.*=$/
60
+
61
+ def initialize(args)
62
+ @config = Arguments.new
63
+ @session = AtPay::Session.new(@config.values)
64
+ @options = {}
65
+ @site_params = [{}]
66
+ @targets = []
67
+
68
+ parse args
69
+ end
70
+
71
+ # Parse our arguments for all of our values
72
+ def parse(args)
73
+ OPTIONS.each do |option|
74
+ @options[option.to_sym] = args[args.index(option) + 1] unless args.grep(option).empty?
75
+ end
76
+
77
+ site_params args if ARGV[0] == 'site_token'
78
+ convert_amounts
79
+ convert_expiration
80
+ get_emails args
81
+ get_cards args
82
+
83
+ unless @options[:card] or @options[:email]
84
+ puts USAGE and exit
85
+ end
86
+ end
87
+
88
+ # Build our list of email addresses to generate tokens for
89
+ def get_emails(args)
90
+ return unless args.index('targets')
91
+
92
+ args[args.index('targets') + 1].split(' ')[0].match(EMAIL) ? @options[:email] = args[(args.index('targets') + 1)].split(' ') : @options[:email] = File.read(args[args.index('targets') + 1]).split("\n")
93
+ end
94
+
95
+ # Grab all the card tokens
96
+ def get_cards(args)
97
+ return unless args.index('cards')
98
+
99
+ args[args.index('cards') + 1].split(' ')[0].match(CARD) ? @options[:card] = args[(args.index('cards') + 1)].split(' ') : @options[:card] = File.read(args[args.index('cards') + 1]).split("\n")
100
+ end
101
+
102
+ # Need expiration as an integer
103
+ def convert_expiration
104
+ @options[:expires] = @options[:expires].to_i if @options[:expires]
105
+ end
106
+
107
+ # Convert any amounts to floats.
108
+ def convert_amounts
109
+ @options[:amount] = @options[:amount].to_f if @options[:amount]
110
+ @options[:amount] = @options[:group].split(' ').map(&:to_f) if @options[:group]
111
+
112
+ @options.delete :group
113
+ end
114
+
115
+ # Check our input for site params.
116
+ def site_params(args)
117
+ if args.grep('addr').empty?
118
+ puts "You must provide an IP Address for a site token.\n" + USAGE
119
+ exit
120
+ end
121
+
122
+ @site_params[1] = args[args.index('addr') + 1]
123
+
124
+ headers(args)
125
+ end
126
+
127
+ def headers(args)
128
+ if args.grep('user_agent').empty? or args.grep('charset').empty? or args.grep('lang').empty?
129
+ puts "You must provide a user_agent, charset, and lang for a site token.\n" + USAGE
130
+ exit
131
+ end
132
+
133
+ @site_params[0][HEADERS[0]] = args[args.index('user_agent') + 1]
134
+ @site_params[0][HEADERS[1]] = args[args.index('lang') + 1]
135
+ @site_params[0][HEADERS[2]] = args[args.index('charset') + 1]
136
+ end
137
+
138
+ # Generate multiple site tokens
139
+ def site_tokens
140
+ options = @options.clone
141
+
142
+ @options[:card].each_with_index do |card, index|
143
+ options[:card] = card
144
+ options[:email] = @options[:email][index] if @options[:email]
145
+
146
+ site_token options
147
+ end
148
+ end
149
+
150
+ # Generate a site token
151
+ def site_token(options = @options)
152
+ if options[:card].is_a? Array
153
+ site_tokens
154
+ return
155
+ end
156
+
157
+ keys = security_key(options)
158
+
159
+ if keys.is_a? Array
160
+ puts keys.map { |key| key.site_token @site_params[1], @site_params[0] }
161
+ else
162
+ puts keys.site_token @site_params[1], @site_params[0]
163
+ end
164
+ end
165
+
166
+ # Generate multiple email tokens
167
+ def email_tokens
168
+ options = @options.clone
169
+
170
+ @options[:email].each_with_index do |email, index|
171
+ options[:email] = email
172
+ options[:card] = @options[:card][index] if @options[:card]
173
+
174
+ email_token options
175
+ end
176
+ end
177
+
178
+ # Generate an email token
179
+ def email_token(options = @options)
180
+ if options[:email].is_a? Array
181
+ email_tokens
182
+ return
183
+ end
184
+
185
+ keys = security_key(options)
186
+
187
+ if keys.is_a? Array
188
+ puts keys.map { |key| key.email_token }
189
+ else
190
+ puts keys.email_token
191
+ end
192
+ end
193
+
194
+ # Get a security key object we can ask to generate keys for us.
195
+ def security_key(options = @options)
196
+ @session.security_key(options)
197
+ end
198
+ end
199
+
200
+
201
+ operation = ARGV[0]
202
+ arguments = ARGV[1..-1]
203
+
204
+ unless operation
205
+ puts Usage::USAGE
206
+ exit
207
+ end
208
+
209
+ unless arguments.length > 0 and arguments.length % 2 == 0
210
+ puts Usage::USAGE
211
+ exit
212
+ end
213
+
214
+ runner = ClientRunner.new arguments
215
+
216
+ runner.send operation
@@ -0,0 +1,4 @@
1
+ :environment: :sandbox
2
+ :partner_id: 0
3
+ :public_key: azkHt3L37euW7HpajtPTix9K6Dqgb0OEjzo3NlvaMBY=
4
+ :private_key: 7hKlBre4DrnU6NlU86VmyY3FOWQ4I/ZtGlH4XzzT6cg=
@@ -0,0 +1,55 @@
1
+ require 'base64'
2
+
3
+ module AtPay
4
+ class Config
5
+ class << self
6
+ def base64_decoding_attr_accessor(*names)
7
+ names.each do |name|
8
+ attr_reader name
9
+
10
+ define_method "#{name}=" do |v|
11
+ instance_variable_set("@#{name}", Base64.decode64(v))
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ attr_reader :partner_id
18
+
19
+ base64_decoding_attr_accessor :private_key,
20
+ :public_key,
21
+ :atpay_private_key,
22
+ :atpay_public_key
23
+
24
+ def initialize(options)
25
+ options.each do |k,v|
26
+ self.send("#{k.to_s}=", v)
27
+ end
28
+
29
+ unless options[:environment] or (atpay_private_key and atpay_public_key)
30
+ self.environment = :sandbox
31
+ end
32
+ end
33
+
34
+ def partner_id=(v)
35
+ @partner_id = v
36
+ end
37
+
38
+ def environment=(v)
39
+ @environment = v
40
+
41
+ raise ValueError unless [:production, :sandbox, :development, :test].include? v
42
+
43
+ @atpay_public_key = Base64.decode64({
44
+ production: "QZuSjGhUz2DKEvjule1uRuW+N6vCOoMuR2PgCl57vB0=",
45
+ sandbox: "x3iJge6NCMx9cYqxoJHmFgUryVyXqCwapGapFURYh18=",
46
+ development: "x3iJge6NCMx9cYqxoJHmFgUryVyXqCwapGapFURYh18=",
47
+ test: '8LkeQ7BDO8+e+WRFLWV6Ac4Aq8Ev0odtWOiR1adDYyI='
48
+ }[v])
49
+
50
+ if @environment == :test
51
+ @atpay_private_key ||= Base64.decode64('bSyQWtGrWsYfJSZisrZ5eKHKcjtZQv1RO299tJ9bqIg=')
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,108 @@
1
+ require 'rbnacl'
2
+ require 'base64'
3
+ require 'securerandom'
4
+
5
+ module AtPay
6
+ class SecurityKey
7
+ def initialize(session, options)
8
+ raise ArgumentError.new("User Data can't exceed 2500 characters.") if options[:user_data] and options[:user_data].length > 2500
9
+ raise ArgumentError.new("email") unless options[:email].nil? or options[:email] =~ /.+@.+/
10
+ raise ArgumentError.new("amount") unless options[:amount].is_a? Float
11
+ raise ArgumentError.new("card or email or member required") if options[:email].nil? and options[:card].nil? and options[:member].nil?
12
+
13
+ @session = session
14
+ @options = options
15
+ end
16
+
17
+ def email_token
18
+ "@#{version}#{Base64.strict_encode64([nonce, partner_frame, body_frame].join)}"
19
+ ensure
20
+ @nonce = nil
21
+ end
22
+
23
+ def site_token(remote_addr, headers)
24
+ raise ArgumentError.new("card or member required for site tokens") if @options[:card].nil? and @options[:member].nil?
25
+ "@#{version}#{Base64.strict_encode64([nonce, partner_frame, site_frame(remote_addr, headers), body_frame].join)}"
26
+ ensure
27
+ @nonce = nil
28
+ end
29
+
30
+ def to_s
31
+ email_token
32
+ end
33
+
34
+
35
+ private
36
+ def version
37
+ @options[:version] ? (Base64.strict_encode64([@options[:version]].pack("Q>")) + '-') : nil
38
+ end
39
+
40
+ def partner_frame
41
+ [@session.config.partner_id].pack("Q>")
42
+ end
43
+
44
+ def site_frame(remote_addr, headers)
45
+ message = boxer.box(nonce, Digest::SHA1.hexdigest([
46
+ headers["HTTP_USER_AGENT"],
47
+ headers["HTTP_ACCEPT_LANGUAGE"],
48
+ headers["HTTP_ACCEPT_CHARSET"],
49
+ remote_addr
50
+ ].join))
51
+
52
+ [[message.length].pack("l>"), message,
53
+ [remote_addr.length].pack("l>"), remote_addr].join
54
+ end
55
+
56
+ def body_frame
57
+ boxer.box(nonce, crypted_frame)
58
+ end
59
+
60
+ def crypted_frame
61
+ if user_data = user_data_frame
62
+ [target, options_group, '/', options_frame, '/', user_data].flatten.compact.join
63
+ else
64
+ [target, options_group, "/", options_frame].flatten.compact.join
65
+ end
66
+ end
67
+
68
+ def options_frame
69
+ [@options[:amount], expires].pack("g l>")
70
+ end
71
+
72
+ def user_data_frame
73
+ @options[:user_data].to_s if @options[:user_data]
74
+ end
75
+
76
+ def options_group
77
+ ":#{@options[:group]}" if @options[:group]
78
+ end
79
+
80
+ def target
81
+ card_format || member_format || email_format
82
+ end
83
+
84
+ def card_format
85
+ "card<#{@options[:card]}>" if @options[:card]
86
+ end
87
+
88
+ def member_format
89
+ "member<#{@options[:member]}>" if @options[:member]
90
+ end
91
+
92
+ def email_format
93
+ "email<#{@options[:email]}>" if @options[:email]
94
+ end
95
+
96
+ def expires
97
+ @options[:expires] || (Time.now.to_i + 3600 * 24 * 7)
98
+ end
99
+
100
+ def boxer
101
+ @session.boxer
102
+ end
103
+
104
+ def nonce
105
+ @nonce ||= SecureRandom.random_bytes(24)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ require 'atpay/config'
2
+ require 'atpay/security_key'
3
+
4
+ module AtPay
5
+ class Session
6
+ attr_accessor :config
7
+
8
+ def initialize(options)
9
+ @config = Config.new(options)
10
+ end
11
+
12
+ def security_keys(options)
13
+ options = options.clone
14
+
15
+ options[:group] ||= "#{SecureRandom.uuid.gsub("-", "")}-#{Time.now.to_i}"
16
+
17
+ keys = []
18
+
19
+ options[:amount].each do |amount|
20
+ keys << security_key(options.update(:amount => amount))
21
+ end
22
+
23
+ keys
24
+ end
25
+
26
+ def security_key(options)
27
+ if options[:amount].is_a? Array
28
+ security_keys(options)
29
+ else
30
+ SecurityKey.new(self, options.update(:amount => options[:amount]))
31
+ end
32
+ end
33
+
34
+ def boxer
35
+ @boxer ||= Crypto::Box.new(config.atpay_public_key, config.private_key)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,186 @@
1
+ module AtPay
2
+ class Tokenator
3
+ attr_reader :token,
4
+ :payload,
5
+ :partner_id,
6
+ :source,
7
+ :amount,
8
+ :expires,
9
+ :group,
10
+ :site_frame,
11
+ :ip,
12
+ :user_data
13
+
14
+ # A bit clunky but useful for testing token decomposition.
15
+ # If you provide a test session then the config values there
16
+ # will be used so that decryption will function without @Pay's
17
+ # private key.
18
+ def initialize(token, session = nil)
19
+ @token = token
20
+ @session = session
21
+ strip_version
22
+
23
+ @checksum = Digest::SHA1.hexdigest(token) # Before or after removing version?
24
+ end
25
+
26
+ class << self
27
+ # Get the version of the given token.
28
+ def token_version(token)
29
+ token.scan('-').empty? ? 0 : unpack_version(token.split('-')[0])
30
+ end
31
+
32
+ # Check and make sure we haven't seen this token before.
33
+ # NOTE: This is really for internal use by @Pay.
34
+ def find_by_checksum(token)
35
+ checksum = Digest::SHA1.hexdigest(token)
36
+
37
+ SecurityKey.find_by_encoded_key(checksum) || ValidationToken.find_by_encoded_key(checksum)
38
+ end
39
+
40
+
41
+ private
42
+
43
+ # Unpack the actual version value.
44
+ def unpack_version(version)
45
+ Base64.decode64(version[1..-1]).unpack("Q>")[0]
46
+ end
47
+ end
48
+
49
+
50
+ # We want to pull the header out of the token. This means we
51
+ # grab the nonce and the partner id from the token. The version
52
+ # frame should be removed before calling header.
53
+ def header
54
+ decode
55
+ nonce
56
+ destination
57
+ end
58
+
59
+ # Here we parse the body of the token. All the useful shit
60
+ # comes out of this.
61
+ def body(key)
62
+ payload(nonce, key, @token)
63
+ part_out_payload
64
+ end
65
+
66
+ # With a site token you want to call this after header. It will
67
+ # pull the ip address and header sha out of the token.
68
+ def browser_data(key)
69
+ length = @token.slice!(0, 4).unpack("l>")[0]
70
+ @site_frame = boxer(key).open(nonce, @token.slice!(0, length))
71
+
72
+ length = @token.slice!(0, 4).unpack("l>")[0]
73
+ @ip = @token.slice!(0, length)
74
+ end
75
+
76
+ def source
77
+ {email: @email, card: @card, member: @member}
78
+ end
79
+
80
+ # Return parts in a hash structure, handy for ActiveRecord.
81
+ def to_h
82
+ {
83
+ sale_price: @amount,
84
+ expires_at: Time.at(@expires),
85
+ group: @group,
86
+ encoded_key: @checksum
87
+ }
88
+ end
89
+
90
+
91
+ private
92
+
93
+ # Strip the version frame from the token.
94
+ def strip_version
95
+ @token = @token.split('-').last
96
+ end
97
+
98
+ # Fix our Base64 problems
99
+ def decode
100
+ @token = Base64.decode64 @token
101
+ end
102
+
103
+ # Extract our entropy
104
+ def nonce
105
+ @nonce ||= @token.slice!(0, 24)
106
+ end
107
+
108
+ # Find the recipient of the betokened transaction
109
+ def destination
110
+ nonce unless @nonce
111
+
112
+ #@partner ||= OpportunityMap.find(@token.slice!(0, 8).unpack("Q>")[0]).opportunity
113
+ @partner_id ||= @token.slice!(0, 8).unpack("Q>")[0]
114
+ end
115
+
116
+ def boxer(key)
117
+ if @session
118
+ Crypto::Box.new(key, @session.config.atpay_private_key)
119
+ else
120
+ Crypto::Box.new(key, ENCRYPTION[:security_key_sec])
121
+ end
122
+ end
123
+
124
+ # Decrypt the payload.
125
+ def payload(nonce, key, decoded)
126
+ @payload = boxer(key).open(nonce, decoded)
127
+ end
128
+
129
+ # Break the payload out into it's constituent logical parts.
130
+ def part_out_payload
131
+ # TARGET:GROUP/AMOUNTEXPIRATION/USERDATA
132
+ # TARGET:GROUP/AMOUNTEXPIRATION
133
+ # TARGET:/AMOUNTEXPIRATION (?)
134
+ # TARGET/AMOUNTEXPIRATION/USERDATA
135
+ # TARGET/AMOUNTEXPIRATION
136
+ if @payload.match '>:'
137
+ raw_target, @group = @payload.split(':', 2)
138
+ else
139
+ raw_target = @payload
140
+ end
141
+
142
+ target raw_target
143
+
144
+ if @group
145
+ @group = @payload.slice!(0, @group.index("/"))
146
+ @payload.slice!(0, 1)
147
+ end
148
+
149
+ @amount = parse_amount!
150
+ @expiration = parse_expiration!
151
+ @user_data = parse_user_data!
152
+ end
153
+
154
+ def parse_amount!
155
+ @amount = @payload.slice!(0, 4).unpack("g")[0]
156
+ end
157
+
158
+ def parse_expiration!
159
+ @expires = @payload.slice!(0, 4).unpack("l>")[0]
160
+ end
161
+
162
+ def parse_user_data!
163
+ @user_data = @payload[1..-1]
164
+ @payload = nil
165
+ return @user_data
166
+ end
167
+
168
+ # Find the target of the token. This could be a Credit Card
169
+ # Email Address or Member UUID.
170
+ def target(target)
171
+ case target
172
+ when /card<(.*?)>/
173
+ @card = $1
174
+ @payload.slice!(0, ($1.length + 7))
175
+ when /email<(.*?)>/
176
+ @email = $1
177
+ @payload.slice!(0, ($1.length + 8))
178
+ when /member<(.*?)>/
179
+ @member = $1
180
+ @payload.slice!(0, ($1.length + 9))
181
+ else
182
+ raise "No target found"
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,5 @@
1
+ require 'atpay/session'
2
+ require 'atpay/tokenator'
3
+
4
+ module AtPay
5
+ end
@@ -0,0 +1,63 @@
1
+ require 'helper'
2
+
3
+ describe AtPay::Config do
4
+ let(:config) {
5
+ AtPay::Config.new({
6
+ :partner_id => partner_id,
7
+ :private_key => private_key,
8
+ :public_key => public_key,
9
+ :environment => :sandbox
10
+ })
11
+ }
12
+
13
+ describe "overview" do
14
+ it "accepts multiple arguments" do
15
+ AtPay::Config.any_instance.should_receive(:partner_id=).with(partner_id)
16
+ AtPay::Config.any_instance.should_receive(:private_key=).with(private_key)
17
+ AtPay::Config.any_instance.should_receive(:public_key=).with(public_key)
18
+ AtPay::Config.any_instance.should_receive(:environment=).with(:sandbox)
19
+
20
+ AtPay::Config.new({
21
+ :partner_id => partner_id,
22
+ :private_key => private_key,
23
+ :public_key => public_key,
24
+ :environment => :sandbox
25
+ })
26
+ end
27
+ end
28
+
29
+ describe "values" do
30
+ it "accepts partner id" do
31
+ config.partner_id = partner_id
32
+ config.partner_id.should eq(partner_id)
33
+ end
34
+
35
+ it "accepts private key" do
36
+ config.private_key = private_key
37
+ config.private_key.should_not be_empty
38
+ end
39
+
40
+ it "accepts public key" do
41
+ config.public_key = public_key
42
+ config.public_key.should_not be_empty
43
+ end
44
+
45
+ it "accepts environment" do
46
+ config.environment = :sandbox
47
+ config.atpay_public_key.should_not be_empty
48
+ end
49
+ end
50
+
51
+ describe "environment" do
52
+ it "required to be production or sandbox" do
53
+ expect {
54
+ AtPay::Config.new :environment => :none
55
+ }.to raise_error
56
+ end
57
+
58
+ it "defaults to sandbox" do
59
+ AtPay::Config.any_instance.should_receive(:environment=).with(:sandbox)
60
+ config = AtPay::Config.new({})
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,60 @@
1
+ # TODO: Decode utilities for further testing
2
+
3
+ require 'atpay'
4
+ require 'rbnacl'
5
+ require 'base64'
6
+ require 'helper'
7
+
8
+ describe AtPay::SecurityKey do
9
+ let(:session){
10
+ AtPay::Session.new({
11
+ :partner_id => partner_id,
12
+ :private_key => private_key,
13
+ :public_key => public_key,
14
+ :environment => :sandbox
15
+ })
16
+ }
17
+
18
+ describe "#initialize" do
19
+ it "fails when no email given" do
20
+ expect {
21
+ AtPay::SecurityKey.new(session, {
22
+ :amount => 25,
23
+ :email => nil
24
+ })
25
+ }.to raise_error
26
+ end
27
+
28
+ it "fails when no amount given" do
29
+ expect {
30
+ AtPay::SecurityKey.new(session, {
31
+ :amount => nil,
32
+ :email => "test@example.com"
33
+ })
34
+ }.to raise_error
35
+ end
36
+
37
+ it "fails when amount not float" do
38
+ expect {
39
+ AtPay::SecurityKey.new(session, {
40
+ :amount => "25",
41
+ :email => "test@example.com"
42
+ })
43
+ }.to raise_error
44
+ end
45
+ end
46
+
47
+ describe "#to_s" do
48
+ it "returns a valid key" do
49
+ key = AtPay::SecurityKey.new(session, {:email => "james@atpay.com", :amount => 25.00}).to_s
50
+ end
51
+
52
+ it "returns a key with a group" do
53
+ key = AtPay::SecurityKey.new(session, {:email => "james@atpay.com", :amount => 25.00, :group => "1234"}).to_s
54
+ end
55
+
56
+ it "returns a key with user_data" do
57
+ key = AtPay::SecurityKey.new(session, {:email => "glen@atpay.com", :amount => 25.00, :user_data => 'bacon and eggs'}).to_s
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ describe AtPay::Session do
4
+ let(:session) {
5
+ AtPay::Session.new({
6
+ :partner_id => partner_id,
7
+ :private_key => private_key,
8
+ :public_key => public_key,
9
+ :environment => :sandbox
10
+ })
11
+ }
12
+
13
+ it "Uses the configuration options" do
14
+ AtPay::Config.any_instance.should_receive(:initialize).with({})
15
+ AtPay::Session.new({})
16
+ end
17
+
18
+ it "Generates security key" do
19
+ session.security_key(:amount => 20.00,
20
+ :email => "james@example.com").to_s.should_not be_empty
21
+ end
22
+
23
+ it "Generates site token" do
24
+ session.security_key(:amount => 20.00,
25
+ :card => "test").site_token("0.0.0.0", {
26
+ "HTTP_USER_AGENT" => "0",
27
+ "HTTP_ACCEPT_LANGUAGE" => "1",
28
+ "HTTP_ACCEPT_CHARSET" => "2"
29
+ }).should_not be_empty
30
+ end
31
+
32
+ it "Generates multiple security keys" do
33
+ session.security_key(:amount => [20.00, 30.00],
34
+ :email => "james@example.com").length.should eq(2)
35
+ end
36
+
37
+ it "Returns a box and caches it" do
38
+ session.boxer.object_id.should eq(session.boxer.object_id)
39
+ end
40
+ end
@@ -0,0 +1,142 @@
1
+ require 'atpay'
2
+ require 'rbnacl'
3
+ require 'base64'
4
+ require 'helper'
5
+
6
+ describe AtPay::Tokenator do
7
+ let(:headers) { {'HTTP_USER_AGENT' => 'agent', 'HTTP_ACCEPT_LANGUAGE' => 'lang', 'HTTP_ACCEPT_CHARSET' => 'charset'} }
8
+ let(:ip) { '1.1.1.1' }
9
+
10
+ let(:payment) { AtPay::Tokenator.new token(50.00, [:email_token], {email: 'email@address'}), build_session }
11
+ let(:site) { AtPay::Tokenator.new token(50.00, [:site_token, ip, headers], {card: 'OGQ3OWE0OWNhMFFTL4mMpQA='}), build_session }
12
+ let(:user_data) { AtPay::Tokenator.new token(50.00, [:email_token], {email: 'email@address', user_data: 'lots of pills, paying forever'}), build_session }
13
+ let(:version) { AtPay::Tokenator.new token(50.00, [:email_token], {email: 'email@address', version: 2}), build_session }
14
+ let(:member) { AtPay::Tokenator.new token(50.00, [:email_token], {member: '4DF08A79-C16C-4842-AA1B-AE878C9C6C2C'}), build_session }
15
+ let(:group) { AtPay::Tokenator.new token(50.00, [:email_token], {member: '4DF08A79-C16C-4842-AA1B-AE878C9C6C2C', group: '18', user_data: 'hello from data'}), build_session }
16
+
17
+ describe "Parsing" do
18
+ it "Uses the key specified in ENCRYPTION if not given a session" do
19
+ tokenator = AtPay::Tokenator.new token(50.0, [:email_token], {email: 'email@address'})
20
+
21
+ expect { tokenator.send(:boxer, Base64.decode64(public_key)) }.to raise_error
22
+ end
23
+
24
+ describe "Payment Tokens" do
25
+ it "Extracts the partner" do
26
+ payment.header
27
+
28
+ payment.instance_eval { @partner_id }.should eq(123)
29
+ end
30
+
31
+ it "Extracts the body" do
32
+ payment.header
33
+ payment.body(Base64.decode64(public_key))
34
+
35
+ payment.source[:email].should eq('email@address')
36
+ payment.amount.should eq(50.0)
37
+ payment.expires.should_not eq(nil)
38
+ end
39
+
40
+ it "Presents token values as a hash" do
41
+ payment.header
42
+ payment.body(Base64.decode64(public_key))
43
+
44
+ payment.to_h.should be_a(Hash)
45
+ end
46
+
47
+ it "Processes a member token" do
48
+ member.header
49
+ member.body(Base64.decode64(public_key))
50
+
51
+ member.source[:member].should eq('4DF08A79-C16C-4842-AA1B-AE878C9C6C2C')
52
+ end
53
+
54
+ it "Processes a token with a group" do
55
+ group.header
56
+ group.body(Base64.decode64(public_key))
57
+
58
+ group.group.should eq('18')
59
+ group.user_data.should eq('hello from data')
60
+ end
61
+ end
62
+
63
+ describe "Exceptions" do
64
+ it "Raises target not found if there is no valid target" do
65
+ expect { payment.send :target, 'mom' }.to raise_error
66
+ end
67
+ end
68
+
69
+ describe "Site Tokens" do
70
+ it "Extracts the Partner" do
71
+ site.header
72
+
73
+ site.instance_eval { @partner_id }.should eq(123)
74
+ end
75
+
76
+ it "Extracts the site frame" do
77
+ site.header
78
+ site.browser_data(Base64.decode64(public_key))
79
+ sha = Digest::SHA1.hexdigest((headers.values + [ip]).join)
80
+
81
+ site.instance_eval { @site_frame }.should eq(sha)
82
+ site.instance_eval { @ip }.should eq(ip)
83
+ end
84
+
85
+ it "Extracts payload after extracting site frame" do
86
+ site.header
87
+ site.browser_data(Base64.decode64(public_key))
88
+ site.body(Base64.decode64(public_key))
89
+
90
+ site.source[:card].should eq('OGQ3OWE0OWNhMFFTL4mMpQA=')
91
+ site.amount.should eq(50.0)
92
+ site.expires.should_not eq(nil)
93
+ end
94
+ end
95
+
96
+ describe "User Data" do
97
+ it "Extracts supplied User Data" do
98
+ user_data.header
99
+ user_data.body(Base64.decode64(public_key))
100
+
101
+ user_data.user_data.should eq('lots of pills, paying forever')
102
+ end
103
+ end
104
+
105
+ describe "Versioning" do
106
+ let(:versioned) { token(50.00, [:email_token], {email: 'email@address', version: 2}) }
107
+
108
+ it "Extracts the version" do
109
+ AtPay::Tokenator.token_version(versioned).should eq(2)
110
+ end
111
+
112
+ it "Returns 0 when there is no version" do
113
+ test_token = token(50.00, [:email_token], {email: 'email@address'})
114
+
115
+ AtPay::Tokenator.token_version(test_token).should eq(0)
116
+ end
117
+
118
+ it "Behaves as a normal token when versioned" do
119
+ version.header
120
+ version.body(Base64.decode64(public_key))
121
+
122
+ version.amount.should eq(50.0)
123
+ end
124
+ end
125
+
126
+ describe "Checksum lookup" do
127
+ before do
128
+ class AtPay::SecurityKey; end
129
+ class AtPay::ValidationToken; end
130
+
131
+ AtPay::SecurityKey.should_receive(:find_by_encoded_key)
132
+ AtPay::ValidationToken.should_receive(:find_by_encoded_key)
133
+ end
134
+
135
+ it "should look for a token with a matching checksum" do
136
+ token = token(50.0, [:email_token], {email: 'email@address'})
137
+
138
+ AtPay::Tokenator.find_by_checksum(token)
139
+ end
140
+ end
141
+ end
142
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+
4
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ Coveralls::SimpleCov::Formatter
7
+ ]
8
+
9
+ SimpleCov.start
10
+
11
+ require 'rubygems'
12
+ require 'bundler/setup'
13
+ require 'atpay'
14
+ require 'rspec/core/shared_context'
15
+
16
+ module Setup
17
+ extend RSpec::Core::SharedContext
18
+
19
+ let(:partner_id) { 123 }
20
+ let(:keys) {
21
+ sec = Crypto::PrivateKey.generate
22
+ pub = sec.public_key
23
+ [pub.to_bytes, sec.to_bytes]
24
+ }
25
+ let(:public_key) { Base64.strict_encode64(keys[0]) }
26
+ let(:private_key) { Base64.strict_encode64(keys[1]) }
27
+
28
+ def token(amount, type, options = {})
29
+ build_session.security_key({
30
+ amount: amount,
31
+ }.merge(options)).send(*type).to_s
32
+ end
33
+
34
+ def build_session
35
+ AtPay::Session.new({
36
+ public_key: public_key,
37
+ private_key: private_key,
38
+ partner_id: partner_id,
39
+ environment: :test
40
+ })
41
+ end
42
+ end
43
+
44
+ RSpec.configure do |r|
45
+ r.include Setup
46
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atpay_tokens
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.7
5
+ platform: ruby
6
+ authors:
7
+ - James Kassemi
8
+ - Glen Holcomb
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rbnacl
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: ffi
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description: Client interface for the @Pay API, key generation for performance optimization
43
+ email: james@atpay.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - .gitignore
49
+ - .travis.yml
50
+ - Gemfile
51
+ - README.md
52
+ - Rakefile
53
+ - atpay_tokens.gemspec
54
+ - bin/atpay-client.rb
55
+ - config/credentials.yml
56
+ - lib/atpay/config.rb
57
+ - lib/atpay/security_key.rb
58
+ - lib/atpay/session.rb
59
+ - lib/atpay/tokenator.rb
60
+ - lib/atpay_tokens.rb
61
+ - spec/atpay/config_spec.rb
62
+ - spec/atpay/security_key_spec.rb
63
+ - spec/atpay/session_spec.rb
64
+ - spec/atpay/tokenator_spec.rb
65
+ - spec/helper.rb
66
+ homepage: https://atpay.com
67
+ licenses: []
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.0.2
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: '@Pay Token Generator'
89
+ test_files:
90
+ - spec/atpay/config_spec.rb
91
+ - spec/atpay/security_key_spec.rb
92
+ - spec/atpay/session_spec.rb
93
+ - spec/atpay/tokenator_spec.rb
94
+ - spec/helper.rb