|
| 1 | +require 'spec_helper' |
| 2 | + |
| 3 | +describe Webpush::Legacy::Encryption do |
| 4 | + describe '#encrypt' do |
| 5 | + let(:curve) do |
| 6 | + group = 'prime256v1' |
| 7 | + OpenSSL::PKey::EC.generate(group) |
| 8 | + end |
| 9 | + |
| 10 | + let(:p256dh) do |
| 11 | + ecdh_key = curve.public_key.to_bn.to_s(2) |
| 12 | + Base64.urlsafe_encode64(ecdh_key) |
| 13 | + end |
| 14 | + |
| 15 | + let(:auth) { Base64.urlsafe_encode64(Random.new.bytes(16)) } |
| 16 | + |
| 17 | + it 'returns ECDH encrypted cipher text, salt, and server_public_key' do |
| 18 | + payload = Webpush::Legacy::Encryption.encrypt('Hello World', p256dh, auth) |
| 19 | + expect(decrypt(payload)).to eq('Hello World') |
| 20 | + end |
| 21 | + |
| 22 | + it 'returns error when message is blank' do |
| 23 | + expect { Webpush::Legacy::Encryption.encrypt(nil, p256dh, auth) }.to raise_error(ArgumentError) |
| 24 | + expect { Webpush::Legacy::Encryption.encrypt('', p256dh, auth) }.to raise_error(ArgumentError) |
| 25 | + end |
| 26 | + |
| 27 | + it 'returns error when p256dh is blank' do |
| 28 | + expect { Webpush::Legacy::Encryption.encrypt('Hello world', nil, auth) }.to raise_error(ArgumentError) |
| 29 | + expect { Webpush::Legacy::Encryption.encrypt('Hello world', '', auth) }.to raise_error(ArgumentError) |
| 30 | + end |
| 31 | + |
| 32 | + it 'returns error when auth is blank' do |
| 33 | + expect { Webpush::Legacy::Encryption.encrypt('Hello world', p256dh, '') }.to raise_error(ArgumentError) |
| 34 | + expect { Webpush::Legacy::Encryption.encrypt('Hello world', p256dh, nil) }.to raise_error(ArgumentError) |
| 35 | + end |
| 36 | + |
| 37 | + # Bug fix for https://github.com/zaru/webpush/issues/22 |
| 38 | + it 'handles unpadded base64 encoded subscription keys' do |
| 39 | + unpadded_p256dh = p256dh.gsub(/=*\Z/, '') |
| 40 | + unpadded_auth = auth.gsub(/=*\Z/, '') |
| 41 | + |
| 42 | + payload = Webpush::Legacy::Encryption.encrypt('Hello World', unpadded_p256dh, unpadded_auth) |
| 43 | + expect(decrypt(payload)).to eq('Hello World') |
| 44 | + end |
| 45 | + |
| 46 | + def decrypt(payload) |
| 47 | + salt = payload.fetch(:salt) |
| 48 | + serverkey16bn = payload.fetch(:server_public_key_bn) |
| 49 | + ciphertext = payload.fetch(:ciphertext) |
| 50 | + |
| 51 | + group_name = 'prime256v1' |
| 52 | + group = OpenSSL::PKey::EC::Group.new(group_name) |
| 53 | + server_public_key_bn = OpenSSL::BN.new(serverkey16bn.unpack('H*').first, 16) |
| 54 | + server_public_key = OpenSSL::PKey::EC::Point.new(group, server_public_key_bn) |
| 55 | + shared_secret = curve.dh_compute_key(server_public_key) |
| 56 | + |
| 57 | + client_public_key_bn = curve.public_key.to_bn |
| 58 | + client_auth_token = Webpush.decode64(auth) |
| 59 | + |
| 60 | + info = "Content-Encoding: auth\0" |
| 61 | + context = create_context(curve.public_key, server_public_key) |
| 62 | + content_encryption_key_info = "Content-Encoding: aesgcm\0P-256#{context}" |
| 63 | + nonce_info = "Content-Encoding: nonce\0P-256#{context}" |
| 64 | + |
| 65 | + prk = HKDF.new(shared_secret, salt: client_auth_token, algorithm: 'SHA256', info: info).next_bytes(32) |
| 66 | + |
| 67 | + content_encryption_key = HKDF.new(prk, salt: salt, info: content_encryption_key_info).next_bytes(16) |
| 68 | + nonce = HKDF.new(prk, salt: salt, info: nonce_info).next_bytes(12) |
| 69 | + |
| 70 | + decrypt_ciphertext(ciphertext, content_encryption_key, nonce) |
| 71 | + end |
| 72 | + |
| 73 | + def create_context(client_public_key, server_public_key) |
| 74 | + c = client_public_key.to_bn.to_s(2) |
| 75 | + s = server_public_key.to_bn.to_s(2) |
| 76 | + context = "\0" |
| 77 | + context += [c.bytesize].pack("n*") |
| 78 | + context += c |
| 79 | + context += [s.bytesize].pack("n*") |
| 80 | + context += s |
| 81 | + context |
| 82 | + end |
| 83 | + |
| 84 | + def decrypt_ciphertext(ciphertext, content_encryption_key, nonce) |
| 85 | + secret_data = ciphertext.byteslice(0, ciphertext.bytesize-16) |
| 86 | + auth = ciphertext.byteslice(ciphertext.bytesize-16, ciphertext.bytesize) |
| 87 | + decipher = OpenSSL::Cipher.new('aes-128-gcm') |
| 88 | + decipher.decrypt |
| 89 | + decipher.key = content_encryption_key |
| 90 | + decipher.iv = nonce |
| 91 | + decipher.auth_tag = auth |
| 92 | + |
| 93 | + decrypted = decipher.update(secret_data) + decipher.final |
| 94 | + |
| 95 | + e = decrypted.byteslice(0, 2) |
| 96 | + expect(e).to eq("\0\0") |
| 97 | + |
| 98 | + decrypted.byteslice(2, decrypted.bytesize-2) |
| 99 | + end |
| 100 | + end |
| 101 | +end |
0 commit comments