atpay_tokens 0.0.7

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