<commit_before>require 'spec_helper'
require 'ece'

describe Webpush::Encryption do
  describe '#encrypt' do
    let(:curve) do
      group = 'prime256v1'
      OpenSSL::PKey::EC.new(group)
    end

    let(:p256dh) do
      curve.generate_key
      ecdh_key = curve.public_key.to_bn.to_s(2)
      encode64(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)
      salt = payload.byteslice(0, 16)
      rs = payload.byteslice(16, 4).unpack("N*").first
      idlen = payload.byteslice(20).unpack("C*").first
      serverkey16bn = payload.byteslice(21, idlen)
      ciphertext = payload.byteslice(21 + idlen + 1, rs)

      expect(payload.bytesize).to eq(21 + idlen + rs)

      group_name = 'prime256v1'
      group = OpenSSL::PKey::EC::Group.new(group_name)
      server_public_key_bn = OpenSSL::BN.new(serverkey16bn.unpack('H*').first, 16)
      server_public_key = OpenSSL::PKey::EC::Point.new(group, server_public_key_bn)
      shared_secret = curve.dh_compute_key(server_public_key)

      decrypted_data = ECE.decrypt(ciphertext,
                                   key: shared_secret,
                                   salt: salt,
                                   server_public_key: serverkey16bn,
                                   user_public_key: decode64(p256dh),
                                   auth: decode64(auth))

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

    it 'returns error when message is blank' do
      expect { Webpush::Encryption.encrypt(nil, p256dh, auth) }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('', p256dh, auth) }.to raise_error(ArgumentError)
    end

    it 'returns error when p256dh is blank' do
      expect { Webpush::Encryption.encrypt('Hello world', nil, auth) }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('Hello world', '', auth) }.to raise_error(ArgumentError)
    end

    it 'returns error when auth is blank' do
      expect { Webpush::Encryption.encrypt('Hello world', p256dh, '') }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('Hello world', p256dh, nil) }.to raise_error(ArgumentError)
    end

    # Bug fix for https://github.com/zaru/webpush/issues/22
    it 'handles unpadded base64 encoded subscription keys' do
      unpadded_p256dh = '<KEY>'
      unpadded_auth = '<PASSWORD>'

      payload = Webpush::Encryption.encrypt('Hello World', unpadded_p256dh, unpadded_auth)
      salt = payload.byteslice(0, 16)
      rs = payload.byteslice(16, 4).unpack("N*").first
      idlen = payload.byteslice(20).unpack("C*").first
      serverkey16bn = payload.byteslice(21, idlen)
      ciphertext = payload.byteslice(21 + idlen + 1, rs)

      expect(payload.bytesize).to eq(21 + idlen + rs)

      group_name = 'prime256v1'
      group = OpenSSL::PKey::EC::Group.new(group_name)
      server_public_key_bn = OpenSSL::BN.new(serverkey16bn.unpack('H*').first, 16)
      server_public_key = OpenSSL::PKey::EC::Point.new(group, server_public_key_bn)
      shared_secret = curve.dh_compute_key(server_public_key)

      decrypted_data = ECE.decrypt(ciphertext,
                                   key: shared_secret,
                                   salt: salt,
                                   server_public_key: serverkey16bn,
                                   user_public_key: decode64(pad64(unpadded_p256dh)),
                                   auth: decode64(pad64(unpadded_auth)))

      expect(decrypted_data).to eq('Hello World')
    end
<commit_msg>move shared test code into its own method, also we need to dynamically generate the unpadded values because we need the private key now to generate the shared secret<commit_after>require 'spec_helper'
require 'ece'

describe Webpush::Encryption do
  describe '#encrypt' do
    let(:curve) do
      group = 'prime256v1'
      curve = OpenSSL::PKey::EC.new(group)
      curve.generate_key
      curve
    end

    let(:p256dh) do
      ecdh_key = curve.public_key.to_bn.to_s(2)
      encode64(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)

      payload_vals = extract_payload_values(payload)

      decrypted_data = ECE.decrypt(payload_vals[:ciphertext],
                                   key: payload_vals[:shared_secret],
                                   salt: payload_vals[:salt],
                                   server_public_key: payload_vals[:serverkey16bn],
                                   user_public_key: decode64(p256dh),
                                   auth: decode64(auth))

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

    it 'returns error when message is blank' do
      expect { Webpush::Encryption.encrypt(nil, p256dh, auth) }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('', p256dh, auth) }.to raise_error(ArgumentError)
    end

    it 'returns error when p256dh is blank' do
      expect { Webpush::Encryption.encrypt('Hello world', nil, auth) }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('Hello world', '', auth) }.to raise_error(ArgumentError)
    end

    it 'returns error when auth is blank' do
      expect { Webpush::Encryption.encrypt('Hello world', p256dh, '') }.to raise_error(ArgumentError)
      expect { Webpush::Encryption.encrypt('Hello world', p256dh, nil) }.to raise_error(ArgumentError)
    end

    # Bug fix for https://github.com/zaru/webpush/issues/22
    it 'handles unpadded base64 encoded subscription keys' do
      unpadded_p256dh = p256dh.gsub(/=*\Z/, '')
      unpadded_auth = auth.gsub(/=*\Z/, '')

      payload = Webpush::Encryption.encrypt('Hello World', unpadded_p256dh, unpadded_auth)

      payload_vals = extract_payload_values(payload)

      decrypted_data = ECE.decrypt(payload_vals[:ciphertext],
                                   key: payload_vals[:shared_secret],
                                   salt: payload_vals[:salt],
                                   server_public_key: payload_vals[:serverkey16bn],
                                   user_public_key: decode64(pad64(unpadded_p256dh)),
                                   auth: decode64(pad64(unpadded_auth)))

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

    def extract_payload_values payload
      salt = payload.byteslice(0, 16)
      rs = payload.byteslice(16, 4).unpack("N*").first
      idlen = payload.byteslice(20).unpack("C*").first
      serverkey16bn = payload.byteslice(21, idlen)
      ciphertext = payload.byteslice(21 + idlen + 1, rs)

      expect(payload.bytesize).to eq(21 + idlen + rs)

      group_name = 'prime256v1'
      group = OpenSSL::PKey::EC::Group.new(group_name)
      server_public_key_bn = OpenSSL::BN.new(serverkey16bn.unpack('H*').first, 16)
      server_public_key = OpenSSL::PKey::EC::Point.new(group, server_public_key_bn)
      shared_secret = curve.dh_compute_key(server_public_key)

      {
          ciphertext: ciphertext,
          shared_secret: shared_secret,
          salt: salt,
          serverkey16bn: serverkey16bn
      }
    end

{'fraction_non_alphanumeric': 0.10920712634298925, 'fraction_numerical': 0.028559771521827825, 'mean_word_length': 2.603135717785399, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7197893', 'n_tokens_mistral': 2448, 'n_tokens_neox': 2244, 'n_words': 458}