summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebPushSubscription.java
blob: 7ce9a3d60ce1c17c5b0470ec5f12b017bf02196b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * vim: ts=4 sw=4 expandtab:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import org.mozilla.gecko.util.GeckoBundle;

/**
 * This class represents a single Web Push subscription, as described in the <a
 * href="https://www.w3.org/TR/push-api/">Web Push API</a> specification.
 *
 * <p>This is a low-level interface, allowing applications to do all of the heavy lifting
 * themselves. It is recommended that consumers have a thorough understanding of the Web Push API,
 * especially <a href="https://tools.ietf.org/html/rfc8291">RFC 8291</a>.
 *
 * <p>Only trivial sanity checks are performed on the values held here. The application must ensure
 * it is generating compliant keys/secrets itself.
 */
public class WebPushSubscription implements Parcelable {
  private static final int P256_PUBLIC_KEY_LENGTH = 65;

  /**
   * The Service Worker scope associated with this subscription.
   *
   * @see <a
   *     href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register">ServiceWorker
   *     registration</a>
   */
  @NonNull public final String scope;

  /**
   * The Web Push endpoint for this subscription. This is the URL of a web service which implements
   * the Web Push protocol.
   *
   * @see <a href="https://tools.ietf.org/html/rfc8030#section-5">RFC 8030</a>
   */
  @NonNull public final String endpoint;

  /**
   * This is an optional public key provided by the application server to authenticate itself with
   * the endpoint, formatted according to X9.62.
   *
   * <p>This key is used for VAPID, the Voluntary Application Server Identification (VAPID) for Web
   * Push, from <a href="https://tools.ietf.org/html/rfc8292">RFC 8292</a>.
   *
   * @see <a
   *     href="https://www.w3.org/TR/push-api/#dom-pushsubscriptionoptions-applicationserverkey">applicationServerKey</a>
   * @see <a href="https://tools.ietf.org/html/rfc8291">Message Encryption for Web Push</a>
   */
  @Nullable public final byte[] appServerKey;

  /**
   * The P-256 EC public key, formatted as X9.62, generated by the embedder, to be provided to the
   * app server for message encryption.
   *
   * @see <a
   *     href="https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-p256dh">PushEncryptionKeyName
   *     - p256dh</a>
   * @see <a href="https://tools.ietf.org/html/rfc8291#section-3.1">RFC 8291 section 3.1</a>
   */
  @NonNull public final byte[] browserPublicKey;

  /**
   * 16 byte secret key, generated by the embedder, to be provided to the app server for use in
   * encrypting and authenticating messages sent to the {@link #endpoint}.
   *
   * @see <a
   *     href="https://www.w3.org/TR/push-api/#dom-pushencryptionkeyname-auth">PushEncryptionKeyName
   *     - auth</a>
   * @see <a href="https://tools.ietf.org/html/rfc8291#section-3.2">RFC 8291, section 3.2</a>
   */
  @NonNull public final byte[] authSecret;

  @SuppressWarnings("checkstyle:javadocmethod")
  public WebPushSubscription(
      final @NonNull String scope,
      final @NonNull String endpoint,
      final @Nullable byte[] appServerKey,
      final @NonNull byte[] browserPublicKey,
      final @NonNull byte[] authSecret) {
    this.scope = scope;
    this.endpoint = endpoint;
    this.appServerKey = appServerKey;
    this.browserPublicKey = browserPublicKey;
    this.authSecret = authSecret;

    if (appServerKey != null) {
      if (appServerKey.length != P256_PUBLIC_KEY_LENGTH) {
        throw new IllegalArgumentException(
            String.format("appServerKey should be %d bytes", P256_PUBLIC_KEY_LENGTH));
      }

      if (Arrays.equals(appServerKey, browserPublicKey)) {
        throw new IllegalArgumentException("appServerKey and browserPublicKey must differ");
      }
    }

    if (browserPublicKey.length != P256_PUBLIC_KEY_LENGTH) {
      throw new IllegalArgumentException(
          String.format("browserPublicKey should be %d bytes", P256_PUBLIC_KEY_LENGTH));
    }

    if (authSecret.length != 16) {
      throw new IllegalArgumentException("authSecret must be 128 bits");
    }
  }

  private WebPushSubscription(final Parcel in) {
    this.scope = in.readString();
    this.endpoint = in.readString();

    if (ParcelableUtils.readBoolean(in)) {
      this.appServerKey = new byte[P256_PUBLIC_KEY_LENGTH];
      in.readByteArray(this.appServerKey);
    } else {
      appServerKey = null;
    }

    this.browserPublicKey = new byte[P256_PUBLIC_KEY_LENGTH];
    in.readByteArray(this.browserPublicKey);

    this.authSecret = new byte[16];
    in.readByteArray(this.authSecret);
  }

  /* package */ GeckoBundle toBundle() {
    final GeckoBundle bundle = new GeckoBundle(5);
    bundle.putString("scope", scope);
    bundle.putString("endpoint", endpoint);
    if (appServerKey != null) {
      bundle.putString("appServerKey", Base64Utils.encode(appServerKey));
    }
    bundle.putString("browserPublicKey", Base64Utils.encode(browserPublicKey));
    bundle.putString("authSecret", Base64Utils.encode(authSecret));
    return bundle;
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(final Parcel out, final int flags) {
    out.writeString(scope);
    out.writeString(endpoint);

    ParcelableUtils.writeBoolean(out, appServerKey != null);
    if (appServerKey != null) {
      out.writeByteArray(appServerKey);
    }

    out.writeByteArray(browserPublicKey);
    out.writeByteArray(authSecret);
  }

  public static final Parcelable.Creator<WebPushSubscription> CREATOR =
      new Parcelable.Creator<WebPushSubscription>() {
        @Override
        @AnyThread
        public WebPushSubscription createFromParcel(final Parcel parcel) {
          return new WebPushSubscription(parcel);
        }

        @Override
        @AnyThread
        public WebPushSubscription[] newArray(final int size) {
          return new WebPushSubscription[size];
        }
      };
}