Skip to content

Commit 6ed21ad

Browse files
committed
first commit
0 parents  commit 6ed21ad

20 files changed

+653
-0
lines changed

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/.bundle/
2+
/.yardoc
3+
/Gemfile.lock
4+
/_yardoc/
5+
/coverage/
6+
/doc/
7+
/pkg/
8+
/spec/reports/
9+
/tmp/

.idea/.name

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.rakeTasks

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/webpush.iml

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/workspace.xml

+347
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.rspec

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
--format documentation
2+
--color

.travis.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
language: ruby
2+
rvm:
3+
- 2.3.0
4+
before_install: gem install bundler -v 1.11.2

Gemfile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
source 'https://rubygems.org'
2+
3+
# Specify your gem's dependencies in webpush.gemspec
4+
gemspec

README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# WebPush
2+
3+
This Gem will send the Web Push API. It supports the encryption necessary to payload.
4+
5+
Payload is supported by Chrome50+.
6+
7+
## Installation
8+
9+
Add this line to your application's Gemfile:
10+
11+
```ruby
12+
gem 'webpush'
13+
```
14+
15+
And then execute:
16+
17+
$ bundle
18+
19+
Or install it yourself as:
20+
21+
$ gem install webpush
22+
23+
## Usage
24+
25+
```
26+
message = {
27+
hoge: "piyo"
28+
}
29+
30+
Webpush.payload_send(message: JSON.generate(message),
31+
endpoint: "https://android.googleapis.com/gcm/send/eah7hak....",
32+
p256dh: "BO/aG9nYXNkZmFkc2ZmZHNmYWRzZmFl...",
33+
auth: "aW1hcmthcmFpa3V6ZQ==",
34+
api_key: "[GoogleDeveloper APIKEY]")
35+
```
36+
37+
## Contributing
38+
39+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/webpush.

Rakefile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require "bundler/gem_tasks"
2+
require "rspec/core/rake_task"
3+
4+
RSpec::Core::RakeTask.new(:spec)
5+
6+
task :default => :spec

bin/console

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env ruby
2+
3+
require "bundler/setup"
4+
require "webpush"
5+
6+
# You can add fixtures and/or initialization code here to make experimenting
7+
# with your gem easier. You can also use a different console, if you like.
8+
9+
# (If you use this, don't forget to add pry to your Gemfile!)
10+
# require "pry"
11+
# Pry.start
12+
13+
require "irb"
14+
IRB.start

bin/setup

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
set -vx
5+
6+
bundle install
7+
8+
# Do any other automated setup that you need to do here

