Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract encryption module #4

Merged
merged 3 commits into from
May 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 4 additions & 83 deletions lib/webpush.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
require 'webpush/version'
require 'openssl'
require 'base64'
require 'hkdf'
require 'net/http'
require 'json'

require 'webpush/version'
require 'webpush/encryption'

module Webpush

# It is temporary URL until supported by the GCM server.
Expand All @@ -14,10 +16,8 @@ module Webpush
class << self
def payload_send(message:, endpoint:, p256dh:, auth:, api_key: "")
endpoint = endpoint.gsub(GCM_URL, TEMP_GCM_URL)
p256dh = unescape_base64(p256dh)
auth = unescape_base64(auth)

payload = encrypt(message, p256dh, auth)
payload = Webpush::Encryption.encrypt(message, p256dh, auth)
push_server_post(endpoint, payload, api_key)
end

Expand Down Expand Up @@ -45,84 +45,5 @@ def push_server_post(endpoint, payload, api_key = "")
return false
end
end

def encrypt(message, p256dh, auth)
group_name = "prime256v1"
salt = Random.new.bytes(16)

server = OpenSSL::PKey::EC.new(group_name)
server.generate_key
server_public_key_bn = server.public_key.to_bn

group = OpenSSL::PKey::EC::Group.new(group_name)
client_public_key_hex = Base64.decode64(p256dh).unpack("H*").first
client_public_key_bn = OpenSSL::BN.new(client_public_key_hex, 16)
client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)

shared_secret = server.dh_compute_key(client_public_key)

clientAuthToken = Base64.decode64(auth)

prk = HKDF.new(shared_secret, :salt => clientAuthToken, :algorithm => 'SHA256', :info => "Content-Encoding: auth\0").next_bytes(32)

context = create_context(client_public_key_bn, server_public_key_bn)

content_encryption_key_info = create_info('aesgcm', context)
content_encryption_key = HKDF.new(prk, :salt => salt, :info => content_encryption_key_info).next_bytes(16)

nonce_info = create_info('nonce', context)
nonce = HKDF.new(prk, :salt => salt, :info => nonce_info).next_bytes(12)

ciphertext = encrypt_payload(message, content_encryption_key, nonce)

{
ciphertext: ciphertext,
salt: salt,
server_public_key_bn: convert16bit(server_public_key_bn)
}
end

def create_context(clientPublicKey, serverPublicKey)
c = convert16bit(clientPublicKey)
s = convert16bit(serverPublicKey)
context = "\0"
context += [c.bytesize].pack("n*")
context += c
context += [s.bytesize].pack("n*")
context += s
context
end

def encrypt_payload(plaintext, content_encryption_key, nonce)
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.encrypt
cipher.key = content_encryption_key
cipher.iv = nonce
padding = cipher.update("\0\0")
text = cipher.update(plaintext)

e_text = padding + text + cipher.final
e_tag = cipher.auth_tag

e_text + e_tag
end

def create_info(type, context)
info = "Content-Encoding: "
info += type
info += "\0"
info += "P-256"
info += context
info
end

def convert16bit(key)
[key.to_s(16)].pack("H*")
end

def unescape_base64(base64)
base64.gsub(/_|\-/, "_" => "/", "-" => "+")
end
end

end
81 changes: 81 additions & 0 deletions lib/webpush/encryption.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module Webpush
module Encryption
extend self

def encrypt(message, p256dh, auth)
group_name = "prime256v1"
salt = Random.new.bytes(16)

server = OpenSSL::PKey::EC.new(group_name)
server.generate_key
server_public_key_bn = server.public_key.to_bn

group = OpenSSL::PKey::EC::Group.new(group_name)
client_public_key_bn = OpenSSL::BN.new(Base64.urlsafe_decode64(p256dh), 2)
client_public_key = OpenSSL::PKey::EC::Point.new(group, client_public_key_bn)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creation of the client_public_key was simplified by using Base64.urlsafe_decode64(p256dh), 2 to construct the the OpenSSL::BN object.


shared_secret = server.dh_compute_key(client_public_key)

client_auth_token = Base64.urlsafe_decode64(auth)

prk = HKDF.new(shared_secret, :salt => client_auth_token, :algorithm => 'SHA256', :info => "Content-Encoding: auth\0").next_bytes(32)

context = create_context(client_public_key_bn, server_public_key_bn)

content_encryption_key_info = create_info('aesgcm', context)
content_encryption_key = HKDF.new(prk, :salt => salt, :info => content_encryption_key_info).next_bytes(16)

nonce_info = create_info('nonce', context)
nonce = HKDF.new(prk, :salt => salt, :info => nonce_info).next_bytes(12)

ciphertext = encrypt_payload(message, content_encryption_key, nonce)

{
ciphertext: ciphertext,
salt: salt,
server_public_key_bn: convert16bit(server_public_key_bn),
shared_secret: shared_secret
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared secret was added to the return value of #encrypt so it is possible to decrypt the ciphertext, as for testing.

}
end

private

def create_context(clientPublicKey, serverPublicKey)
c = convert16bit(clientPublicKey)
s = convert16bit(serverPublicKey)
context = "\0"
context += [c.bytesize].pack("n*")
context += c
context += [s.bytesize].pack("n*")
context += s
context
end

def encrypt_payload(plaintext, content_encryption_key, nonce)
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.encrypt
cipher.key = content_encryption_key
cipher.iv = nonce
padding = cipher.update("\0\0")
text = cipher.update(plaintext)

e_text = padding + text + cipher.final
e_tag = cipher.auth_tag

e_text + e_tag
end

def create_info(type, context)
info = "Content-Encoding: "
info += type
info += "\0"
info += "P-256"
info += context
info
end

def convert16bit(key)
[key.to_s(16)].pack("H*")
end
end
end
42 changes: 42 additions & 0 deletions spec/webpush/encryption_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'spec_helper'
require 'ece'

describe Webpush::Encryption do
describe "#encrypt" do
let(:p256dh) do
encode64(generate_ecdh_key)
end

let(:auth) { encode64(Random.new.bytes(16)) }

it "returns ECDH encrypted cipher text, salt, and server_public_key" do
payload = Webpush::Encryption.encrypt("Hello World", p256dh, auth)

encrypted = payload.fetch(:ciphertext)

decrypted_data = ECE.decrypt(encrypted,
key: payload.fetch(:shared_secret),
salt: payload.fetch(:salt),
server_public_key: payload.fetch(:server_public_key_bn),
user_public_key: decode64(p256dh),
auth: decode64(auth))

expect(decrypted_data).to eq("Hello World")
end

def generate_ecdh_key
group = "prime256v1"
curve = OpenSSL::PKey::EC.new(group)
curve.generate_key
curve.public_key.to_bn.to_s(2)
end

def encode64(bytes)
Base64.urlsafe_encode64(bytes)
end

def decode64(bytes)
Base64.urlsafe_decode64(bytes)
end
end
end
4 changes: 0 additions & 4 deletions spec/webpush_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@
it 'has a version number' do
expect(Webpush::VERSION).not_to be nil
end

it 'does something useful' do
expect(false).to eq(true)
end
end
1 change: 1 addition & 0 deletions webpush.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 1.11"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "ece", "~> 0.2"
end