we_whisper 0.0.1

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: 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