lib/webpush.rb

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
require 'webpush/version'
2+
require 'openssl'
3+
require 'base64'
4+
require 'hkdf'
5+
require 'net/http'
6+
require 'json'
7+
8+
module Webpush
9+
10+
# It is temporary URL until supported by the GCM server.
11+
GCM_URL = 'https://android.googleapis.com/gcm/send'
12+
TEMP_GCM_URL = 'https://gcm-http.googleapis.com/gcm'
13+
14+
class << self
15+
def payload_send(message:, endpoint:, p256dh:, auth:, api_key:)
16+
endpoint = endpoint.gsub(GCM_URL, TEMP_GCM_URL)
17+
p256dh = unescape_base64(p256dh)
18+
auth = unescape_base64(auth)
19+
20+
payload = encrypt(message, p256dh, auth)
21+
gcm_post(endpoint, payload, api_key)
22+
end
23+
24+
private
25+
26+
def gcm_post(endpoint, payload, api_key)
27+
begin
28+
uri = URI.parse(endpoint)
29+
http = Net::HTTP.new(uri.host, uri.port)
30+
http.use_ssl = true
31+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
32+
header = {
33+
"Encryption" => "salt=#{Base64.urlsafe_encode64(payload[:salt]).delete('=')}",
34+
"Crypto-Key" => "dh=#{Base64.urlsafe_encode64(payload[:server_public_key_bn]).delete('=')}",
35+
"Authorization" => "key=#{api_key}"
36+
}
37+
req = Net::HTTP::Post.new(uri.request_uri, header)
38+
req.body = payload[:ciphertext]
39+
res = http.request(req)
40+
return ("201" == res.code) ? true : false
41+
rescue
42+
return false
43+
end
44+
end
45+
46+
def encrypt(message, p256dh, auth)
47+
group_name = "prime256v1"
48+
salt = Random.new.bytes(16)
49+
50+
server = OpenSSL::PKey::EC.new(group_name)
51+
server.generate_key
52+
server_public_key_bn = server.public_key.to_bn
53+
54+
group = OpenSSL::PKey::EC::Group.new(group_name)
55+
client_public_key_hex = Base64.decode64(p256dh).unpack("H*").first
56+
client_public_key_bn = OpenSSL::BN.new(client_public_key_hex, 16)
57+
client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)
58+
59+
shared_secret = server.dh_compute_key(client_public_key)
60+
61+
clientAuthToken = Base64.decode64(auth)
62+
63+
prk = HKDF.new(shared_secret, :salt => clientAuthToken, :algorithm => 'SHA256', :info => "Content-Encoding: auth\0").next_bytes(32)
64+
65+
context = create_context(client_public_key_bn, server_public_key_bn)
66+
67+
content_encryption_key_info = create_info('aesgcm', context)
68+
content_encryption_key = HKDF.new(prk, :salt => salt, :info => content_encryption_key_info).next_bytes(16)
69+
70+
nonce_info = create_info('nonce', context)
71+
nonce = HKDF.new(prk, :salt => salt, :info => nonce_info).next_bytes(12)
72+
73+
ciphertext = encrypt_payload(message, content_encryption_key, nonce)
74+
75+
{
76+
ciphertext: ciphertext,
77+
salt: salt,
78+
server_public_key_bn: convert16bit(server_public_key_bn)
79+
}
80+
end
81+
82+
def create_context(clientPublicKey, serverPublicKey)
83+
c = convert16bit(clientPublicKey)
84+
s = convert16bit(serverPublicKey)
85+
context = "\0"
86+
context += [c.bytesize].pack("n*")
87+
context += c
88+
context += [s.bytesize].pack("n*")
89+
context += s
90+
context
91+
end
92+
93+
def encrypt_payload(plaintext, content_encryption_key, nonce)
94+
cipher = OpenSSL::Cipher.new('aes-128-gcm')
95+
cipher.encrypt
96+
cipher.key = content_encryption_key
97+
cipher.iv = nonce
98+
padding = cipher.update("\0\0")
99+
text = cipher.update(plaintext)
100+
101+
e_text = padding + text + cipher.final
102+
e_tag = cipher.auth_tag
103+
104+
e_text + e_tag
105+
end
106+
107+
def create_info(type, context)
108+
info = "Content-Encoding: "
109+
info += type
110+
info += "\0"
111+
info += "P-256"
112+
info += context
113+
info
114+
end
115+
116+
def convert16bit(key)
117+
[key.to_s(16)].pack("H*")
118+
end
119+
120+
def unescape_base64(base64)
121+
base64.gsub(/_|\-/, "_" => "/", "-" => "+")
122+
end
123+
end
124+
125+
end

lib/webpush/version.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Webpush
2+
VERSION = "0.1.1"
3+
end

spec/spec_helper.rb

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2+
require 'webpush'

spec/webpush_spec.rb

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require 'spec_helper'
2+
3+
describe Webpush do
4+
it 'has a version number' do
5+
expect(Webpush::VERSION).not_to be nil
6+
end
7+
8+
it 'does something useful' do
9+
expect(false).to eq(true)
10+
end
11+
end

webpush.gemspec

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# coding: utf-8
2+
lib = File.expand_path('../lib', __FILE__)
3+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4+
require 'webpush/version'
5+
6+
Gem::Specification.new do |spec|
7+
spec.name = "webpush"
8+
spec.version = Webpush::VERSION
9+
spec.authors = ["zaru@sakuraba"]
10+
spec.email = ["[email protected]"]
11+
12+
spec.summary = %q{Encryption Utilities for Web Push payload. }
13+
spec.homepage = "https://github.com/zaru/webpush"
14+
15+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16+
spec.bindir = "exe"
17+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18+
spec.require_paths = ["lib"]
19+
20+
spec.add_development_dependency "bundler", "~> 1.11"
21+
spec.add_development_dependency "rake", "~> 10.0"
22+
spec.add_development_dependency "rspec", "~> 3.0"
23+
spec.add_development_dependency "hkdf", "~> 0.2"
24+
end

0 commit comments

Comments
 (0)