Dactyloidae/mobile/ios/Push/PushCrypto.swift
2026-06-26 21:04:09 -07:00

335 lines
14 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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/. */
import Foundation
import FxA
/// Class to wrap ecec which does the encryption, decryption and key generation with OpenSSL.
/// This supports aesgcm and the newer aes128gcm.
/// For each standard of decryption, two methods are supplied: one with Data parameters and return value,
/// and one with a String based one.
class PushCrypto {
// Stateless, we provide a singleton for convenience.
open static var sharedInstance = PushCrypto()
}
// AES128GCM
extension PushCrypto {
func aes128gcm(payload data: String, decryptWith privateKey: String, authenticateWith authKey: String) throws -> String {
guard let authSecret = authKey.base64urlSafeDecodedData,
let rawRecvPrivKey = privateKey.base64urlSafeDecodedData,
let payload = data.base64urlSafeDecodedData else {
throw PushCryptoError.base64DecodeError
}
let decrypted = try aes128gcm(payload: payload,
decryptWith: rawRecvPrivKey,
authenticateWith: authSecret)
guard let plaintext = decrypted.utf8EncodedString else {
throw PushCryptoError.utf8EncodingError
}
return plaintext
}
func aes128gcm(payload: Data, decryptWith rawRecvPrivKey: Data, authenticateWith authSecret: Data) throws -> Data {
var plaintextLen = ece_aes128gcm_plaintext_max_length(payload.getBytes(), payload.count) + 1
var plaintext = [UInt8](repeating: 0, count: plaintextLen)
let err = ece_webpush_aes128gcm_decrypt(
rawRecvPrivKey.getBytes(), rawRecvPrivKey.count,
authSecret.getBytes(), authSecret.count,
payload.getBytes(), payload.count,
&plaintext, &plaintextLen)
if err != ECE_OK {
throw PushCryptoError.decryptionError(errCode: err)
}
return Data(bytes: plaintext, count: plaintextLen)
}
func aes128gcm(plaintext: String, encryptWith rawRecvPubKey: String, authenticateWith authSecret: String, rs: Int, padLen: Int) throws -> String {
guard let rawRecvPubKey = rawRecvPubKey.base64urlSafeDecodedData,
let authSecret = authSecret.base64urlSafeDecodedData else {
throw PushCryptoError.base64DecodeError
}
let plaintextData = plaintext.utf8EncodedData
let payloadData = try aes128gcm(plaintext: plaintextData,
encryptWith: rawRecvPubKey,
authenticateWith: authSecret,
rs: rs, padLen: padLen)
guard let payload = payloadData.base64urlSafeEncodedString else {
throw PushCryptoError.base64EncodeError
}
return payload
}
func aes128gcm(plaintext: Data, encryptWith rawRecvPubKey: Data, authenticateWith authSecret: Data, rs rsInt: Int, padLen: Int) throws -> Data {
let rs = UInt32(rsInt)
// rs needs to be >= 18.
assert(rsInt >= Int(ECE_AES128GCM_MIN_RS))
var payloadLen = ece_aes128gcm_payload_max_length(rs, padLen, plaintext.count) + 1
var payload = [UInt8](repeating: 0, count: payloadLen)
let err = ece_webpush_aes128gcm_encrypt(rawRecvPubKey.getBytes(), rawRecvPubKey.count,
authSecret.getBytes(), authSecret.count,
rs, padLen,
plaintext.getBytes(), plaintext.count,
&payload, &payloadLen)
if err != ECE_OK {
throw PushCryptoError.encryptionError(errCode: err)
}
return Data(bytes: payload, count: payloadLen)
}
}
// AESGCM
extension PushCrypto {
func aesgcm(ciphertext data: String, withHeaders headers: PushCryptoHeaders, decryptWith privateKey: String, authenticateWith authKey: String) throws -> String {
guard let authSecret = authKey.base64urlSafeDecodedData,
let rawRecvPrivKey = privateKey.base64urlSafeDecodedData,
let ciphertext = data.base64urlSafeDecodedData else {
throw PushCryptoError.base64DecodeError
}
let decrypted = try aesgcm(ciphertext: ciphertext,
withHeaders: headers,
decryptWith: rawRecvPrivKey,
authenticateWith: authSecret)
guard let plaintext = decrypted.utf8EncodedString else {
throw PushCryptoError.utf8EncodingError
}
return plaintext
}
func aesgcm(ciphertext: Data, withHeaders headers: PushCryptoHeaders, decryptWith rawRecvPrivKey: Data, authenticateWith authSecret: Data) throws -> Data {
let saltLength = Int(ECE_SALT_LENGTH)
var salt = [UInt8](repeating: 0, count: saltLength)
let rawSenderPubKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH)
var rawSenderPubKey = [UInt8](repeating: 0, count: rawSenderPubKeyLength)
var rs = UInt32(0)
let paramsErr = ece_webpush_aesgcm_headers_extract_params(
headers.cryptoKey, headers.encryption,
&salt, saltLength,
&rawSenderPubKey, rawSenderPubKeyLength,
&rs)
if paramsErr != ECE_OK {
throw PushCryptoError.decryptionError(errCode: paramsErr)
}
var plaintextLen = ece_aesgcm_plaintext_max_length(rs, ciphertext.count) + 1
var plaintext = [UInt8](repeating: 0, count: plaintextLen)
let decryptErr = ece_webpush_aesgcm_decrypt(
rawRecvPrivKey.getBytes(), rawRecvPrivKey.count,
authSecret.getBytes(), authSecret.count,
&salt, salt.count,
&rawSenderPubKey, rawSenderPubKey.count,
rs,
ciphertext.getBytes(), ciphertext.count,
&plaintext, &plaintextLen)
if decryptErr != ECE_OK {
throw PushCryptoError.decryptionError(errCode: decryptErr)
}
return Data(bytes: plaintext, count: plaintextLen)
}
func aesgcm(plaintext: String, encryptWith rawRecvPubKey: String, authenticateWith authSecret: String, rs: Int, padLen: Int) throws -> (headers: PushCryptoHeaders, ciphertext: String) {
guard let rawRecvPubKey = rawRecvPubKey.base64urlSafeDecodedData,
let authSecret = authSecret.base64urlSafeDecodedData else {
throw PushCryptoError.base64DecodeError
}
let plaintextData = plaintext.utf8EncodedData
let (headers, messageData) = try aesgcm(
plaintext: plaintextData,
encryptWith: rawRecvPubKey,
authenticateWith: authSecret,
rs: rs, padLen: padLen)
guard let message = messageData.base64urlSafeEncodedString else {
throw PushCryptoError.base64EncodeError
}
return (headers, message)
}
func aesgcm(plaintext: Data, encryptWith rawRecvPubKey: Data, authenticateWith authSecret: Data, rs rsInt: Int, padLen: Int) throws -> (headers: PushCryptoHeaders, data: Data) {
let rs = UInt32(rsInt)
// rs needs to be >= 3.
assert(rsInt >= Int(ECE_AESGCM_MIN_RS))
var ciphertextLength = ece_aesgcm_ciphertext_max_length(rs, padLen, plaintext.count) + 1
var ciphertext = [UInt8](repeating: 0, count: ciphertextLength)
let saltLength = Int(ECE_SALT_LENGTH)
var salt = [UInt8](repeating: 0, count: saltLength)
let rawSenderPubKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH)
var rawSenderPubKey = [UInt8](repeating: 0, count: rawSenderPubKeyLength)
let encryptErr = ece_webpush_aesgcm_encrypt(rawRecvPubKey.getBytes(), rawRecvPubKey.count,
authSecret.getBytes(), authSecret.count,
rs, padLen,
plaintext.getBytes(), plaintext.count,
&salt, saltLength,
&rawSenderPubKey, rawSenderPubKeyLength,
&ciphertext, &ciphertextLength)
if encryptErr != ECE_OK {
throw PushCryptoError.encryptionError(errCode: encryptErr)
}
var cryptoKeyHeaderLength = 0
var encryptionHeaderLength = 0
let paramsSizeErr = ece_webpush_aesgcm_headers_from_params(
salt, saltLength,
rawSenderPubKey, rawSenderPubKeyLength,
rs,
nil, &cryptoKeyHeaderLength,
nil, &encryptionHeaderLength)
if paramsSizeErr != ECE_OK {
throw PushCryptoError.encryptionError(errCode: paramsSizeErr)
}
var cryptoKeyHeaderBytes = [CChar](repeating: 0, count: cryptoKeyHeaderLength)
var encryptionHeaderBytes = [CChar](repeating: 0, count: encryptionHeaderLength)
let paramsErr = ece_webpush_aesgcm_headers_from_params(
salt, saltLength,
rawSenderPubKey, rawSenderPubKeyLength,
rs,
&cryptoKeyHeaderBytes, &cryptoKeyHeaderLength,
&encryptionHeaderBytes, &encryptionHeaderLength)
if paramsErr != ECE_OK {
throw PushCryptoError.encryptionError(errCode: paramsErr)
}
guard let cryptoKeyHeader = String(data: Data(bytes: cryptoKeyHeaderBytes, count: cryptoKeyHeaderLength),
encoding: .ascii),
let encryptionHeader = String(data: Data(bytes: encryptionHeaderBytes, count: encryptionHeaderLength),
encoding: .ascii) else {
throw PushCryptoError.base64EncodeError
}
let headers = PushCryptoHeaders(encryption: encryptionHeader, cryptoKey: cryptoKeyHeader)
return (headers, Data(bytes: ciphertext, count: ciphertextLength))
}
}
extension PushCrypto {
func generateKeys() throws -> PushKeys {
// The subscription private key. This key should never be sent to the app
// server. It should be persisted with the endpoint and auth secret, and used
// to decrypt all messages sent to the subscription.
let privateKeyLength = Int(ECE_WEBPUSH_PRIVATE_KEY_LENGTH)
var rawRecvPrivKey = [UInt8](repeating: 0, count: privateKeyLength)
// The subscription public key. This key should be sent to the app server,
// and used to encrypt messages. The Push DOM API exposes the public key via
// `pushSubscription.getKey("p256dh")`.
let publicKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH)
var rawRecvPubKey = [UInt8](repeating: 0, count: publicKeyLength)
// The shared auth secret. This secret should be persisted with the
// subscription information, and sent to the app server. The DOM API exposes
// the auth secret via `pushSubscription.getKey("auth")`.
let authSecretLength = Int(ECE_WEBPUSH_AUTH_SECRET_LENGTH)
var authSecret = [UInt8](repeating: 0, count: authSecretLength)
let err = ece_webpush_generate_keys(
&rawRecvPrivKey, privateKeyLength,
&rawRecvPubKey, publicKeyLength,
&authSecret, authSecretLength)
if err != ECE_OK {
throw PushCryptoError.keyGenerationError(errCode: err)
}
guard let privKey = Data(bytes: rawRecvPrivKey, count: privateKeyLength).base64urlSafeEncodedString,
let pubKey = Data(bytes: rawRecvPubKey, count: publicKeyLength).base64urlSafeEncodedString,
let authKey = Data(bytes: authSecret, count: authSecretLength).base64urlSafeEncodedString else {
throw PushCryptoError.base64EncodeError
}
return PushKeys(p256dhPrivateKey: privKey, p256dhPublicKey: pubKey, auth: authKey)
}
}
struct PushKeys {
let p256dhPrivateKey: String
let p256dhPublicKey: String
let auth: String
}
enum PushCryptoError: Error {
case base64DecodeError
case base64EncodeError
case decryptionError(errCode: Int32)
case encryptionError(errCode: Int32)
case keyGenerationError(errCode: Int32)
case utf8EncodingError
}
struct PushCryptoHeaders {
let encryption: String
let cryptoKey: String
}
extension String {
/// Returns a base64 url safe decoding of the given string.
/// The string is allowed to be padded
/// What is padding?: http://stackoverflow.com/a/26632221
var base64urlSafeDecodedData: Data? {
// We call this method twice: once with the last two args as nil, 0 this gets us the length
// of the decoded string.
let length = ece_base64url_decode(self, self.characters.count, ECE_BASE64URL_REJECT_PADDING, nil, 0)
guard length > 0 else {
return nil
}
// The second time, we actually decode, and copy it into a made to measure byte array.
var bytes = [UInt8](repeating: 0, count: length)
let checkLength = ece_base64url_decode(self, self.characters.count, ECE_BASE64URL_REJECT_PADDING, &bytes, length)
guard checkLength == length else {
return nil
}
return Data(bytes: bytes, count: length)
}
}
extension Data {
/// Returns a base64 url safe encoding of the given data.
var base64urlSafeEncodedString: String? {
let length = ece_base64url_encode(self.getBytes(), self.count, ECE_BASE64URL_OMIT_PADDING, nil, 0)
guard length > 0 else {
return nil
}
var bytes = [CChar](repeating: 0, count: length)
let checkLength = ece_base64url_encode(self.getBytes(), self.count, ECE_BASE64URL_OMIT_PADDING, &bytes, length)
guard checkLength == length else {
return nil
}
return String(data: Data(bytes: bytes, count: length), encoding: .ascii)
}
}