mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 15:28:39 +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)
|
||
}
|
||
}
|