This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
executable file
335 lines (272 sloc)
13.8 KB
| /* 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.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.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) | |
| } | |
| } |