├── spec ├── key_hex.txt ├── token_data_base64.txt └── decrypt_spec.rb ├── Gemfile ├── ext └── aes256gcm_decrypt │ ├── extconf.rb │ └── aes256gcm_decrypt.c ├── .rubocop.yml ├── .rubocop_todo.yml ├── Rakefile ├── .circleci └── config.yml ├── aes256gcm_decrypt.gemspec ├── LICENSE.txt └── README.md /spec/key_hex.txt: -------------------------------------------------------------------------------- 1 | 1ce49a828f59d43861ba442fce6829b8218fbb0ab55b40206ac31058d66f5086 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem 'rubocop', require: false 9 | end 10 | -------------------------------------------------------------------------------- /ext/aes256gcm_decrypt/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mkmf' 4 | extension_name = 'aes256gcm_decrypt' 5 | dir_config(extension_name) 6 | $CFLAGS << ' -Wall' 7 | $LDFLAGS << ' -lcrypto' 8 | create_makefile(extension_name) 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | - 'aes256gcm_decrypt.gemspec' 6 | - 'ext/aes256gcm_decrypt/extconf.rb' 7 | - 'tmp/**/aes256gcm_decrypt.gemspec' 8 | - 'tmp/**/extconf.rb' 9 | 10 | Metrics/BlockLength: 11 | Max: 48 12 | 13 | Metrics/LineLength: 14 | Max: 86 15 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-05-23 10:32:04 -0400 using RuboCop version 0.55.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake/extensiontask' 4 | require 'rspec/core/rake_task' 5 | 6 | spec = Gem::Specification.load('aes256gcm_decrypt.gemspec') 7 | Rake::ExtensionTask.new('aes256gcm_decrypt', spec) 8 | 9 | desc '' 10 | RSpec::Core::RakeTask.new(:spec) do |task| 11 | task.pattern = './spec/**/*_spec.rb' 12 | end 13 | 14 | desc 'Compile extension and run specs' 15 | task test: %i[compile spec] 16 | -------------------------------------------------------------------------------- /spec/token_data_base64.txt: -------------------------------------------------------------------------------- 1 | 4OZho15e9Yp5K0EtKergKzeRpPAjnKHwmSNnagxhjwhKQ5d29sfTXjdbh1CtTJ4DYjsD6kfulNUnYmBTsruphBz7RRVI1WI8P0LrmfTnImjcq1mi+BRN7EtR2y6MkDmAr78anff91hlc+x8eWD/NpO/oZ1ey5qV5RBy/Jp5zh6ndVUVq8MHHhvQv4pLy5Tfi57Yo4RUhAsyXyTh4x/p1360BZmoWomK15NcJfUmoUCuwEYoi7xUkRwNr1z4MKnzMfneSRpUgdc0wADMeB6u1jcuwqQnnh2cusiagOTCfD6jO6tmouvu6KO54uU7bAbKz6cocIOEAOc6keyFXG5dfw8i3hJg6G2vIefHCwcKu1zFCHr4P7jLnYFDEhvxLm1KskDcuZeQHAkBMmLRSgj9NIcpBa94VN/JTga8W75IWAA== 2 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | test: 5 | docker: 6 | - image: circleci/ruby:2.3-stretch 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install gems 11 | command: bundle install --path vendor/bundle 12 | - run: 13 | name: Install libssl-dev 14 | command: sudo apt-get install -y --no-install-recommends libssl-dev 15 | - run: 16 | name: Execute tests 17 | command: bundle exec rake test 18 | 19 | workflows: 20 | version: 2 21 | 22 | test_aes256gcm_decrypt: 23 | jobs: 24 | - test 25 | -------------------------------------------------------------------------------- /aes256gcm_decrypt.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'aes256gcm_decrypt' 5 | s.version = '1.0.0' 6 | s.summary = 'Decrypt AES256GCM-encrypted data in Apple Pay Payment Tokens' 7 | s.author = 'Clearhaus' 8 | s.homepage = 'https://github.com/clearhaus/aes256gcm_decrypt' 9 | s.license = 'MIT' 10 | 11 | s.files = `git ls-files -z`.split("\x0") 12 | 13 | s.extensions << 'ext/aes256gcm_decrypt/extconf.rb' 14 | 15 | s.required_ruby_version = '< 2.4' 16 | 17 | s.add_development_dependency 'rake-compiler' 18 | s.add_development_dependency 'rspec' 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Clearhaus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aes256GcmDecrypt 2 | 3 | Decrypt AES256GCM-encrypted data in Apple Pay Payment Tokens. 4 | 5 | This library is necessary for Ruby < 2.4 (if you use the stdlib openssl rather than the [openssl gem](https://rubygems.org/gems/openssl)), as the OpenSSL bindings do not support setting the length of the initialisation vector (IV). Setting the IV length is necessary for decrypting Apple Pay data. 6 | 7 | The library becomes obsolete when we start using Ruby >= 2.4. 8 | 9 | ## Usage 10 | 11 | ``` 12 | bundle install 13 | bundle exec rake test 14 | 15 | irb -r base64 -I lib -r aes256gcm_decrypt 16 | 17 | ciphertext_and_tag = Base64.decode64(File.read('spec/token_data_base64.txt')) 18 | key = [File.read('spec/key_hex.txt').strip].pack('H*') 19 | 20 | begin 21 | puts Aes256GcmDecrypt::decrypt(ciphertext_and_tag, key) 22 | rescue Aes256GcmDecrypt::AuthenticationError => e 23 | # somebody is up to something 24 | rescue Aes256GcmDecrypt::Error => e 25 | # super class for the possible errors; Aes256GcmDecrypt::InputError and 26 | # Aes256GcmDecrypt::OpenSSLError are left, i.e. either you supplied invalid 27 | # input or we got an unexpected error from OpenSSL 28 | end 29 | ``` 30 | 31 | See also [the specs](spec/decrypt_spec.rb). 32 | 33 | ## Inspirational sources 34 | 35 | * [Your first Ruby native extension: C](https://blog.jcoglan.com/2012/07/29/your-first-ruby-native-extension-c/) 36 | * [Step-by-Step Guide to Building Your First Ruby Gem](https://quickleft.com/blog/engineering-lunch-series-step-by-step-guide-to-building-your-first-ruby-gem/) 37 | * [OpenSSL EVP Authenticated Decryption using GCM](https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Decryption_using_GCM_mode) 38 | * [The Ruby C API](http://silverhammermba.github.io/emberb/c/) 39 | * [Apple Pay Payment Token Format Reference](https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html) 40 | * [Spreedly's gala gem](https://github.com/spreedly/gala) 41 | -------------------------------------------------------------------------------- /spec/decrypt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'aes256gcm_decrypt' 4 | require 'base64' 5 | 6 | describe 'Aes256GcmDecrypt::decrypt' do 7 | before(:each) do 8 | @ciphertext_and_tag = Base64.decode64(File.read('spec/token_data_base64.txt')) 9 | @key = [File.read('spec/key_hex.txt').strip].pack('H*') 10 | end 11 | 12 | it 'decrypts correctly' do 13 | plaintext = Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, @key) 14 | expect(plaintext).to eq \ 15 | '{"applicationPrimaryAccountNumber":"4109370251004320","applicationExp' \ 16 | 'irationDate":"200731","currencyCode":"840","transactionAmount":100,"d' \ 17 | 'eviceManufacturerIdentifier":"040010030273","paymentDataType":"3DSecu' \ 18 | 're","paymentData":{"onlinePaymentCryptogram":"Af9x/QwAA/DjmU65oyc1MAA' \ 19 | 'BAAA=","eciIndicator":"5"}}' 20 | end 21 | 22 | it 'detects wrong parameter types' do 23 | expect { Aes256GcmDecrypt.decrypt(42, @key) }.to \ 24 | raise_error(Aes256GcmDecrypt::InputError, 'ciphertext_and_tag must be a string') 25 | expect { Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, 42) }.to \ 26 | raise_error(Aes256GcmDecrypt::InputError, 'key must be a string') 27 | end 28 | 29 | it 'detects too short ciphertext_and_tag' do 30 | (0..16).each do |i| 31 | ciphertext_and_tag = 'x' * i 32 | expect { Aes256GcmDecrypt.decrypt(ciphertext_and_tag, @key) }.to \ 33 | raise_error(Aes256GcmDecrypt::InputError, 'ciphertext_and_tag too short') 34 | end 35 | end 36 | 37 | it 'detects wrong key length' do 38 | ((0..64).to_a - [32]).each do |i| 39 | key = 'x' * i 40 | expect { Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, key) }.to \ 41 | raise_error(Aes256GcmDecrypt::InputError, 'length of key must be 32') 42 | end 43 | end 44 | 45 | it 'detects tampering with the ciphertext' do 46 | @ciphertext_and_tag[0] = 'x' 47 | expect { Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, @key) }.to \ 48 | raise_error(Aes256GcmDecrypt::AuthenticationError, 'Authentication failed') 49 | end 50 | 51 | it 'detects an incorrect tag' do 52 | @ciphertext_and_tag[-1] = 'x' 53 | expect { Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, @key) }.to \ 54 | raise_error(Aes256GcmDecrypt::AuthenticationError, 'Authentication failed') 55 | end 56 | 57 | it 'detects an incorrect key' do 58 | @key[0] = 'x' 59 | expect { Aes256GcmDecrypt.decrypt(@ciphertext_and_tag, @key) }.to \ 60 | raise_error(Aes256GcmDecrypt::AuthenticationError, 'Authentication failed') 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /ext/aes256gcm_decrypt/aes256gcm_decrypt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | VALUE aes256gcm_input_error; 8 | VALUE aes256gcm_openssl_error; 9 | VALUE aes256gcm_auth_error; 10 | 11 | VALUE aes256gcm_decrypt(VALUE self, VALUE rb_ciphertext_and_tag, VALUE rb_key) { 12 | 13 | /* Declare variables */ 14 | VALUE result; 15 | unsigned int ciphertext_and_tag_len, ciphertext_len, tag_len; 16 | unsigned int block_len, key_len, iv_len; 17 | int plaintext_len, len; 18 | unsigned char *ciphertext, *tag, *key, *plaintext; 19 | char *rb_ciphertext_p; 20 | unsigned char iv[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 21 | const char *openssl_error_message = ""; 22 | EVP_CIPHER_CTX *ctx; 23 | 24 | /* Check parameters */ 25 | if (!RB_TYPE_P(rb_ciphertext_and_tag, T_STRING)) { 26 | rb_raise(aes256gcm_input_error, "ciphertext_and_tag must be a string"); 27 | } 28 | if (!RB_TYPE_P(rb_key, T_STRING)) { 29 | rb_raise(aes256gcm_input_error, "key must be a string"); 30 | } 31 | ciphertext_and_tag_len = RSTRING_LEN(rb_ciphertext_and_tag); 32 | tag_len = 16; 33 | if (ciphertext_and_tag_len <= tag_len) { 34 | rb_raise(aes256gcm_input_error, "ciphertext_and_tag too short"); 35 | } 36 | key_len = RSTRING_LEN(rb_key); 37 | if (key_len != 32) { 38 | rb_raise(aes256gcm_input_error, "length of key must be 32"); 39 | } 40 | 41 | /* Prepare variables */ 42 | result = Qnil; 43 | block_len = 16; 44 | iv_len = 16; 45 | 46 | ciphertext_len = ciphertext_and_tag_len - tag_len; 47 | ciphertext = calloc(ciphertext_len, sizeof(unsigned char)); 48 | rb_ciphertext_p = StringValuePtr(rb_ciphertext_and_tag); 49 | memcpy(ciphertext, rb_ciphertext_p, ciphertext_len); 50 | 51 | tag = calloc(tag_len, sizeof(unsigned char)); 52 | memcpy(tag, &rb_ciphertext_p[ciphertext_len], tag_len); 53 | 54 | key = calloc(key_len, sizeof(unsigned char)); 55 | memcpy(key, StringValuePtr(rb_key), key_len); 56 | 57 | plaintext = calloc(ciphertext_len + block_len, sizeof(unsigned char)); 58 | 59 | /* Create and initialise context */ 60 | if (!(ctx = EVP_CIPHER_CTX_new())) { 61 | openssl_error_message = "Could not create and initialise context"; 62 | goto cleanup1; 63 | } 64 | 65 | /* Initialise decryption operation */ 66 | if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL)) { 67 | openssl_error_message = "Could not initialise decryption operation"; 68 | goto cleanup2; 69 | } 70 | 71 | /* Set initialisation vector (IV) length */ 72 | if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)) { 73 | openssl_error_message = "Could not set IV length"; 74 | goto cleanup2; 75 | } 76 | 77 | /* Initialise key and IV */ 78 | if (!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) { 79 | openssl_error_message = "Could not initialise key and IV"; 80 | goto cleanup2; 81 | } 82 | 83 | /* Provide message to be decrypted */ 84 | if (!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) { 85 | openssl_error_message = "DecryptUpdate failed"; 86 | goto cleanup2; 87 | } 88 | plaintext_len = len; 89 | 90 | /* Set expected tag */ 91 | if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag_len, tag)) { 92 | openssl_error_message = "Could not set expected tag"; 93 | goto cleanup2; 94 | } 95 | 96 | /* Finalise decryption */ 97 | if (EVP_DecryptFinal_ex(ctx, &plaintext[len], &len) > 0) { 98 | plaintext_len += len; 99 | if ((unsigned int)plaintext_len > ciphertext_len + block_len) { 100 | fprintf(stderr, "Plaintext overflow in AES256GCM decryption! Aborting.\n"); 101 | abort(); 102 | } 103 | result = rb_str_new((char *)plaintext, plaintext_len); 104 | } else { 105 | result = Qfalse; 106 | } 107 | 108 | cleanup2: 109 | EVP_CIPHER_CTX_free(ctx); 110 | 111 | cleanup1: 112 | free(plaintext); 113 | free(key); 114 | free(tag); 115 | free(ciphertext); 116 | 117 | switch (result) { 118 | case Qnil: 119 | rb_raise(aes256gcm_openssl_error, "%s", openssl_error_message); 120 | case Qfalse: 121 | rb_raise(aes256gcm_auth_error, "Authentication failed"); 122 | default: 123 | return result; 124 | } 125 | } 126 | 127 | void Init_aes256gcm_decrypt() { 128 | VALUE module, error; 129 | 130 | module = rb_define_module("Aes256GcmDecrypt"); 131 | error = rb_define_class_under(module, "Error", rb_eStandardError); 132 | aes256gcm_input_error = rb_define_class_under(module, "InputError", error); 133 | aes256gcm_openssl_error = rb_define_class_under(module, "OpenSSLError", error); 134 | aes256gcm_auth_error = rb_define_class_under(module, "AuthenticationError", error); 135 | rb_define_singleton_method(module, "decrypt", aes256gcm_decrypt, 2); 136 | } 137 | --------------------------------------------------------------------------------