summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebPushUtils.java
blob: 340025502e680fde01101d594c56f1033ded45a8 (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
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

package org.mozilla.geckoview.test;

import android.util.Base64;
import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;

/**
 * Utilities for converting {@link ECPublicKey} to/from X9.62 encoding.
 *
 * @see <a href="https://tools.ietf.org/html/rfc8291">Message Encryption for Web Push</a>
 */
/* package */ class WebPushUtils {
  public static final int P256_PUBLIC_KEY_LENGTH = 65; // 1 + 32 + 32
  private static final byte NIST_HEADER = 0x04; // uncompressed format

  private static ECParameterSpec sSpec;

  private WebPushUtils() {}

  /**
   * Encodes an {@link ECPublicKey} into X9.62 format as required by Web Push.
   *
   * @param key the {@link ECPublicKey} to encode
   * @return the encoded {@link ECPublicKey}
   */
  @AnyThread
  public static @Nullable byte[] keyToBytes(final @Nullable ECPublicKey key) {
    if (key == null) {
      return null;
    }

    final ByteBuffer buffer = ByteBuffer.allocate(P256_PUBLIC_KEY_LENGTH);
    buffer.put(NIST_HEADER);

    putUnsignedBigInteger(buffer, key.getW().getAffineX());
    putUnsignedBigInteger(buffer, key.getW().getAffineY());

    if (buffer.position() != P256_PUBLIC_KEY_LENGTH) {
      throw new RuntimeException("Unexpected key length " + buffer.position());
    }

    return buffer.array();
  }

  private static void putUnsignedBigInteger(final ByteBuffer buffer, final BigInteger value) {
    final byte[] bytes = value.toByteArray();
    if (bytes.length < 32) {
      buffer.put(new byte[32 - bytes.length]);
      buffer.put(bytes);
    } else {
      buffer.put(bytes, bytes.length - 32, 32);
    }
  }

  /**
   * Encodes an {@link ECPublicKey} into X9.62 format as required by Web Push, further encoded into
   * Base64.
   *
   * @param key the {@link ECPublicKey} to encode
   * @return the encoded {@link ECPublicKey}
   */
  @AnyThread
  public static @Nullable String keyToString(final @Nullable ECPublicKey key) {
    return Base64.encodeToString(
        keyToBytes(key), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
  }

  /**
   * @return A {@link ECParameterSpec} for P-256 (secp256r1).
   */
  public static ECParameterSpec getP256Spec() {
    if (sSpec == null) {
      try {
        final KeyPairGenerator gen = KeyPairGenerator.getInstance("EC");
        final ECGenParameterSpec genSpec = new ECGenParameterSpec("secp256r1");
        gen.initialize(genSpec);
        sSpec = ((ECPublicKey) gen.generateKeyPair().getPublic()).getParams();
      } catch (final NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
      } catch (final InvalidAlgorithmParameterException e) {
        throw new RuntimeException(e);
      }
    }

    return sSpec;
  }

  /**
   * Converts a Base64 X9.62 encoded Web Push key into a {@link ECPublicKey}.
   *
   * @param base64Bytes the X9.62 data as Base64
   * @return a {@link ECPublicKey}
   */
  @AnyThread
  public static @Nullable ECPublicKey keyFromString(final @Nullable String base64Bytes) {
    if (base64Bytes == null) {
      return null;
    }

    return keyFromBytes(Base64.decode(base64Bytes, Base64.URL_SAFE));
  }

  private static BigInteger readUnsignedBigInteger(
      final byte[] bytes, final int offset, final int length) {
    byte[] mag = bytes;
    if (offset != 0 || length != bytes.length) {
      mag = new byte[length];
      System.arraycopy(bytes, offset, mag, 0, length);
    }
    return new BigInteger(1, mag);
  }

  /**
   * Converts a X9.62 encoded Web Push key into a {@link ECPublicKey}.
   *
   * @param bytes the X9.62 data
   * @return a {@link ECPublicKey}
   */
  @AnyThread
  public static @Nullable ECPublicKey keyFromBytes(final @Nullable byte[] bytes) {
    if (bytes == null) {
      return null;
    }

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

    if (bytes[0] != NIST_HEADER) {
      throw new IllegalArgumentException("Expected uncompressed NIST format");
    }

    try {
      final BigInteger x = readUnsignedBigInteger(bytes, 1, 32);
      final BigInteger y = readUnsignedBigInteger(bytes, 33, 32);

      final ECPoint point = new ECPoint(x, y);
      final ECPublicKeySpec spec = new ECPublicKeySpec(point, getP256Spec());
      final KeyFactory factory = KeyFactory.getInstance("EC");
      final ECPublicKey key = (ECPublicKey) factory.generatePublic(spec);

      return key;
    } catch (final NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    } catch (final InvalidKeySpecException e) {
      throw new RuntimeException(e);
    }
  }
}