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