we_whisper 0.0.1

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: 3a56a0106dba9c9dec08902dab5edc2b149e2413
4
+ data.tar.gz: 6559b472e8beae2752e28dae4af52b996af1dcaf
5
+ SHA512:
6
+ metadata.gz: 0ce8d458f5ec1faa48f54fdaefe2cc3b35fffa5bd2f7a540ea182e5e5c400d10593dc8dcf5abdb333de09f5f00c1d3e03fa26226d5474063f2c1f111b905841f
7
+ data.tar.gz: 6f2c2d9c94c1368b56ef0f7cec5471d30b2b907f8b4062572e68399c8878ff5fbeb1f783a5331151da4f40ed0c15b7f9221391cbd35c826d95fde36883847fe9
@@ -0,0 +1,72 @@
1
+ # Credit: https://github.com/Eric-Guo/wechat/blob/master/lib/wechat/cipher.rb
2
+
3
+ require 'openssl/cipher'
4
+ require 'securerandom'
5
+ require 'base64'
6
+
7
+ module WeWhisper
8
+ module Cipher
9
+
10
+ BLOCK_SIZE = 32
11
+ CIPHER = 'AES-256-CBC'.freeze
12
+
13
+ def encrypt(plain, encoding_aes_key)
14
+ cipher = OpenSSL::Cipher.new(CIPHER)
15
+ cipher.encrypt
16
+
17
+ cipher.padding = 0
18
+ key_data = Base64.decode64(encoding_aes_key + '=')
19
+ cipher.key = key_data
20
+ cipher.iv = key_data[0..16]
21
+
22
+ cipher.update(plain) + cipher.final
23
+ end
24
+
25
+ def decrypt(msg, encoding_aes_key)
26
+ cipher = OpenSSL::Cipher.new(CIPHER)
27
+ cipher.decrypt
28
+
29
+ cipher.padding = 0
30
+ key_data = Base64.decode64(encoding_aes_key + '=')
31
+ cipher.key = key_data
32
+ cipher.iv = key_data[0..16]
33
+
34
+ plain = cipher.update(msg) + cipher.final
35
+ decode_padding(plain)
36
+ end
37
+
38
+ def pack(content, app_id)
39
+ random = SecureRandom.hex(8)
40
+ text = content.force_encoding('ASCII-8BIT')
41
+ msg_len = [text.length].pack('N')
42
+
43
+ encode_padding("#{random}#{msg_len}#{text}#{app_id}")
44
+ end
45
+
46
+ def unpack(msg)
47
+ msg = decode_padding(msg)
48
+ msg_len = msg[16, 4].reverse.unpack('V')[0]
49
+ content = msg[20, msg_len]
50
+ app_id = msg[(20 + msg_len)..-1]
51
+
52
+ [content, app_id]
53
+ end
54
+
55
+ private
56
+
57
+ def encode_padding(data)
58
+ length = data.bytes.length
59
+ amount_to_pad = BLOCK_SIZE - (length % BLOCK_SIZE)
60
+ amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
61
+ padding = ([amount_to_pad].pack('c') * amount_to_pad)
62
+ data + padding
63
+ end
64
+
65
+ def decode_padding(plain)
66
+ pad = plain.bytes[-1]
67
+ # if padding is less than 1 or larger than block size, then set to 0
68
+ pad = 0 if pad < 1 || pad > BLOCK_SIZE
69
+ plain[0...(plain.length - pad)]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_support/core_ext/hash'
2
+
3
+ module WeWhisper
4
+
5
+ InvalidMessageClassError = Class.new StandardError
6
+
7
+ module Message
8
+ extend self
9
+
10
+ def to_xml(content, signature, timestamp, nonce)
11
+ """<xml>
12
+ <Encrypt><![CDATA[#{content}]]></Encrypt>
13
+ <MsgSignature><![CDATA[#{signature}]]></MsgSignature>
14
+ <TimeStamp>#{timestamp}</TimeStamp>
15
+ <Nonce><![CDATA[#{nonce}]]></Nonce>
16
+ </xml>"""
17
+ end
18
+
19
+ def get_value_of_key_in_message(message, key)
20
+ case message.class.name
21
+ when "String"
22
+ message_hash = Hash.from_xml(message)
23
+ message_hash[key] || message_hash["xml"][key]
24
+ when "Hash"
25
+ message[key] || message[key.to_sym]
26
+ else
27
+ raise InvalidMessageClassError, "Message can only be a String or a Hash"
28
+ end
29
+ end
30
+
31
+ def get_encrypted_content_from_message(message)
32
+ get_value_of_key_in_message(message, "Encrypt")
33
+ end
34
+
35
+ def get_signature_from_messge(message)
36
+ get_value_of_key_in_message(message, "MsgSignature")
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ require 'digest/sha2'
2
+
3
+ module WeWhisper
4
+ module Signature
5
+ def self.sign(token, timestamp, nonce, encrypted)
6
+ array = [token, timestamp, nonce]
7
+ array << encrypted unless encrypted.nil?
8
+ Digest::SHA1.hexdigest array.compact.collect(&:to_s).sort.join
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module WeWhisper
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,61 @@
1
+ require 'base64'
2
+
3
+ require_relative 'cipher'
4
+ require_relative 'signature'
5
+ require_relative 'message'
6
+
7
+ module WeWhisper
8
+ InvalidSignature = Class.new StandardError
9
+ AppIdNotMatch = Class.new StandardError
10
+
11
+ class Whisper
12
+ include Cipher
13
+
14
+ attr_reader :appid, :encoding_aes_key, :token, :options
15
+
16
+ def initialize(appid, token, encoding_aes_key, opts={})
17
+ @options = {
18
+ assert_signature: true,
19
+ assert_appid: true
20
+ }.merge(opts)
21
+
22
+ @appid = appid
23
+ @token = token
24
+ @encoding_aes_key = encoding_aes_key
25
+ end
26
+
27
+ def decrypt_message(message, nonce="", timestamp="")
28
+ # 1. Get the encrypted content from XML Message
29
+ encrypted_text = Message.get_encrypted_content_from_message(message)
30
+
31
+ # 2. If we need to validate signature, generate one from the encrypted text
32
+ # and check with the Signature in message
33
+ if options[:assert_signature] && signature = Message.get_signature_from_messge(message)
34
+ sign = Signature.sign(token, timestamp, nonce, encrypted_text)
35
+ raise InvalidSignature if sign != signature
36
+ end
37
+
38
+ # 3. Decode and decrypt the encrypted text
39
+ decrypted_message, decrypted_appid = \
40
+ unpack(decrypt(Base64.decode64(encrypted_text), encoding_aes_key))
41
+
42
+ if options[:assert_appid]
43
+ raise AppIdNotMatch if decrypted_appid != appid
44
+ end
45
+
46
+ decrypted_message
47
+ end
48
+
49
+ def encrypt_message(message, nonce, timestamp)
50
+ # 1. Encrypt and encode the xml message
51
+ encrypt = Base64.strict_encode64(encrypt(pack(message, appid), encoding_aes_key))
52
+
53
+ # 2. Create signature
54
+ sign = Signature.sign(token, timestamp, nonce, encrypt)
55
+
56
+ # 3. Construct xml
57
+ Message.to_xml(encrypt, sign, timestamp, nonce)
58
+ end
59
+
60
+ end
61
+ end
data/lib/we_whisper.rb ADDED
@@ -0,0 +1,6 @@
1
+ module WeWhisper
2
+ require_relative 'we_whisper/cipher'
3
+ require_relative 'we_whisper/signature'
4
+ require_relative 'we_whisper/message'
5
+ require_relative 'we_whisper/whisper'
6
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require "we_whisper"
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe WeWhisper::Message do
4
+
5
+ it "constructs XML message" do
6
+ expect(subject.to_xml("hello", "signature", "2016/10/10", "nonce")).to \
7
+ eq """<xml>
8
+ <Encrypt><![CDATA[hello]]></Encrypt>
9
+ <MsgSignature><![CDATA[signature]]></MsgSignature>
10
+ <TimeStamp>2016/10/10</TimeStamp>
11
+ <Nonce><![CDATA[nonce]]></Nonce>
12
+ </xml>"""
13
+ end
14
+
15
+ describe "Message parsing" do
16
+ let(:hash_message) { { Encrypt: "hash_encrypted" } }
17
+ let(:xml_message) { """<xml>
18
+ <Encrypt>xml_encrypted_message</Encrypt>
19
+ </xml>"""
20
+ }
21
+
22
+ it "parses encrypted content from Hash message" do
23
+ expect(subject.get_encrypted_content_from_message(hash_message)).to \
24
+ eq "hash_encrypted"
25
+ end
26
+
27
+ it "parses encrypted content from XML message" do
28
+ expect(subject.get_encrypted_content_from_message(xml_message)).to \
29
+ eq "xml_encrypted_message"
30
+ end
31
+
32
+ it "raises invalid message class error from unknown message" do
33
+ expect{ subject.get_encrypted_content_from_message(nil) }.to \
34
+ raise_error WeWhisper::InvalidMessageClassError
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe WeWhisper::Signature do
4
+
5
+ let(:timestamp) { "1415979516" }
6
+ let(:nonce) { "1320562132" }
7
+ let(:signature) { "096d8cda45e4678ca23460f6b8cd281b3faf1fc3" }
8
+ let(:token) { "spamtest" }
9
+ let(:encrypted) { "3kKZ++U5ocvIF8dAHPct7xvUqEv6vplhuzA8Vwj7OnVcBu9fdmbbI41zclSfKqP6/bdYAxuE3x8jse43ImHaV07siJF473TsXhl8Yt8task0n9KC7BDA73mFTwlhYvuCIFnU6wFlzOkHyM5Bh2qpOHYk5nSMRyUG4BwmXpxq8TvLgJV1jj2DXdGW4qdknGLfJgDH5sCPJeBzNC8j8KtrJFxmG7qIwKHn3H5sqBf6UqhXFdbLuTWL3jwE7yMLhzOmiHi/MX/ZsVQ7sMuBiV6bW0wkgielESC3yNUPo4q/RMAFEH0fRLr76BR5Ct0nUbf9PdClc0RdlYcztyOs54X/KLbYRNCQ2kXxmJYL6ekdNe70PCAReIEfXEp+pGpry4ss8bD6LKAtNvBJUwHshZe6sbf+fOiDiuKEqp1wdQLmgN+8nX62LklySWr8QrNCpsmKClxco0kbVYNX/QVh5yd0UA1sAqIn6baZ9G+Z/OXG+Q4n9lUuzLprLhDBPaCvXm4N14oqXNcw7tqU2xfhYNIDaD72djyIc/4eyAi2ZsJ+3hb+jgiISR5WVveRWYYqGZGTW3u+27JiXEo0fs3DQDbGVIcYxaMgU/RRIDdXzZSFcf6Z1azjzCDyV9FFEsicghHn" }
10
+
11
+ it "signs message" do
12
+ expect(subject.sign(token, timestamp, nonce, encrypted)).to eq signature
13
+ end
14
+
15
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe WeWhisper::Whisper do
4
+
5
+ let(:timestamp) { "1415979516" }
6
+ let(:nonce) { "1320562132" }
7
+ let(:signature) { "096d8cda45e4678ca23460f6b8cd281b3faf1fc3" }
8
+ let(:message) { "<xml><ToUserName><![CDATA[oia2TjjewbmiOUlr6X-1crbLOvLw]]></ToUserName><FromUserName><![CDATA[gh_7f083739789a]]></FromUserName><CreateTime>1407743423</CreateTime><MsgType> <![CDATA[video]]></MsgType><Video><MediaId><![CDATA[eYJ1MbwPRJtOvIEabaxHs7TX2D-HV71s79GUxqdUkjm6Gs2Ed1KF3ulAOA9H1xG0]]></MediaId><Title><![CDATA[testCallBackReplyVideo]]></Title><Description><![CDATA[testCallBackReplyVideo]]></Description></Video></xml>" }
9
+ let(:whisper) { WeWhisper::Whisper.new "wx2c2769f8efd9abc2", "spamtest", "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" }
10
+ let(:encrypted_message) {
11
+ """<xml>
12
+ <Encrypt><![CDATA[3kKZ++U5ocvIF8dAHPct7xvUqEv6vplhuzA8Vwj7OnVcBu9fdmbbI41zclSfKqP6/bdYAxuE3x8jse43ImHaV07siJF473TsXhl8Yt8task0n9KC7BDA73mFTwlhYvuCIFnU6wFlzOkHyM5Bh2qpOHYk5nSMRyUG4BwmXpxq8TvLgJV1jj2DXdGW4qdknGLfJgDH5sCPJeBzNC8j8KtrJFxmG7qIwKHn3H5sqBf6UqhXFdbLuTWL3jwE7yMLhzOmiHi/MX/ZsVQ7sMuBiV6bW0wkgielESC3yNUPo4q/RMAFEH0fRLr76BR5Ct0nUbf9PdClc0RdlYcztyOs54X/KLbYRNCQ2kXxmJYL6ekdNe70PCAReIEfXEp+pGpry4ss8bD6LKAtNvBJUwHshZe6sbf+fOiDiuKEqp1wdQLmgN+8nX62LklySWr8QrNCpsmKClxco0kbVYNX/QVh5yd0UA1sAqIn6baZ9G+Z/OXG+Q4n9lUuzLprLhDBPaCvXm4N14oqXNcw7tqU2xfhYNIDaD72djyIc/4eyAi2ZsJ+3hb+jgiISR5WVveRWYYqGZGTW3u+27JiXEo0fs3DQDbGVIcYxaMgU/RRIDdXzZSFcf6Z1azjzCDyV9FFEsicghHn]]></Encrypt>
13
+ <MsgSignature><![CDATA[096d8cda45e4678ca23460f6b8cd281b3faf1fc3]]></MsgSignature>
14
+ <TimeStamp>1415979516</TimeStamp>
15
+ <Nonce><![CDATA[1320562132]]></Nonce>
16
+ </xml>"""
17
+ }
18
+
19
+ it "decrypts message" do
20
+ decrypted_message = whisper.decrypt_message(encrypted_message, nonce, timestamp)
21
+ expect(decrypted_message).to eq message
22
+ end
23
+
24
+ it "encryptes message" do
25
+ expect(SecureRandom).to receive(:hex).with(8).and_return("HLFOQjbkfgUh46s8")
26
+ encrypted_msg = whisper.encrypt_message(message, nonce, timestamp)
27
+
28
+ expect(encrypted_msg).to eq encrypted_message
29
+ end
30
+
31
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: we_whisper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Qi He
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.6
27
+ description: |2
28
+ Wechat(微信) open platform requires requests/messages to be encrypted. This
29
+ gem wrapps the encryption.
30
+ email: qihe229@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - lib/we_whisper/cipher.rb
36
+ - lib/we_whisper/message.rb
37
+ - lib/we_whisper/signature.rb
38
+ - lib/we_whisper/version.rb
39
+ - lib/we_whisper/whisper.rb
40
+ - lib/we_whisper.rb
41
+ - spec/spec_helper.rb
42
+ - spec/we_whisper/message_spec.rb
43
+ - spec/we_whisper/signature_spec.rb
44
+ - spec/we_whisper/whisper_spec.rb
45
+ homepage: http://github.com/he9qi/we_whisper
46
+ licenses:
47
+ - MIT
48
+ metadata: {}
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 2.0.14
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: A Ruby Wrapper for Wechat Message Encryption.
69
+ test_files:
70
+ - spec/spec_helper.rb
71
+ - spec/we_whisper/message_spec.rb
72
+ - spec/we_whisper/signature_spec.rb
73
+ - spec/we_whisper/whisper_spec.rb