mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-21 15:57:31 +09:00
335 lines
14 KiB
Swift
335 lines
14 KiB
Swift
|
|
/* 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)
|
|||
|
|
}
|
|||
|
|
}
|