|
1 | 1 | require 'spec_helper'
|
2 |
| -require 'ece' |
3 | 2 |
|
4 | 3 | describe Webpush::Encryption do
|
5 | 4 | describe '#encrypt' do
|
| 5 | + let(:curve) do |
| 6 | + group = 'prime256v1' |
| 7 | + curve = OpenSSL::PKey::EC.new(group) |
| 8 | + curve.generate_key |
| 9 | + curve |
| 10 | + end |
| 11 | + |
6 | 12 | let(:p256dh) do
|
7 |
| - encode64(generate_ecdh_key) |
| 13 | + ecdh_key = curve.public_key.to_bn.to_s(2) |
| 14 | + Base64.urlsafe_encode64(ecdh_key) |
8 | 15 | end
|
9 | 16 |
|
10 |
| - let(:auth) { encode64(Random.new.bytes(16)) } |
| 17 | + let(:auth) { Base64.urlsafe_encode64(Random.new.bytes(16)) } |
11 | 18 |
|
12 | 19 | it 'returns ECDH encrypted cipher text, salt, and server_public_key' do
|
13 | 20 | payload = Webpush::Encryption.encrypt('Hello World', p256dh, auth)
|
14 |
| - |
15 |
| - encrypted = payload.fetch(:ciphertext) |
16 |
| - |
17 |
| - decrypted_data = ECE.decrypt(encrypted, |
18 |
| - key: payload.fetch(:shared_secret), |
19 |
| - salt: payload.fetch(:salt), |
20 |
| - server_public_key: payload.fetch(:server_public_key_bn), |
21 |
| - user_public_key: decode64(p256dh), |
22 |
| - auth: decode64(auth)) |
23 |
| - |
24 |
| - expect(decrypted_data).to eq('Hello World') |
| 21 | + expect(decrypt(payload)).to eq('Hello World') |
25 | 22 | end
|
26 | 23 |
|
27 | 24 | it 'returns error when message is blank' do
|
|
41 | 38 |
|
42 | 39 | # Bug fix for https://github.com/zaru/webpush/issues/22
|
43 | 40 | it 'handles unpadded base64 encoded subscription keys' do
|
44 |
| - unpadded_p256dh = 'BK74n-ZA6kfMDEuCFbQH1Y5T33p39PvnzNeuD5LqTs8cF-uaQFUHn_v5kwV6dYIIL4nFabxghQNF_vlnAXX7OiU' |
45 |
| - unpadded_auth = '1C1PBkJQsVwD9tkuLR1x5A' |
| 41 | + unpadded_p256dh = p256dh.gsub(/=*\Z/, '') |
| 42 | + unpadded_auth = auth.gsub(/=*\Z/, '') |
46 | 43 |
|
47 | 44 | payload = Webpush::Encryption.encrypt('Hello World', unpadded_p256dh, unpadded_auth)
|
48 |
| - encrypted = payload.fetch(:ciphertext) |
| 45 | + expect(decrypt(payload)).to eq('Hello World') |
| 46 | + end |
49 | 47 |
|
50 |
| - decrypted_data = ECE.decrypt(encrypted, |
51 |
| - key: payload.fetch(:shared_secret), |
52 |
| - salt: payload.fetch(:salt), |
53 |
| - server_public_key: payload.fetch(:server_public_key_bn), |
54 |
| - user_public_key: decode64(pad64(unpadded_p256dh)), |
55 |
| - auth: decode64(pad64(unpadded_auth))) |
| 48 | + def decrypt payload |
| 49 | + salt = payload.byteslice(0, 16) |
| 50 | + rs = payload.byteslice(16, 4).unpack("N*").first |
| 51 | + idlen = payload.byteslice(20).unpack("C*").first |
| 52 | + serverkey16bn = payload.byteslice(21, idlen) |
| 53 | + ciphertext = payload.byteslice(21 + idlen, rs) |
56 | 54 |
|
57 |
| - expect(decrypted_data).to eq('Hello World') |
58 |
| - end |
| 55 | + expect(payload.bytesize).to eq(21 + idlen + rs) |
59 | 56 |
|
60 |
| - def generate_ecdh_key |
61 |
| - group = 'prime256v1' |
62 |
| - curve = OpenSSL::PKey::EC.new(group) |
63 |
| - curve.generate_key |
64 |
| - curve.public_key.to_bn.to_s(2) |
65 |
| - end |
| 57 | + group_name = 'prime256v1' |
| 58 | + group = OpenSSL::PKey::EC::Group.new(group_name) |
| 59 | + server_public_key_bn = OpenSSL::BN.new(serverkey16bn.unpack('H*').first, 16) |
| 60 | + server_public_key = OpenSSL::PKey::EC::Point.new(group, server_public_key_bn) |
| 61 | + shared_secret = curve.dh_compute_key(server_public_key) |
66 | 62 |
|
67 |
| - def encode64(bytes) |
68 |
| - Base64.urlsafe_encode64(bytes) |
69 |
| - end |
| 63 | + client_public_key_bn = curve.public_key.to_bn |
| 64 | + client_auth_token = Webpush.decode64(auth) |
| 65 | + |
| 66 | + info = "WebPush: info\0" + client_public_key_bn.to_s(2) + server_public_key_bn.to_s(2) |
| 67 | + content_encryption_key_info = "Content-Encoding: aes128gcm\0" |
| 68 | + nonce_info = "Content-Encoding: nonce\0" |
70 | 69 |
|
71 |
| - def decode64(str) |
72 |
| - Base64.urlsafe_decode64(str) |
| 70 | + prk = HKDF.new(shared_secret, salt: client_auth_token, algorithm: 'SHA256', info: info).next_bytes(32) |
| 71 | + |
| 72 | + content_encryption_key = HKDF.new(prk, salt: salt, info: content_encryption_key_info).next_bytes(16) |
| 73 | + nonce = HKDF.new(prk, salt: salt, info: nonce_info).next_bytes(12) |
| 74 | + |
| 75 | + decrypt_ciphertext(ciphertext, content_encryption_key, nonce) |
73 | 76 | end
|
74 | 77 |
|
75 |
| - def pad64(str) |
76 |
| - str = str.ljust((str.length + 3) & ~3, '=') if !str.end_with?('=') && str.length % 4 != 0 |
77 |
| - str |
| 78 | + def decrypt_ciphertext(ciphertext, content_encryption_key, nonce) |
| 79 | + secret_data = ciphertext.byteslice(0, ciphertext.bytesize-16) |
| 80 | + auth = ciphertext.byteslice(ciphertext.bytesize-16, ciphertext.bytesize) |
| 81 | + decipher = OpenSSL::Cipher.new('aes-128-gcm') |
| 82 | + decipher.decrypt |
| 83 | + decipher.key = content_encryption_key |
| 84 | + decipher.iv = nonce |
| 85 | + decipher.auth_tag = auth |
| 86 | + |
| 87 | + decrypted = decipher.update(secret_data) + decipher.final |
| 88 | + |
| 89 | + e = decrypted.byteslice(-2, decrypted.bytesize) |
| 90 | + expect(e).to eq("\2\0") |
| 91 | + |
| 92 | + decrypted.byteslice(0, decrypted.bytesize-2) |
78 | 93 | end
|
79 | 94 | end
|
80 | 95 | end
|
0 commit comments