we_whisper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/we_whisper/cipher.rb +72 -0
- data/lib/we_whisper/message.rb +40 -0
- data/lib/we_whisper/signature.rb +11 -0
- data/lib/we_whisper/version.rb +3 -0
- data/lib/we_whisper/whisper.rb +61 -0
- data/lib/we_whisper.rb +6 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/we_whisper/message_spec.rb +38 -0
- data/spec/we_whisper/signature_spec.rb +15 -0
- data/spec/we_whisper/whisper_spec.rb +31 -0
- metadata +73 -0
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,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
data/spec/spec_helper.rb
ADDED
@@ -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
|