Убраны actor-isolation warnings и выровненны версии extension
This commit is contained in:
@@ -273,7 +273,7 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = RosettaNotificationService/RosettaNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = RosettaNotificationService/Info.plist;
|
||||
@@ -283,7 +283,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.8;
|
||||
MARKETING_VERSION = 1.2.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -500,7 +500,7 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = RosettaNotificationService/RosettaNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = RosettaNotificationService/Info.plist;
|
||||
@@ -510,7 +510,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.8;
|
||||
MARKETING_VERSION = 1.2.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
|
||||
|
||||
enum BIP39 {
|
||||
static let wordList: [String] = [
|
||||
nonisolated static let wordList: [String] = [
|
||||
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
|
||||
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
|
||||
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
|
||||
@@ -261,9 +261,9 @@ enum BIP39 {
|
||||
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
|
||||
]
|
||||
|
||||
static let wordSet: Set<String> = Set(wordList)
|
||||
nonisolated static let wordSet: Set<String> = Set(wordList)
|
||||
|
||||
static func index(of word: String) -> Int? {
|
||||
nonisolated static func index(of word: String) -> Int? {
|
||||
wordList.firstIndex(of: word)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,14 +221,17 @@ final class BiometricAuthManager: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// Whether a biometric-protected password exists for the given account.
|
||||
/// Uses `kSecUseAuthenticationUIFail` to check existence WITHOUT triggering Face ID.
|
||||
/// Uses an `LAContext` with `interactionNotAllowed = true` to check existence
|
||||
/// without triggering biometric UI.
|
||||
func hasStoredPassword(forAccount publicKey: String) -> Bool {
|
||||
let key = passwordKey(for: publicKey)
|
||||
let context = LAContext()
|
||||
context.interactionNotAllowed = true
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail,
|
||||
kSecUseAuthenticationContext as String: context,
|
||||
]
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
@@ -36,23 +36,23 @@ enum CryptoError: LocalizedError {
|
||||
/// Low-level primitives are in `CryptoPrimitives`.
|
||||
final class CryptoManager: @unchecked Sendable {
|
||||
|
||||
static let shared = CryptoManager()
|
||||
nonisolated static let shared = CryptoManager()
|
||||
|
||||
// MARK: - Android Parity: PBKDF2 Key Cache
|
||||
|
||||
/// Caches derived PBKDF2 keys to avoid repeated ~50-100ms derivations.
|
||||
/// Android: `CryptoManager.pbkdf2KeyCache` (ConcurrentHashMap).
|
||||
/// Key format: "algorithm::password", value: derived 32-byte key.
|
||||
private let pbkdf2CacheLock = NSLock()
|
||||
private var pbkdf2Cache: [String: Data] = [:]
|
||||
nonisolated private let pbkdf2CacheLock = NSLock()
|
||||
nonisolated(unsafe) private var pbkdf2Cache: [String: Data] = [:]
|
||||
|
||||
// MARK: - Android Parity: Decryption Cache
|
||||
|
||||
/// Caches decrypted results to avoid repeated AES + PBKDF2 for same input.
|
||||
/// Android: `CryptoManager.decryptionCache` (ConcurrentHashMap, max 2000).
|
||||
private static let decryptionCacheMaxSize = 2000
|
||||
private let decryptionCacheLock = NSLock()
|
||||
private var decryptionCache: [String: Data] = [:]
|
||||
nonisolated private static let decryptionCacheMaxSize = 2000
|
||||
nonisolated private let decryptionCacheLock = NSLock()
|
||||
nonisolated(unsafe) private var decryptionCache: [String: Data] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
@@ -236,7 +236,7 @@ final class CryptoManager: @unchecked Sendable {
|
||||
}
|
||||
|
||||
private extension CryptoManager {
|
||||
func decryptWithPassword(
|
||||
nonisolated func decryptWithPassword(
|
||||
ciphertext: Data,
|
||||
iv: Data,
|
||||
password: String,
|
||||
@@ -257,7 +257,7 @@ private extension CryptoManager {
|
||||
|
||||
private extension CryptoManager {
|
||||
|
||||
func mnemonicFromEntropy(_ entropy: Data) throws -> [String] {
|
||||
nonisolated func mnemonicFromEntropy(_ entropy: Data) throws -> [String] {
|
||||
guard entropy.count == 16 else { throw CryptoError.invalidEntropy }
|
||||
|
||||
let hashBytes = Data(SHA256.hash(data: entropy))
|
||||
@@ -283,7 +283,7 @@ private extension CryptoManager {
|
||||
}
|
||||
}
|
||||
|
||||
func entropyFromMnemonic(_ words: [String]) throws -> Data {
|
||||
nonisolated func entropyFromMnemonic(_ words: [String]) throws -> Data {
|
||||
guard words.count == 12 else { throw CryptoError.invalidMnemonic }
|
||||
|
||||
var bits = [Bool]()
|
||||
|
||||
@@ -11,7 +11,7 @@ enum CryptoPrimitives {
|
||||
|
||||
// MARK: - AES-256-CBC
|
||||
|
||||
static func aesCBCEncrypt(_ data: Data, key: Data, iv: Data) throws -> Data {
|
||||
nonisolated static func aesCBCEncrypt(_ data: Data, key: Data, iv: Data) throws -> Data {
|
||||
let outputSize = data.count + kCCBlockSizeAES128
|
||||
var ciphertext = Data(count: outputSize)
|
||||
var numBytes = 0
|
||||
@@ -41,7 +41,7 @@ enum CryptoPrimitives {
|
||||
return ciphertext.prefix(numBytes)
|
||||
}
|
||||
|
||||
static func aesCBCDecrypt(_ data: Data, key: Data, iv: Data) throws -> Data {
|
||||
nonisolated static func aesCBCDecrypt(_ data: Data, key: Data, iv: Data) throws -> Data {
|
||||
let outputSize = data.count + kCCBlockSizeAES128
|
||||
var plaintext = Data(count: outputSize)
|
||||
var numBytes = 0
|
||||
@@ -73,7 +73,7 @@ enum CryptoPrimitives {
|
||||
|
||||
// MARK: - PBKDF2
|
||||
|
||||
static func pbkdf2(
|
||||
nonisolated static func pbkdf2(
|
||||
password: String,
|
||||
salt: String,
|
||||
iterations: Int,
|
||||
@@ -108,7 +108,7 @@ enum CryptoPrimitives {
|
||||
|
||||
// MARK: - Random Bytes
|
||||
|
||||
static func randomBytes(count: Int) throws -> Data {
|
||||
nonisolated static func randomBytes(count: Int) throws -> Data {
|
||||
var data = Data(count: count)
|
||||
let status = data.withUnsafeMutableBytes { ptr -> OSStatus in
|
||||
guard let base = ptr.baseAddress else { return errSecAllocate }
|
||||
@@ -129,7 +129,7 @@ extension CryptoPrimitives {
|
||||
///
|
||||
/// Previously used Apple's `compression_encode_buffer(COMPRESSION_ZLIB)` (raw deflate)
|
||||
/// with a manual zlib wrapper — that output was incompatible with pako.inflate().
|
||||
static func zlibDeflate(_ data: Data) throws -> Data {
|
||||
nonisolated static func zlibDeflate(_ data: Data) throws -> Data {
|
||||
let sourceLen = uLong(data.count)
|
||||
var destLen = compressBound(sourceLen)
|
||||
var dest = Data(count: Int(destLen))
|
||||
@@ -148,7 +148,7 @@ extension CryptoPrimitives {
|
||||
}
|
||||
|
||||
/// Raw deflate compression (no zlib header, compatible with Java Deflater(nowrap=true)).
|
||||
static func rawDeflate(_ data: Data) throws -> Data {
|
||||
nonisolated static func rawDeflate(_ data: Data) throws -> Data {
|
||||
let sourceSize = data.count
|
||||
let destinationSize = sourceSize + 512
|
||||
var destination = Data(count: destinationSize)
|
||||
@@ -172,7 +172,7 @@ extension CryptoPrimitives {
|
||||
/// ⚠️ zlib-wrapped data MUST be stripped FIRST — `tryRawInflate` can produce
|
||||
/// garbage output from zlib header bytes (0x78 0x9C are valid but meaningless
|
||||
/// raw deflate instructions), causing false-positive decompression.
|
||||
static func rawInflate(_ data: Data) throws -> Data {
|
||||
nonisolated static func rawInflate(_ data: Data) throws -> Data {
|
||||
// 1. Strip zlib wrapper FIRST if present (iOS zlibDeflate / desktop pako.deflate).
|
||||
// Must be tried before raw inflate to avoid false-positive decompression
|
||||
// where raw inflate interprets the zlib header as deflate instructions.
|
||||
@@ -189,7 +189,7 @@ extension CryptoPrimitives {
|
||||
throw CryptoError.compressionFailed
|
||||
}
|
||||
|
||||
private static func tryRawInflate(_ data: Data) -> Data? {
|
||||
nonisolated private static func tryRawInflate(_ data: Data) -> Data? {
|
||||
let sourceSize = data.count
|
||||
for multiplier in [4, 8, 16, 32] {
|
||||
let destinationSize = max(sourceSize * multiplier, 256)
|
||||
@@ -218,7 +218,7 @@ extension Data {
|
||||
}
|
||||
|
||||
/// Initialize from a hex string (case-insensitive).
|
||||
init(hexString: String) {
|
||||
nonisolated init(hexString: String) {
|
||||
let hex = hexString.lowercased()
|
||||
var data = Data(capacity: hex.count / 2)
|
||||
var index = hex.startIndex
|
||||
|
||||
@@ -24,7 +24,7 @@ enum MessageCrypto {
|
||||
/// Decrypts an incoming message and returns both plaintext and the working key+nonce.
|
||||
/// The returned `keyAndNonce` is the candidate that successfully decrypted the message —
|
||||
/// critical for deriving the correct attachment password.
|
||||
static func decryptIncomingFull(
|
||||
nonisolated static func decryptIncomingFull(
|
||||
ciphertext: String,
|
||||
encryptedKey: String,
|
||||
myPrivateKeyHex: String
|
||||
@@ -50,7 +50,7 @@ enum MessageCrypto {
|
||||
throw CryptoError.invalidData("Failed to decrypt message content with all key candidates")
|
||||
}
|
||||
|
||||
static func decryptIncoming(
|
||||
nonisolated static func decryptIncoming(
|
||||
ciphertext: String,
|
||||
encryptedKey: String,
|
||||
myPrivateKeyHex: String
|
||||
@@ -67,7 +67,7 @@ enum MessageCrypto {
|
||||
/// - plaintext: The message text.
|
||||
/// - recipientPublicKeyHex: Recipient's secp256k1 compressed public key (hex).
|
||||
/// - Returns: Tuple of (content: hex ciphertext+tag, chachaKey: base64 encrypted key for recipient, plainKeyAndNonce: raw key+nonce bytes).
|
||||
static func encryptOutgoing(
|
||||
nonisolated static func encryptOutgoing(
|
||||
plaintext: String,
|
||||
recipientPublicKeyHex: String
|
||||
) throws -> (content: String, chachaKey: String, plainKeyAndNonce: Data) {
|
||||
@@ -96,7 +96,7 @@ enum MessageCrypto {
|
||||
|
||||
/// Decrypts an incoming message using already decrypted key+nonce bytes.
|
||||
/// Mirrors Android `decryptIncomingWithPlainKey`.
|
||||
static func decryptIncomingWithPlainKey(
|
||||
nonisolated static func decryptIncomingWithPlainKey(
|
||||
ciphertext: String,
|
||||
plainKeyAndNonce: Data
|
||||
) throws -> String {
|
||||
@@ -109,7 +109,7 @@ enum MessageCrypto {
|
||||
/// Extract raw decrypted key+nonce data from an encrypted key string (ECDH path).
|
||||
/// Verifies each candidate by attempting XChaCha20 decryption to find the correct one.
|
||||
/// Falls back to first candidate if ciphertext is unavailable.
|
||||
static func extractDecryptedKeyData(
|
||||
nonisolated static func extractDecryptedKeyData(
|
||||
encryptedKey: String,
|
||||
myPrivateKeyHex: String,
|
||||
verifyCiphertext: String? = nil
|
||||
@@ -133,7 +133,7 @@ enum MessageCrypto {
|
||||
/// Emulates Android's `String(bytes, UTF_8).toByteArray(ISO_8859_1)` round-trip.
|
||||
/// Uses BOTH WHATWG and Android UTF-8 decoders — returns candidates for each.
|
||||
/// WHATWG and Android decoders handle invalid UTF-8 differently → different bytes.
|
||||
static func androidUtf8BytesToLatin1Bytes(_ utf8Bytes: Data) -> Data {
|
||||
nonisolated static func androidUtf8BytesToLatin1Bytes(_ utf8Bytes: Data) -> Data {
|
||||
// Primary: WHATWG decoder (matches Java's Modified UTF-8 for most cases)
|
||||
let decoded = String(decoding: utf8Bytes, as: UTF8.self)
|
||||
if let latin1 = decoded.data(using: .isoLatin1, allowLossyConversion: true) {
|
||||
@@ -143,7 +143,7 @@ enum MessageCrypto {
|
||||
}
|
||||
|
||||
/// Alternative key recovery using Android UTF-8 decoder.
|
||||
static func androidUtf8BytesToLatin1BytesAlt(_ utf8Bytes: Data) -> Data {
|
||||
nonisolated static func androidUtf8BytesToLatin1BytesAlt(_ utf8Bytes: Data) -> Data {
|
||||
let decoded = bytesToAndroidUtf8String(utf8Bytes)
|
||||
if let latin1 = decoded.data(using: .isoLatin1, allowLossyConversion: true) {
|
||||
return latin1
|
||||
@@ -156,7 +156,7 @@ enum MessageCrypto {
|
||||
/// Returns password candidates from a stored attachment password string.
|
||||
/// New format: `"rawkey:<hex>"` → derives Android (`bytesToJsUtf8String`) + WHATWG passwords.
|
||||
/// Legacy format: plain string → used as-is (backward compat with persisted messages).
|
||||
static func attachmentPasswordCandidates(from stored: String) -> [String] {
|
||||
nonisolated static func attachmentPasswordCandidates(from stored: String) -> [String] {
|
||||
if stored.hasPrefix("rawkey:") {
|
||||
let hex = String(stored.dropFirst("rawkey:".count))
|
||||
let keyData = Data(hexString: hex)
|
||||
@@ -193,7 +193,7 @@ enum MessageCrypto {
|
||||
/// Uses feross/buffer npm polyfill UTF-8 decoding semantics.
|
||||
/// Key difference from previous implementation: on invalid sequence, ALWAYS advance by 1 byte
|
||||
/// and emit 1× U+FFFD (not variable bytes/U+FFFD count).
|
||||
static func bytesToAndroidUtf8String(_ bytes: Data) -> String {
|
||||
nonisolated static func bytesToAndroidUtf8String(_ bytes: Data) -> String {
|
||||
var codePoints: [Int] = []
|
||||
codePoints.reserveCapacity(bytes.count)
|
||||
var index = 0
|
||||
@@ -276,7 +276,7 @@ private extension MessageCrypto {
|
||||
/// Decrypts and returns candidate XChaCha20 key+nonce buffers.
|
||||
/// Format: Base64(ivHex:encryptedKeyHex:ephemeralPrivateKeyHex)
|
||||
/// Supports Android sync shorthand `sync:<aesChachaKey>`.
|
||||
static func decryptKeyFromSenderCandidates(encryptedKey: String, myPrivateKeyHex: String) throws -> [Data] {
|
||||
nonisolated static func decryptKeyFromSenderCandidates(encryptedKey: String, myPrivateKeyHex: String) throws -> [Data] {
|
||||
if encryptedKey.hasPrefix("sync:") {
|
||||
let aesChachaKey = String(encryptedKey.dropFirst("sync:".count))
|
||||
guard !aesChachaKey.isEmpty else {
|
||||
@@ -365,7 +365,7 @@ private extension MessageCrypto {
|
||||
}
|
||||
|
||||
/// Encrypts the XChaCha20 key+nonce for a recipient using ECDH.
|
||||
static func encryptKeyForRecipient(keyAndNonce: Data, recipientPublicKeyHex: String) throws -> String {
|
||||
nonisolated static func encryptKeyForRecipient(keyAndNonce: Data, recipientPublicKeyHex: String) throws -> String {
|
||||
let ephemeralPrivKey = try P256K.KeyAgreement.PrivateKey()
|
||||
|
||||
let recipientPubKey = try P256K.KeyAgreement.PublicKey(
|
||||
@@ -399,7 +399,7 @@ private extension MessageCrypto {
|
||||
}
|
||||
|
||||
/// Extracts the 32-byte x-coordinate from ECDH shared secret bytes.
|
||||
static func extractXCoordinate(from sharedSecretData: Data) -> Data {
|
||||
nonisolated static func extractXCoordinate(from sharedSecretData: Data) -> Data {
|
||||
// Uncompressed point: 0x04 || X(32) || Y(32)
|
||||
if sharedSecretData.count == 65, sharedSecretData.first == 0x04 {
|
||||
return sharedSecretData[1..<33]
|
||||
@@ -421,13 +421,13 @@ private extension MessageCrypto {
|
||||
}
|
||||
|
||||
/// Legacy Android compatibility: x-coordinate serialized through BigInteger loses leading zeros.
|
||||
static func legacySharedKey(fromExactX exactX: Data) -> Data {
|
||||
nonisolated static func legacySharedKey(fromExactX exactX: Data) -> Data {
|
||||
let trimmed = exactX.drop(while: { $0 == 0 })
|
||||
return trimmed.isEmpty ? Data([0]) : Data(trimmed)
|
||||
}
|
||||
|
||||
/// JS/Android compatibility: private key hex can arrive without leading zero bytes.
|
||||
static func normalizePrivateKeyHex(_ rawHex: String) -> String {
|
||||
nonisolated static func normalizePrivateKeyHex(_ rawHex: String) -> String {
|
||||
var hex = rawHex
|
||||
if hex.count % 2 != 0 {
|
||||
hex = "0" + hex
|
||||
@@ -441,7 +441,7 @@ private extension MessageCrypto {
|
||||
return hex
|
||||
}
|
||||
|
||||
static func decryptWithKeyAndNonce(ciphertext: String, keyAndNonce: Data) throws -> String {
|
||||
nonisolated static func decryptWithKeyAndNonce(ciphertext: String, keyAndNonce: Data) throws -> String {
|
||||
guard keyAndNonce.count >= 56 else {
|
||||
throw CryptoError.invalidData("Key+nonce must be 56 bytes, got \(keyAndNonce.count)")
|
||||
}
|
||||
|
||||
18
Rosetta/Core/Crypto/NativeCryptoBridge.h
Normal file
18
Rosetta/Core/Crypto/NativeCryptoBridge.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Objective-C++ bridge exposing native C++ XChaCha20-Poly1305 routines to Swift.
|
||||
@interface NativeCryptoBridge : NSObject
|
||||
|
||||
+ (nullable NSData *)xChaCha20Poly1305Encrypt:(NSData *)plaintext
|
||||
key:(NSData *)key
|
||||
nonce:(NSData *)nonce;
|
||||
|
||||
+ (nullable NSData *)xChaCha20Poly1305Decrypt:(NSData *)ciphertextWithTag
|
||||
key:(NSData *)key
|
||||
nonce:(NSData *)nonce;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
65
Rosetta/Core/Crypto/NativeCryptoBridge.mm
Normal file
65
Rosetta/Core/Crypto/NativeCryptoBridge.mm
Normal file
@@ -0,0 +1,65 @@
|
||||
#import "NativeCryptoBridge.h"
|
||||
|
||||
#include "NativeXChaCha20.hpp"
|
||||
|
||||
@implementation NativeCryptoBridge
|
||||
|
||||
+ (nullable NSData *)xChaCha20Poly1305Encrypt:(NSData *)plaintext
|
||||
key:(NSData *)key
|
||||
nonce:(NSData *)nonce {
|
||||
if (key.length != 32 || nonce.length != 24) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const auto *plainBytes = static_cast<const std::uint8_t *>(plaintext.bytes);
|
||||
const auto *keyBytes = static_cast<const std::uint8_t *>(key.bytes);
|
||||
const auto *nonceBytes = static_cast<const std::uint8_t *>(nonce.bytes);
|
||||
|
||||
std::vector<std::uint8_t> encrypted;
|
||||
const bool ok = rosetta::nativecrypto::xchacha20poly1305_encrypt(
|
||||
plainBytes,
|
||||
static_cast<std::size_t>(plaintext.length),
|
||||
keyBytes,
|
||||
nonceBytes,
|
||||
encrypted
|
||||
);
|
||||
if (!ok) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (encrypted.empty()) {
|
||||
return [NSData data];
|
||||
}
|
||||
return [NSData dataWithBytes:encrypted.data() length:encrypted.size()];
|
||||
}
|
||||
|
||||
+ (nullable NSData *)xChaCha20Poly1305Decrypt:(NSData *)ciphertextWithTag
|
||||
key:(NSData *)key
|
||||
nonce:(NSData *)nonce {
|
||||
if (key.length != 32 || nonce.length != 24 || ciphertextWithTag.length < 16) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const auto *cipherBytes = static_cast<const std::uint8_t *>(ciphertextWithTag.bytes);
|
||||
const auto *keyBytes = static_cast<const std::uint8_t *>(key.bytes);
|
||||
const auto *nonceBytes = static_cast<const std::uint8_t *>(nonce.bytes);
|
||||
|
||||
std::vector<std::uint8_t> plaintext;
|
||||
const bool ok = rosetta::nativecrypto::xchacha20poly1305_decrypt(
|
||||
cipherBytes,
|
||||
static_cast<std::size_t>(ciphertextWithTag.length),
|
||||
keyBytes,
|
||||
nonceBytes,
|
||||
plaintext
|
||||
);
|
||||
if (!ok) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (plaintext.empty()) {
|
||||
return [NSData data];
|
||||
}
|
||||
return [NSData dataWithBytes:plaintext.data() length:plaintext.size()];
|
||||
}
|
||||
|
||||
@end
|
||||
413
Rosetta/Core/Crypto/NativeXChaCha20.cpp
Normal file
413
Rosetta/Core/Crypto/NativeXChaCha20.cpp
Normal file
@@ -0,0 +1,413 @@
|
||||
#include "NativeXChaCha20.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kXChaChaKeySize = 32;
|
||||
constexpr std::size_t kXChaChaNonceSize = 24;
|
||||
constexpr std::size_t kChaChaNonceSize = 12;
|
||||
constexpr std::size_t kChaChaBlockSize = 64;
|
||||
constexpr std::size_t kPoly1305TagSize = 16;
|
||||
|
||||
inline std::uint32_t load_le32(const std::uint8_t *src) {
|
||||
return static_cast<std::uint32_t>(src[0])
|
||||
| (static_cast<std::uint32_t>(src[1]) << 8U)
|
||||
| (static_cast<std::uint32_t>(src[2]) << 16U)
|
||||
| (static_cast<std::uint32_t>(src[3]) << 24U);
|
||||
}
|
||||
|
||||
inline std::uint64_t load_le64(const std::uint8_t *src) {
|
||||
return static_cast<std::uint64_t>(src[0])
|
||||
| (static_cast<std::uint64_t>(src[1]) << 8U)
|
||||
| (static_cast<std::uint64_t>(src[2]) << 16U)
|
||||
| (static_cast<std::uint64_t>(src[3]) << 24U)
|
||||
| (static_cast<std::uint64_t>(src[4]) << 32U)
|
||||
| (static_cast<std::uint64_t>(src[5]) << 40U)
|
||||
| (static_cast<std::uint64_t>(src[6]) << 48U)
|
||||
| (static_cast<std::uint64_t>(src[7]) << 56U);
|
||||
}
|
||||
|
||||
inline void store_le32(std::uint32_t value, std::uint8_t *dst) {
|
||||
dst[0] = static_cast<std::uint8_t>(value & 0xFFU);
|
||||
dst[1] = static_cast<std::uint8_t>((value >> 8U) & 0xFFU);
|
||||
dst[2] = static_cast<std::uint8_t>((value >> 16U) & 0xFFU);
|
||||
dst[3] = static_cast<std::uint8_t>((value >> 24U) & 0xFFU);
|
||||
}
|
||||
|
||||
inline void store_le64(std::uint64_t value, std::uint8_t *dst) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
dst[i] = static_cast<std::uint8_t>((value >> (i * 8U)) & 0xFFU);
|
||||
}
|
||||
}
|
||||
|
||||
inline void quarter_round(std::uint32_t *state, int a, int b, int c, int d) {
|
||||
state[a] += state[b];
|
||||
state[d] ^= state[a];
|
||||
state[d] = (state[d] << 16U) | (state[d] >> 16U);
|
||||
|
||||
state[c] += state[d];
|
||||
state[b] ^= state[c];
|
||||
state[b] = (state[b] << 12U) | (state[b] >> 20U);
|
||||
|
||||
state[a] += state[b];
|
||||
state[d] ^= state[a];
|
||||
state[d] = (state[d] << 8U) | (state[d] >> 24U);
|
||||
|
||||
state[c] += state[d];
|
||||
state[b] ^= state[c];
|
||||
state[b] = (state[b] << 7U) | (state[b] >> 25U);
|
||||
}
|
||||
|
||||
void chacha20_block(
|
||||
const std::uint8_t key[kXChaChaKeySize],
|
||||
const std::uint8_t nonce[kChaChaNonceSize],
|
||||
std::uint32_t counter,
|
||||
std::uint8_t out[kChaChaBlockSize]
|
||||
) {
|
||||
std::uint32_t state[16] = {};
|
||||
state[0] = 0x61707865U;
|
||||
state[1] = 0x3320646eU;
|
||||
state[2] = 0x79622d32U;
|
||||
state[3] = 0x6b206574U;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
state[4 + i] = load_le32(key + (i * 4));
|
||||
}
|
||||
state[12] = counter;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
state[13 + i] = load_le32(nonce + (i * 4));
|
||||
}
|
||||
|
||||
std::uint32_t working[16];
|
||||
std::memcpy(working, state, sizeof(state));
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
quarter_round(working, 0, 4, 8, 12);
|
||||
quarter_round(working, 1, 5, 9, 13);
|
||||
quarter_round(working, 2, 6, 10, 14);
|
||||
quarter_round(working, 3, 7, 11, 15);
|
||||
quarter_round(working, 0, 5, 10, 15);
|
||||
quarter_round(working, 1, 6, 11, 12);
|
||||
quarter_round(working, 2, 7, 8, 13);
|
||||
quarter_round(working, 3, 4, 9, 14);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
working[i] += state[i];
|
||||
store_le32(working[i], out + (i * 4));
|
||||
}
|
||||
}
|
||||
|
||||
void hchacha20(
|
||||
const std::uint8_t key[kXChaChaKeySize],
|
||||
const std::uint8_t nonce[16],
|
||||
std::uint8_t out[kXChaChaKeySize]
|
||||
) {
|
||||
std::uint32_t state[16] = {};
|
||||
state[0] = 0x61707865U;
|
||||
state[1] = 0x3320646eU;
|
||||
state[2] = 0x79622d32U;
|
||||
state[3] = 0x6b206574U;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
state[4 + i] = load_le32(key + (i * 4));
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
state[12 + i] = load_le32(nonce + (i * 4));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
quarter_round(state, 0, 4, 8, 12);
|
||||
quarter_round(state, 1, 5, 9, 13);
|
||||
quarter_round(state, 2, 6, 10, 14);
|
||||
quarter_round(state, 3, 7, 11, 15);
|
||||
quarter_round(state, 0, 5, 10, 15);
|
||||
quarter_round(state, 1, 6, 11, 12);
|
||||
quarter_round(state, 2, 7, 8, 13);
|
||||
quarter_round(state, 3, 4, 9, 14);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
store_le32(state[i], out + (i * 4));
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
store_le32(state[12 + i], out + (16 + i * 4));
|
||||
}
|
||||
}
|
||||
|
||||
void chacha20_xor(
|
||||
const std::uint8_t *input,
|
||||
std::size_t input_length,
|
||||
const std::uint8_t key[kXChaChaKeySize],
|
||||
const std::uint8_t nonce[kChaChaNonceSize],
|
||||
std::uint32_t initial_counter,
|
||||
std::vector<std::uint8_t> &output
|
||||
) {
|
||||
output.resize(input_length);
|
||||
std::uint32_t counter = initial_counter;
|
||||
std::uint8_t block[kChaChaBlockSize];
|
||||
|
||||
for (std::size_t offset = 0; offset < input_length; offset += kChaChaBlockSize) {
|
||||
chacha20_block(key, nonce, counter, block);
|
||||
const std::size_t block_size = std::min(kChaChaBlockSize, input_length - offset);
|
||||
for (std::size_t i = 0; i < block_size; i++) {
|
||||
output[offset + i] = static_cast<std::uint8_t>(input[offset + i] ^ block[i]);
|
||||
}
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
void poly1305_to_limbs26(const std::uint8_t block16[16], std::uint64_t limbs[5]) {
|
||||
const std::uint64_t lo = load_le64(block16);
|
||||
const std::uint64_t hi = load_le64(block16 + 8);
|
||||
limbs[0] = lo & 0x3FFFFFFULL;
|
||||
limbs[1] = (lo >> 26U) & 0x3FFFFFFULL;
|
||||
limbs[2] = ((lo >> 52U) | (hi << 12U)) & 0x3FFFFFFULL;
|
||||
limbs[3] = (hi >> 14U) & 0x3FFFFFFULL;
|
||||
limbs[4] = (hi >> 40U) & 0x3FFFFFFULL;
|
||||
}
|
||||
|
||||
void poly1305_multiply_reduce(std::uint64_t h[5], const std::uint64_t r[5]) {
|
||||
const std::uint64_t r0 = r[0];
|
||||
const std::uint64_t r1 = r[1];
|
||||
const std::uint64_t r2 = r[2];
|
||||
const std::uint64_t r3 = r[3];
|
||||
const std::uint64_t r4 = r[4];
|
||||
const std::uint64_t s1 = r1 * 5ULL;
|
||||
const std::uint64_t s2 = r2 * 5ULL;
|
||||
const std::uint64_t s3 = r3 * 5ULL;
|
||||
const std::uint64_t s4 = r4 * 5ULL;
|
||||
|
||||
const std::uint64_t h0_in = h[0];
|
||||
const std::uint64_t h1_in = h[1];
|
||||
const std::uint64_t h2_in = h[2];
|
||||
const std::uint64_t h3_in = h[3];
|
||||
const std::uint64_t h4_in = h[4];
|
||||
|
||||
std::uint64_t h0 = h0_in * r0 + h1_in * s4 + h2_in * s3 + h3_in * s2 + h4_in * s1;
|
||||
std::uint64_t h1 = h0_in * r1 + h1_in * r0 + h2_in * s4 + h3_in * s3 + h4_in * s2;
|
||||
std::uint64_t h2 = h0_in * r2 + h1_in * r1 + h2_in * r0 + h3_in * s4 + h4_in * s3;
|
||||
std::uint64_t h3 = h0_in * r3 + h1_in * r2 + h2_in * r1 + h3_in * r0 + h4_in * s4;
|
||||
std::uint64_t h4 = h0_in * r4 + h1_in * r3 + h2_in * r2 + h3_in * r1 + h4_in * r0;
|
||||
|
||||
std::uint64_t c = h0 >> 26U;
|
||||
h1 += c;
|
||||
h0 &= 0x3FFFFFFULL;
|
||||
c = h1 >> 26U;
|
||||
h2 += c;
|
||||
h1 &= 0x3FFFFFFULL;
|
||||
c = h2 >> 26U;
|
||||
h3 += c;
|
||||
h2 &= 0x3FFFFFFULL;
|
||||
c = h3 >> 26U;
|
||||
h4 += c;
|
||||
h3 &= 0x3FFFFFFULL;
|
||||
c = h4 >> 26U;
|
||||
h0 += c * 5ULL;
|
||||
h4 &= 0x3FFFFFFULL;
|
||||
c = h0 >> 26U;
|
||||
h1 += c;
|
||||
h0 &= 0x3FFFFFFULL;
|
||||
|
||||
h[0] = h0;
|
||||
h[1] = h1;
|
||||
h[2] = h2;
|
||||
h[3] = h3;
|
||||
h[4] = h4;
|
||||
}
|
||||
|
||||
void poly1305_mac(
|
||||
const std::uint8_t *data,
|
||||
std::size_t data_length,
|
||||
const std::uint8_t key32[32],
|
||||
std::uint8_t out_tag16[16]
|
||||
) {
|
||||
std::uint8_t r_bytes[16];
|
||||
std::memcpy(r_bytes, key32, 16);
|
||||
r_bytes[3] &= 15U;
|
||||
r_bytes[7] &= 15U;
|
||||
r_bytes[11] &= 15U;
|
||||
r_bytes[15] &= 15U;
|
||||
r_bytes[4] &= 252U;
|
||||
r_bytes[8] &= 252U;
|
||||
r_bytes[12] &= 252U;
|
||||
|
||||
std::uint8_t s_bytes[16];
|
||||
std::memcpy(s_bytes, key32 + 16, 16);
|
||||
|
||||
std::uint64_t r[5];
|
||||
poly1305_to_limbs26(r_bytes, r);
|
||||
std::uint64_t h[5] = {0, 0, 0, 0, 0};
|
||||
|
||||
std::vector<std::uint8_t> mac_input;
|
||||
const std::size_t pad = (16 - (data_length % 16)) % 16;
|
||||
mac_input.reserve(data_length + pad + 16);
|
||||
mac_input.insert(mac_input.end(), data, data + data_length);
|
||||
mac_input.insert(mac_input.end(), pad, 0);
|
||||
mac_input.insert(mac_input.end(), 8, 0);
|
||||
std::uint8_t ct_len_le[8];
|
||||
store_le64(static_cast<std::uint64_t>(data_length), ct_len_le);
|
||||
mac_input.insert(mac_input.end(), ct_len_le, ct_len_le + 8);
|
||||
|
||||
for (std::size_t offset = 0; offset < mac_input.size(); offset += 16) {
|
||||
std::uint8_t block[16];
|
||||
std::memcpy(block, mac_input.data() + offset, 16);
|
||||
|
||||
std::uint64_t n[5];
|
||||
poly1305_to_limbs26(block, n);
|
||||
h[0] += n[0];
|
||||
h[1] += n[1];
|
||||
h[2] += n[2];
|
||||
h[3] += n[3];
|
||||
h[4] += n[4] + (1ULL << 24U);
|
||||
poly1305_multiply_reduce(h, r);
|
||||
}
|
||||
|
||||
std::uint64_t h0 = h[0];
|
||||
std::uint64_t h1 = h[1];
|
||||
std::uint64_t h2 = h[2];
|
||||
std::uint64_t h3 = h[3];
|
||||
std::uint64_t h4 = h[4];
|
||||
|
||||
std::uint64_t c = h0 >> 26U;
|
||||
h1 += c;
|
||||
h0 &= 0x3FFFFFFULL;
|
||||
c = h1 >> 26U;
|
||||
h2 += c;
|
||||
h1 &= 0x3FFFFFFULL;
|
||||
c = h2 >> 26U;
|
||||
h3 += c;
|
||||
h2 &= 0x3FFFFFFULL;
|
||||
c = h3 >> 26U;
|
||||
h4 += c;
|
||||
h3 &= 0x3FFFFFFULL;
|
||||
c = h4 >> 26U;
|
||||
h0 += c * 5ULL;
|
||||
h4 &= 0x3FFFFFFULL;
|
||||
c = h0 >> 26U;
|
||||
h1 += c;
|
||||
h0 &= 0x3FFFFFFULL;
|
||||
|
||||
std::uint64_t g0 = h0 + 5ULL;
|
||||
c = g0 >> 26U;
|
||||
g0 &= 0x3FFFFFFULL;
|
||||
std::uint64_t g1 = h1 + c;
|
||||
c = g1 >> 26U;
|
||||
g1 &= 0x3FFFFFFULL;
|
||||
std::uint64_t g2 = h2 + c;
|
||||
c = g2 >> 26U;
|
||||
g2 &= 0x3FFFFFFULL;
|
||||
std::uint64_t g3 = h3 + c;
|
||||
c = g3 >> 26U;
|
||||
g3 &= 0x3FFFFFFULL;
|
||||
const std::uint64_t g4 = h4 + c - (1ULL << 26U);
|
||||
|
||||
const std::uint64_t mask = (g4 >> 63U) - 1ULL;
|
||||
const std::uint64_t nmask = ~mask;
|
||||
h0 = (h0 & nmask) | (g0 & mask);
|
||||
h1 = (h1 & nmask) | (g1 & mask);
|
||||
h2 = (h2 & nmask) | (g2 & mask);
|
||||
h3 = (h3 & nmask) | (g3 & mask);
|
||||
h4 = (h4 & nmask) | (g4 & mask);
|
||||
|
||||
const std::uint64_t lo = h0 | (h1 << 26U) | (h2 << 52U);
|
||||
const std::uint64_t hi = (h2 >> 12U) | (h3 << 14U) | (h4 << 40U);
|
||||
|
||||
const std::uint64_t s_lo = load_le64(s_bytes);
|
||||
const std::uint64_t s_hi = load_le64(s_bytes + 8);
|
||||
const std::uint64_t result_lo = lo + s_lo;
|
||||
const std::uint64_t carry = (result_lo < lo) ? 1ULL : 0ULL;
|
||||
const std::uint64_t result_hi = hi + s_hi + carry;
|
||||
|
||||
store_le64(result_lo, out_tag16);
|
||||
store_le64(result_hi, out_tag16 + 8);
|
||||
}
|
||||
|
||||
bool constant_time_equal(const std::uint8_t *a, const std::uint8_t *b, std::size_t length) {
|
||||
std::uint8_t diff = 0;
|
||||
for (std::size_t i = 0; i < length; i++) {
|
||||
diff |= static_cast<std::uint8_t>(a[i] ^ b[i]);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
void derive_subkey_and_nonce(
|
||||
const std::uint8_t key32[kXChaChaKeySize],
|
||||
const std::uint8_t nonce24[kXChaChaNonceSize],
|
||||
std::uint8_t out_subkey32[kXChaChaKeySize],
|
||||
std::uint8_t out_chacha_nonce12[kChaChaNonceSize]
|
||||
) {
|
||||
hchacha20(key32, nonce24, out_subkey32);
|
||||
std::memset(out_chacha_nonce12, 0, kChaChaNonceSize);
|
||||
std::memcpy(out_chacha_nonce12 + 4, nonce24 + 16, 8);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace rosetta::nativecrypto {
|
||||
|
||||
bool xchacha20poly1305_encrypt(
|
||||
const std::uint8_t *plaintext,
|
||||
std::size_t plaintext_length,
|
||||
const std::uint8_t *key32,
|
||||
const std::uint8_t *nonce24,
|
||||
std::vector<std::uint8_t> &ciphertext_with_tag
|
||||
) {
|
||||
if (key32 == nullptr || nonce24 == nullptr || (plaintext == nullptr && plaintext_length > 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint8_t subkey[kXChaChaKeySize];
|
||||
std::uint8_t chacha_nonce[kChaChaNonceSize];
|
||||
derive_subkey_and_nonce(key32, nonce24, subkey, chacha_nonce);
|
||||
|
||||
std::vector<std::uint8_t> ciphertext;
|
||||
chacha20_xor(plaintext, plaintext_length, subkey, chacha_nonce, 1, ciphertext);
|
||||
|
||||
std::uint8_t block[kChaChaBlockSize];
|
||||
chacha20_block(subkey, chacha_nonce, 0, block);
|
||||
std::uint8_t tag[kPoly1305TagSize];
|
||||
poly1305_mac(ciphertext.data(), ciphertext.size(), block, tag);
|
||||
|
||||
ciphertext_with_tag = ciphertext;
|
||||
ciphertext_with_tag.insert(ciphertext_with_tag.end(), tag, tag + kPoly1305TagSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool xchacha20poly1305_decrypt(
|
||||
const std::uint8_t *ciphertext_with_tag,
|
||||
std::size_t ciphertext_with_tag_length,
|
||||
const std::uint8_t *key32,
|
||||
const std::uint8_t *nonce24,
|
||||
std::vector<std::uint8_t> &plaintext
|
||||
) {
|
||||
if (key32 == nullptr || nonce24 == nullptr || ciphertext_with_tag == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (ciphertext_with_tag_length < kPoly1305TagSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t ciphertext_length = ciphertext_with_tag_length - kPoly1305TagSize;
|
||||
const std::uint8_t *ciphertext = ciphertext_with_tag;
|
||||
const std::uint8_t *tag = ciphertext_with_tag + ciphertext_length;
|
||||
|
||||
std::uint8_t subkey[kXChaChaKeySize];
|
||||
std::uint8_t chacha_nonce[kChaChaNonceSize];
|
||||
derive_subkey_and_nonce(key32, nonce24, subkey, chacha_nonce);
|
||||
|
||||
std::uint8_t block[kChaChaBlockSize];
|
||||
chacha20_block(subkey, chacha_nonce, 0, block);
|
||||
std::uint8_t computed_tag[kPoly1305TagSize];
|
||||
poly1305_mac(ciphertext, ciphertext_length, block, computed_tag);
|
||||
if (!constant_time_equal(tag, computed_tag, kPoly1305TagSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
chacha20_xor(ciphertext, ciphertext_length, subkey, chacha_nonce, 1, plaintext);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rosetta::nativecrypto
|
||||
25
Rosetta/Core/Crypto/NativeXChaCha20.hpp
Normal file
25
Rosetta/Core/Crypto/NativeXChaCha20.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace rosetta::nativecrypto {
|
||||
|
||||
bool xchacha20poly1305_encrypt(
|
||||
const uint8_t *plaintext,
|
||||
std::size_t plaintext_length,
|
||||
const uint8_t *key32,
|
||||
const uint8_t *nonce24,
|
||||
std::vector<uint8_t> &ciphertext_with_tag
|
||||
);
|
||||
|
||||
bool xchacha20poly1305_decrypt(
|
||||
const uint8_t *ciphertext_with_tag,
|
||||
std::size_t ciphertext_with_tag_length,
|
||||
const uint8_t *key32,
|
||||
const uint8_t *nonce24,
|
||||
std::vector<uint8_t> &plaintext
|
||||
);
|
||||
|
||||
} // namespace rosetta::nativecrypto
|
||||
@@ -7,7 +7,7 @@ import Foundation
|
||||
enum Poly1305Engine {
|
||||
|
||||
/// Computes a Poly1305 MAC matching the AEAD construction.
|
||||
static func mac(data: Data, key: Data) -> Data {
|
||||
nonisolated static func mac(data: Data, key: Data) -> Data {
|
||||
// Clamp r (first 16 bytes of key)
|
||||
var r = [UInt8](key[0..<16])
|
||||
r[3] &= 15; r[7] &= 15; r[11] &= 15; r[15] &= 15
|
||||
@@ -61,7 +61,7 @@ enum Poly1305Engine {
|
||||
private extension Poly1305Engine {
|
||||
|
||||
/// Convert 16 bytes to 5 limbs of 26 bits each (little-endian).
|
||||
static func toLimbs26(_ bytes: [UInt8]) -> [UInt64] {
|
||||
nonisolated static func toLimbs26(_ bytes: [UInt8]) -> [UInt64] {
|
||||
let b = bytes.count >= 16 ? bytes : bytes + [UInt8](repeating: 0, count: 16 - bytes.count)
|
||||
|
||||
var full = [UInt8](repeating: 0, count: 17)
|
||||
@@ -82,7 +82,7 @@ private extension Poly1305Engine {
|
||||
}
|
||||
|
||||
/// Multiply two numbers in 26-bit limb form, reduce mod 2^130 - 5.
|
||||
static func multiply(_ a: [UInt64], _ r: [UInt64]) -> [UInt64] {
|
||||
nonisolated static func multiply(_ a: [UInt64], _ r: [UInt64]) -> [UInt64] {
|
||||
let r0 = r[0], r1 = r[1], r2 = r[2], r3 = r[3], r4 = r[4]
|
||||
let s1 = r1 * 5, s2 = r2 * 5, s3 = r3 * 5, s4 = r4 * 5
|
||||
let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4]
|
||||
@@ -105,7 +105,7 @@ private extension Poly1305Engine {
|
||||
}
|
||||
|
||||
/// Final reduction mod 2^130-5 and add s.
|
||||
static func freeze(_ h: [UInt64], s: [UInt8]) -> [UInt8] {
|
||||
nonisolated static func freeze(_ h: [UInt64], s: [UInt8]) -> [UInt8] {
|
||||
var h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3], h4 = h[4]
|
||||
|
||||
var c: UInt64
|
||||
|
||||
@@ -6,12 +6,12 @@ import Foundation
|
||||
/// Matches the Android `MessageCrypto` XChaCha20 implementation for cross-platform compatibility.
|
||||
enum XChaCha20Engine {
|
||||
|
||||
static let poly1305TagSize = 16
|
||||
nonisolated static let poly1305TagSize = 16
|
||||
|
||||
// MARK: - XChaCha20-Poly1305 Decrypt
|
||||
|
||||
/// Decrypts ciphertext+tag using XChaCha20-Poly1305.
|
||||
static func decrypt(ciphertextWithTag: Data, key: Data, nonce: Data) throws -> Data {
|
||||
nonisolated static func decrypt(ciphertextWithTag: Data, key: Data, nonce: Data) throws -> Data {
|
||||
guard ciphertextWithTag.count >= poly1305TagSize else {
|
||||
throw CryptoError.invalidData("Ciphertext too short for Poly1305 tag")
|
||||
}
|
||||
@@ -19,6 +19,14 @@ enum XChaCha20Engine {
|
||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||
}
|
||||
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Decrypt(
|
||||
ciphertextWithTag,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
}
|
||||
|
||||
let ciphertext = ciphertextWithTag[0..<(ciphertextWithTag.count - poly1305TagSize)]
|
||||
let tag = ciphertextWithTag[(ciphertextWithTag.count - poly1305TagSize)...]
|
||||
|
||||
@@ -48,11 +56,19 @@ enum XChaCha20Engine {
|
||||
// MARK: - XChaCha20-Poly1305 Encrypt
|
||||
|
||||
/// Encrypts plaintext using XChaCha20-Poly1305.
|
||||
static func encrypt(plaintext: Data, key: Data, nonce: Data) throws -> Data {
|
||||
nonisolated static func encrypt(plaintext: Data, key: Data, nonce: Data) throws -> Data {
|
||||
guard key.count == 32, nonce.count == 24 else {
|
||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||
}
|
||||
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Encrypt(
|
||||
plaintext,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
}
|
||||
|
||||
// Step 1: HChaCha20 — derive subkey
|
||||
let subkey = hchacha20(key: key, nonce: Data(nonce[0..<16]))
|
||||
|
||||
@@ -80,7 +96,7 @@ enum XChaCha20Engine {
|
||||
extension XChaCha20Engine {
|
||||
|
||||
/// ChaCha20 quarter round.
|
||||
static func quarterRound(_ state: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
|
||||
nonisolated static func quarterRound(_ state: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
|
||||
state[a] = state[a] &+ state[b]; state[d] ^= state[a]; state[d] = (state[d] << 16) | (state[d] >> 16)
|
||||
state[c] = state[c] &+ state[d]; state[b] ^= state[c]; state[b] = (state[b] << 12) | (state[b] >> 20)
|
||||
state[a] = state[a] &+ state[b]; state[d] ^= state[a]; state[d] = (state[d] << 8) | (state[d] >> 24)
|
||||
@@ -88,7 +104,7 @@ extension XChaCha20Engine {
|
||||
}
|
||||
|
||||
/// Generates a 64-byte ChaCha20 block.
|
||||
static func chacha20Block(key: Data, nonce: Data, counter: UInt32) -> Data {
|
||||
nonisolated static func chacha20Block(key: Data, nonce: Data, counter: UInt32) -> Data {
|
||||
var state = [UInt32](repeating: 0, count: 16)
|
||||
|
||||
// Constants: "expand 32-byte k"
|
||||
@@ -133,7 +149,7 @@ extension XChaCha20Engine {
|
||||
}
|
||||
|
||||
/// ChaCha20 stream cipher encryption/decryption.
|
||||
static func chacha20Encrypt(data: Data, key: Data, nonce: Data, initialCounter: UInt32) -> Data {
|
||||
nonisolated static func chacha20Encrypt(data: Data, key: Data, nonce: Data, initialCounter: UInt32) -> Data {
|
||||
var result = Data(count: data.count)
|
||||
var counter = initialCounter
|
||||
|
||||
@@ -150,7 +166,7 @@ extension XChaCha20Engine {
|
||||
}
|
||||
|
||||
/// HChaCha20: derives a 32-byte subkey from a 32-byte key and 16-byte nonce.
|
||||
static func hchacha20(key: Data, nonce: Data) -> Data {
|
||||
nonisolated static func hchacha20(key: Data, nonce: Data) -> Data {
|
||||
var state = [UInt32](repeating: 0, count: 16)
|
||||
|
||||
state[0] = 0x61707865; state[1] = 0x3320646e
|
||||
@@ -190,7 +206,7 @@ extension XChaCha20Engine {
|
||||
}
|
||||
|
||||
/// Constant-time comparison of two Data objects.
|
||||
static func constantTimeEqual(_ a: Data, _ b: Data) -> Bool {
|
||||
nonisolated static func constantTimeEqual(_ a: Data, _ b: Data) -> Bool {
|
||||
guard a.count == b.count else { return false }
|
||||
var result: UInt8 = 0
|
||||
for i in 0..<a.count {
|
||||
|
||||
@@ -44,16 +44,16 @@ actor ImageLoadLimiter {
|
||||
/// Key format: attachment ID (8-char random string).
|
||||
final class AttachmentCache: @unchecked Sendable {
|
||||
|
||||
static let shared = AttachmentCache()
|
||||
nonisolated static let shared = AttachmentCache()
|
||||
|
||||
private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "AttachmentCache")
|
||||
nonisolated private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "AttachmentCache")
|
||||
|
||||
private let cacheDir: URL
|
||||
nonisolated private let cacheDir: URL
|
||||
|
||||
/// In-memory image cache — eliminates disk I/O + crypto on scroll-back.
|
||||
/// Android parity: LruCache in AttachmentFileManager.kt.
|
||||
/// Same pattern as AvatarRepository.cache (NSCache is thread-safe).
|
||||
private let imageCache: NSCache<NSString, UIImage> = {
|
||||
nonisolated(unsafe) private let imageCache: NSCache<NSString, UIImage> = {
|
||||
let cache = NSCache<NSString, UIImage>()
|
||||
cache.countLimit = 100
|
||||
cache.totalCostLimit = 80 * 1024 * 1024 // 80 MB — auto-evicts under memory pressure
|
||||
@@ -62,9 +62,9 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
|
||||
/// Private key for encrypting files at rest (Android parity).
|
||||
/// Set from SessionManager.startSession() after unlocking account.
|
||||
private let keyLock = NSLock()
|
||||
private var _privateKey: String?
|
||||
var privateKey: String? {
|
||||
nonisolated private let keyLock = NSLock()
|
||||
nonisolated(unsafe) private var _privateKey: String?
|
||||
nonisolated var privateKey: String? {
|
||||
get { keyLock.lock(); defer { keyLock.unlock() }; return _privateKey }
|
||||
set {
|
||||
keyLock.lock()
|
||||
@@ -85,7 +85,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
/// Android parity: `BitmapFactory.Options.inSampleSize` with max 4096px.
|
||||
/// Uses `CGImageSource` for memory-efficient downsampled decoding + EXIF orientation.
|
||||
/// `kCGImageSourceShouldCacheImmediately` forces decode now (not lazily on first draw).
|
||||
static func downsampledImage(from data: Data, maxPixelSize: Int = 4096) -> UIImage? {
|
||||
nonisolated static func downsampledImage(from data: Data, maxPixelSize: Int = 4096) -> UIImage? {
|
||||
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
return UIImage(data: data)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
// MARK: - Images
|
||||
|
||||
/// Saves a decoded image to cache, encrypted with private key (Android parity).
|
||||
func saveImage(_ image: UIImage, forAttachmentId id: String) {
|
||||
nonisolated func saveImage(_ image: UIImage, forAttachmentId id: String) {
|
||||
guard let data = image.jpegData(compressionQuality: 0.95) else { return }
|
||||
|
||||
// Warm in-memory cache immediately — next loadImage() returns in O(1)
|
||||
@@ -124,7 +124,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// Loads a cached image for an attachment ID, or `nil` if not cached.
|
||||
func loadImage(forAttachmentId id: String) -> UIImage? {
|
||||
nonisolated func loadImage(forAttachmentId id: String) -> UIImage? {
|
||||
// Fast path: in-memory cache hit — no disk I/O, no crypto
|
||||
if let cached = imageCache.object(forKey: id as NSString) {
|
||||
return cached
|
||||
@@ -161,7 +161,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
|
||||
/// Saves raw file data to cache (encrypted), returns the file URL.
|
||||
@discardableResult
|
||||
func saveFile(_ data: Data, forAttachmentId id: String, fileName: String) -> URL {
|
||||
nonisolated func saveFile(_ data: Data, forAttachmentId id: String, fileName: String) -> URL {
|
||||
let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
|
||||
let url = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")
|
||||
|
||||
@@ -178,7 +178,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// Returns cached file URL, or `nil` if not cached.
|
||||
func fileURL(forAttachmentId id: String, fileName: String) -> URL? {
|
||||
nonisolated func fileURL(forAttachmentId id: String, fileName: String) -> URL? {
|
||||
let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
|
||||
// Check encrypted
|
||||
let encUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")
|
||||
@@ -190,7 +190,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// Load file data, decrypting if needed.
|
||||
func loadFileData(forAttachmentId id: String, fileName: String) -> Data? {
|
||||
nonisolated func loadFileData(forAttachmentId id: String, fileName: String) -> Data? {
|
||||
let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
|
||||
// Try encrypted
|
||||
let encUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")
|
||||
@@ -211,7 +211,7 @@ final class AttachmentCache: @unchecked Sendable {
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
func clearAll() {
|
||||
nonisolated func clearAll() {
|
||||
imageCache.removeAllObjects()
|
||||
try? FileManager.default.removeItem(at: cacheDir)
|
||||
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
|
||||
|
||||
@@ -399,7 +399,7 @@ extension MessageCellLayout {
|
||||
|
||||
// MARK: - Collage Height (Thread-Safe)
|
||||
|
||||
/// Photo collage height — same formulas as C++ MessageLayout & PhotoCollageView.swift.
|
||||
/// Photo collage height — same formulas as PhotoCollageView.swift.
|
||||
private static func collageHeight(count: Int, width: CGFloat) -> CGFloat {
|
||||
guard count > 0 else { return 0 }
|
||||
if count == 1 { return max(180, min(width * 0.93, 340)) }
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
#include "MessageLayout.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace rosetta {
|
||||
|
||||
// Constants matching MessageCellView.swift / PhotoCollageView.swift exactly
|
||||
static constexpr float kReplyQuoteHeight = 41.0f;
|
||||
static constexpr float kReplyQuoteTopPadding = 5.0f;
|
||||
static constexpr float kFileAttachmentHeight = 56.0f;
|
||||
static constexpr float kAvatarAttachmentHeight = 60.0f;
|
||||
static constexpr float kTailProtrusion = 6.0f;
|
||||
static constexpr float kTextPaddingVertical = 5.0f;
|
||||
static constexpr float kForwardHeaderHeight = 40.0f;
|
||||
static constexpr float kBorderWidth = 2.0f;
|
||||
static constexpr float kCollageSpacing = 2.0f;
|
||||
static constexpr float kCaptionTopPadding = 6.0f;
|
||||
static constexpr float kCaptionBottomPadding = 5.0f;
|
||||
|
||||
float calculateCollageHeight(int count, float width) {
|
||||
if (count <= 0) return 0.0f;
|
||||
|
||||
if (count == 1) {
|
||||
// Single image: aspect ratio preserved, max 320pt
|
||||
return std::min(width * 0.75f, 320.0f);
|
||||
}
|
||||
if (count == 2) {
|
||||
// Side by side
|
||||
float cellW = (width - kCollageSpacing) / 2.0f;
|
||||
return std::min(cellW * 1.2f, 320.0f);
|
||||
}
|
||||
if (count == 3) {
|
||||
// 1 large left + 2 stacked right
|
||||
float leftW = width * 0.66f;
|
||||
return std::min(leftW * 1.1f, 320.0f);
|
||||
}
|
||||
if (count == 4) {
|
||||
// 2×2 grid
|
||||
float cellW = (width - kCollageSpacing) / 2.0f;
|
||||
float cellH = std::min(cellW * 0.85f, 160.0f);
|
||||
return cellH * 2.0f + kCollageSpacing;
|
||||
}
|
||||
// 5+ images: 2 top + 3 bottom
|
||||
float topH = std::min(width / 2.0f * 0.85f, 176.0f);
|
||||
float botH = std::min(width / 3.0f * 0.85f, 144.0f);
|
||||
return topH + kCollageSpacing + botH;
|
||||
}
|
||||
|
||||
MessageLayoutResult calculateLayout(const MessageLayoutInput& input) {
|
||||
MessageLayoutResult result{};
|
||||
float height = 0.0f;
|
||||
|
||||
// Top padding: 6pt for single/top, 2pt for mid/bottom
|
||||
bool isTopOrSingle = (input.position == BubblePosition::Single ||
|
||||
input.position == BubblePosition::Top);
|
||||
height += isTopOrSingle ? 6.0f : 2.0f;
|
||||
|
||||
bool hasVisibleAttachments = (input.imageCount + input.fileCount + input.avatarCount) > 0;
|
||||
|
||||
if (input.isForward) {
|
||||
// ── Forwarded message ──
|
||||
height += kForwardHeaderHeight;
|
||||
|
||||
if (input.forwardImageCount > 0) {
|
||||
result.photoCollageHeight = calculateCollageHeight(
|
||||
input.forwardImageCount, input.containerWidth - 20.0f);
|
||||
height += result.photoCollageHeight;
|
||||
}
|
||||
|
||||
height += input.forwardFileCount * kFileAttachmentHeight;
|
||||
|
||||
if (input.forwardHasCaption) {
|
||||
height += input.forwardCaptionHeight + kCaptionTopPadding + kCaptionBottomPadding;
|
||||
} else if (input.forwardImageCount == 0 && input.forwardFileCount == 0) {
|
||||
height += 20.0f; // fallback text ("Photo"/"File"/"Message")
|
||||
} else {
|
||||
height += 5.0f; // bottom spacer
|
||||
}
|
||||
} else if (hasVisibleAttachments) {
|
||||
// ── Attachment bubble (images, files, avatars) ──
|
||||
if (input.imageCount > 0) {
|
||||
result.photoCollageHeight = calculateCollageHeight(
|
||||
input.imageCount, input.containerWidth - kBorderWidth * 2.0f);
|
||||
height += result.photoCollageHeight;
|
||||
}
|
||||
|
||||
height += input.fileCount * kFileAttachmentHeight;
|
||||
height += input.avatarCount * kAvatarAttachmentHeight;
|
||||
|
||||
if (input.hasText) {
|
||||
height += input.textHeight + kCaptionTopPadding + kCaptionBottomPadding;
|
||||
}
|
||||
} else {
|
||||
// ── Text-only bubble ──
|
||||
if (input.hasReplyQuote) {
|
||||
height += kReplyQuoteHeight + kReplyQuoteTopPadding;
|
||||
}
|
||||
|
||||
height += input.textHeight + kTextPaddingVertical * 2.0f;
|
||||
}
|
||||
|
||||
// Tail protrusion: single/bottom positions have a tail (+6pt)
|
||||
bool hasTail = (input.position == BubblePosition::Single ||
|
||||
input.position == BubblePosition::Bottom);
|
||||
if (hasTail) {
|
||||
height += kTailProtrusion;
|
||||
}
|
||||
|
||||
result.totalHeight = std::ceil(height);
|
||||
result.bubbleHeight = result.totalHeight - (isTopOrSingle ? 6.0f : 2.0f);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rosetta
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/// Pure C++ message cell height calculator.
|
||||
/// No UIKit, no CoreText, no ObjC runtime — just math.
|
||||
/// Text height is measured externally (CoreText via ObjC++ bridge)
|
||||
/// and passed in as `textHeight`.
|
||||
|
||||
namespace rosetta {
|
||||
|
||||
enum class BubblePosition : int {
|
||||
Single = 0,
|
||||
Top = 1,
|
||||
Mid = 2,
|
||||
Bottom = 3
|
||||
};
|
||||
|
||||
enum class AttachmentType : int {
|
||||
Image = 0,
|
||||
Messages = 1,
|
||||
File = 2,
|
||||
Avatar = 3
|
||||
};
|
||||
|
||||
struct MessageLayoutInput {
|
||||
float containerWidth;
|
||||
float textHeight; // Measured by CoreText externally
|
||||
bool hasText;
|
||||
bool isOutgoing;
|
||||
BubblePosition position;
|
||||
bool hasReplyQuote;
|
||||
bool isForward;
|
||||
int imageCount;
|
||||
int fileCount;
|
||||
int avatarCount;
|
||||
int forwardImageCount;
|
||||
int forwardFileCount;
|
||||
bool forwardHasCaption;
|
||||
float forwardCaptionHeight;
|
||||
};
|
||||
|
||||
struct MessageLayoutResult {
|
||||
float totalHeight;
|
||||
float bubbleHeight;
|
||||
float photoCollageHeight;
|
||||
};
|
||||
|
||||
/// Calculate total cell height from message properties.
|
||||
/// All constants match MessageCellView.swift exactly.
|
||||
MessageLayoutResult calculateLayout(const MessageLayoutInput& input);
|
||||
|
||||
/// Calculate photo collage height for N images at given width.
|
||||
/// Matches PhotoCollageView.swift grid formulas.
|
||||
float calculateCollageHeight(int imageCount, float containerWidth);
|
||||
|
||||
} // namespace rosetta
|
||||
@@ -1,38 +0,0 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Objective-C++ bridge exposing C++ MessageLayout engine to Swift.
|
||||
/// Uses CoreText for text measurement, C++ for layout math.
|
||||
@interface MessageLayoutBridge : NSObject
|
||||
|
||||
/// Calculate cell height for a text-only message.
|
||||
+ (CGFloat)textCellHeight:(NSString *)text
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
hasReplyQuote:(BOOL)hasReplyQuote
|
||||
font:(UIFont *)font;
|
||||
|
||||
/// Calculate cell height for a message with direct attachments.
|
||||
+ (CGFloat)attachmentCellHeightWithImages:(int)imageCount
|
||||
files:(int)fileCount
|
||||
avatars:(int)avatarCount
|
||||
caption:(nullable NSString *)caption
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
font:(UIFont *)font;
|
||||
|
||||
/// Calculate cell height for a forwarded message.
|
||||
+ (CGFloat)forwardCellHeightWithImages:(int)imageCount
|
||||
files:(int)fileCount
|
||||
caption:(nullable NSString *)caption
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
font:(UIFont *)font;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,112 +0,0 @@
|
||||
#import "MessageLayoutBridge.h"
|
||||
#include "MessageLayout.hpp"
|
||||
|
||||
@implementation MessageLayoutBridge
|
||||
|
||||
/// Measure text height using CoreText (10-20x faster than SwiftUI Text measurement).
|
||||
+ (CGFloat)measureTextHeight:(NSString *)text
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
font:(UIFont *)font {
|
||||
if (!text || text.length == 0) return 0.0;
|
||||
|
||||
// Inner padding: 11pt leading, 64pt (outgoing) or 48pt (incoming) trailing
|
||||
CGFloat trailingPad = isOutgoing ? 64.0 : 48.0;
|
||||
CGFloat textMaxW = maxWidth - 11.0 - trailingPad;
|
||||
if (textMaxW <= 0) return 0.0;
|
||||
|
||||
NSDictionary *attrs = @{NSFontAttributeName: font};
|
||||
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:text
|
||||
attributes:attrs];
|
||||
CGRect rect = [attrStr boundingRectWithSize:CGSizeMake(textMaxW, CGFLOAT_MAX)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
context:nil];
|
||||
return ceil(rect.size.height);
|
||||
}
|
||||
|
||||
+ (CGFloat)textCellHeight:(NSString *)text
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
hasReplyQuote:(BOOL)hasReplyQuote
|
||||
font:(UIFont *)font {
|
||||
CGFloat textH = [self measureTextHeight:text maxWidth:maxWidth isOutgoing:isOutgoing font:font];
|
||||
|
||||
rosetta::MessageLayoutInput input{};
|
||||
input.containerWidth = static_cast<float>(maxWidth);
|
||||
input.textHeight = static_cast<float>(textH);
|
||||
input.hasText = (text.length > 0);
|
||||
input.isOutgoing = isOutgoing;
|
||||
input.position = static_cast<rosetta::BubblePosition>(position);
|
||||
input.hasReplyQuote = hasReplyQuote;
|
||||
input.isForward = false;
|
||||
input.imageCount = 0;
|
||||
input.fileCount = 0;
|
||||
input.avatarCount = 0;
|
||||
|
||||
auto result = rosetta::calculateLayout(input);
|
||||
return static_cast<CGFloat>(result.totalHeight);
|
||||
}
|
||||
|
||||
+ (CGFloat)attachmentCellHeightWithImages:(int)imageCount
|
||||
files:(int)fileCount
|
||||
avatars:(int)avatarCount
|
||||
caption:(nullable NSString *)caption
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
font:(UIFont *)font {
|
||||
CGFloat captionH = 0;
|
||||
if (caption && caption.length > 0) {
|
||||
captionH = [self measureTextHeight:caption maxWidth:maxWidth isOutgoing:isOutgoing font:font];
|
||||
}
|
||||
|
||||
rosetta::MessageLayoutInput input{};
|
||||
input.containerWidth = static_cast<float>(maxWidth);
|
||||
input.textHeight = static_cast<float>(captionH);
|
||||
input.hasText = (caption && caption.length > 0);
|
||||
input.isOutgoing = isOutgoing;
|
||||
input.position = static_cast<rosetta::BubblePosition>(position);
|
||||
input.hasReplyQuote = false;
|
||||
input.isForward = false;
|
||||
input.imageCount = imageCount;
|
||||
input.fileCount = fileCount;
|
||||
input.avatarCount = avatarCount;
|
||||
|
||||
auto result = rosetta::calculateLayout(input);
|
||||
return static_cast<CGFloat>(result.totalHeight);
|
||||
}
|
||||
|
||||
+ (CGFloat)forwardCellHeightWithImages:(int)imageCount
|
||||
files:(int)fileCount
|
||||
caption:(nullable NSString *)caption
|
||||
maxWidth:(CGFloat)maxWidth
|
||||
isOutgoing:(BOOL)isOutgoing
|
||||
position:(int)position
|
||||
font:(UIFont *)font {
|
||||
CGFloat captionH = 0;
|
||||
if (caption && caption.length > 0) {
|
||||
captionH = [self measureTextHeight:caption maxWidth:maxWidth isOutgoing:isOutgoing font:font];
|
||||
}
|
||||
|
||||
rosetta::MessageLayoutInput input{};
|
||||
input.containerWidth = static_cast<float>(maxWidth);
|
||||
input.textHeight = 0;
|
||||
input.hasText = false;
|
||||
input.isOutgoing = isOutgoing;
|
||||
input.position = static_cast<rosetta::BubblePosition>(position);
|
||||
input.hasReplyQuote = false;
|
||||
input.isForward = true;
|
||||
input.imageCount = 0;
|
||||
input.fileCount = 0;
|
||||
input.avatarCount = 0;
|
||||
input.forwardImageCount = imageCount;
|
||||
input.forwardFileCount = fileCount;
|
||||
input.forwardHasCaption = (caption && caption.length > 0);
|
||||
input.forwardCaptionHeight = static_cast<float>(captionH);
|
||||
|
||||
auto result = rosetta::calculateLayout(input);
|
||||
return static_cast<CGFloat>(result.totalHeight);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -4,7 +4,7 @@ import Foundation
|
||||
/// Matches the React Native / Android implementation exactly.
|
||||
final class Stream: @unchecked Sendable {
|
||||
|
||||
private var bytes: [Int]
|
||||
private var bytes: [UInt8]
|
||||
private var readPointer: Int = 0
|
||||
private var writePointer: Int = 0
|
||||
|
||||
@@ -12,33 +12,36 @@ final class Stream: @unchecked Sendable {
|
||||
|
||||
init() {
|
||||
bytes = []
|
||||
bytes.reserveCapacity(256)
|
||||
}
|
||||
|
||||
init(data: Data) {
|
||||
bytes = data.map { Int($0) & 0xFF }
|
||||
bytes = Array(data)
|
||||
}
|
||||
|
||||
// MARK: - Output
|
||||
|
||||
func toData() -> Data {
|
||||
Data(bytes.map { UInt8($0 & 0xFF) })
|
||||
Data(bytes)
|
||||
}
|
||||
|
||||
// MARK: - Bit-Level I/O
|
||||
|
||||
func writeBit(_ value: Int) {
|
||||
let bit = value & 1
|
||||
let bit = UInt8(value & 1)
|
||||
ensureCapacityForUpcomingBits(1)
|
||||
let byteIndex = writePointer >> 3
|
||||
ensureCapacity(byteIndex)
|
||||
bytes[byteIndex] = bytes[byteIndex] | (bit << (7 - (writePointer & 7)))
|
||||
let shift = 7 - (writePointer & 7)
|
||||
bytes[byteIndex] = bytes[byteIndex] | (bit << shift)
|
||||
writePointer += 1
|
||||
}
|
||||
|
||||
func readBit() -> Int {
|
||||
let byteIndex = readPointer >> 3
|
||||
let bit = (bytes[byteIndex] >> (7 - (readPointer & 7))) & 1
|
||||
let shift = 7 - (readPointer & 7)
|
||||
let bit = (bytes[byteIndex] >> shift) & 1
|
||||
readPointer += 1
|
||||
return bit
|
||||
return Int(bit)
|
||||
}
|
||||
|
||||
// MARK: - Bool
|
||||
@@ -54,30 +57,33 @@ final class Stream: @unchecked Sendable {
|
||||
// MARK: - Int8 (9 bits: 1 sign + 8 data)
|
||||
|
||||
func writeInt8(_ value: Int) {
|
||||
let negationBit = value < 0 ? 1 : 0
|
||||
let int8Value = abs(value) & 0xFF
|
||||
let negationBit: UInt8 = value < 0 ? 1 : 0
|
||||
let int8Value = UInt8(abs(value) & 0xFF)
|
||||
ensureCapacityForUpcomingBits(9)
|
||||
|
||||
let byteIndex = writePointer >> 3
|
||||
ensureCapacity(byteIndex)
|
||||
bytes[byteIndex] = bytes[byteIndex] | (negationBit << (7 - (writePointer & 7)))
|
||||
let signShift = 7 - (writePointer & 7)
|
||||
bytes[byteIndex] = bytes[byteIndex] | (negationBit << signShift)
|
||||
writePointer += 1
|
||||
|
||||
for i in 0..<8 {
|
||||
let bit = (int8Value >> (7 - i)) & 1
|
||||
let idx = writePointer >> 3
|
||||
ensureCapacity(idx)
|
||||
bytes[idx] = bytes[idx] | (bit << (7 - (writePointer & 7)))
|
||||
let shift = 7 - (writePointer & 7)
|
||||
bytes[idx] = bytes[idx] | (bit << shift)
|
||||
writePointer += 1
|
||||
}
|
||||
}
|
||||
|
||||
func readInt8() -> Int {
|
||||
var value = 0
|
||||
let negationBit = (bytes[readPointer >> 3] >> (7 - (readPointer & 7))) & 1
|
||||
let signShift = 7 - (readPointer & 7)
|
||||
let negationBit = Int((bytes[readPointer >> 3] >> signShift) & 1)
|
||||
readPointer += 1
|
||||
|
||||
for i in 0..<8 {
|
||||
let bit = (bytes[readPointer >> 3] >> (7 - (readPointer & 7))) & 1
|
||||
let shift = 7 - (readPointer & 7)
|
||||
let bit = Int((bytes[readPointer >> 3] >> shift) & 1)
|
||||
value = value | (bit << (7 - i))
|
||||
readPointer += 1
|
||||
}
|
||||
@@ -128,6 +134,8 @@ final class Stream: @unchecked Sendable {
|
||||
|
||||
func writeString(_ value: String) {
|
||||
let utf16Units = Array(value.utf16)
|
||||
let requiredBits = 36 + utf16Units.count * 18
|
||||
ensureCapacityForUpcomingBits(requiredBits)
|
||||
writeInt32(utf16Units.count)
|
||||
for codeUnit in utf16Units {
|
||||
writeInt16(Int(codeUnit))
|
||||
@@ -153,6 +161,8 @@ final class Stream: @unchecked Sendable {
|
||||
// MARK: - Bytes (Int32 length + raw Int8s)
|
||||
|
||||
func writeBytes(_ value: Data) {
|
||||
let requiredBits = 36 + value.count * 9
|
||||
ensureCapacityForUpcomingBits(requiredBits)
|
||||
writeInt32(value.count)
|
||||
for byte in value {
|
||||
writeInt8(Int(byte))
|
||||
@@ -163,16 +173,22 @@ final class Stream: @unchecked Sendable {
|
||||
let length = readInt32()
|
||||
var result = Data(capacity: length)
|
||||
for _ in 0..<length {
|
||||
result.append(UInt8(readInt8() & 0xFF))
|
||||
result.append(UInt8(truncatingIfNeeded: readInt8()))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func ensureCapacityForUpcomingBits(_ bitCount: Int) {
|
||||
guard bitCount > 0 else { return }
|
||||
let lastBitIndex = writePointer + bitCount - 1
|
||||
ensureCapacity(lastBitIndex >> 3)
|
||||
}
|
||||
|
||||
private func ensureCapacity(_ index: Int) {
|
||||
while bytes.count <= index {
|
||||
bytes.append(0)
|
||||
if bytes.count <= index {
|
||||
bytes.append(contentsOf: repeatElement(0, count: index - bytes.count + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ final class AccountManager {
|
||||
|
||||
private let crypto = CryptoManager.shared
|
||||
private let keychain = KeychainManager.shared
|
||||
private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "AccountManager")
|
||||
nonisolated private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "AccountManager")
|
||||
|
||||
private init() {
|
||||
migrateFromSingleAccount()
|
||||
|
||||
@@ -12,7 +12,7 @@ final class SessionManager {
|
||||
|
||||
static let shared = SessionManager()
|
||||
|
||||
private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "Session")
|
||||
nonisolated private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "Session")
|
||||
|
||||
private(set) var isAuthenticated = false
|
||||
private(set) var currentPublicKey: String = ""
|
||||
|
||||
@@ -15,7 +15,7 @@ enum BlurHashEncoder {
|
||||
/// - image: Source image (will be downscaled internally for performance).
|
||||
/// - numberOfComponents: AC components (x, y). Android parity: `BlurHash.encode(bitmap, 4, 3)`.
|
||||
/// - Returns: BlurHash string, or `nil` if encoding fails.
|
||||
static func encode(image: UIImage, numberOfComponents components: (Int, Int) = (4, 3)) -> String? {
|
||||
nonisolated static func encode(image: UIImage, numberOfComponents components: (Int, Int) = (4, 3)) -> String? {
|
||||
let (componentX, componentY) = components
|
||||
guard componentX >= 1, componentX <= 9, componentY >= 1, componentY <= 9 else { return nil }
|
||||
|
||||
@@ -89,7 +89,7 @@ enum BlurHashEncoder {
|
||||
|
||||
// MARK: - Basis Function
|
||||
|
||||
private static func multiplyBasisFunction(
|
||||
private nonisolated static func multiplyBasisFunction(
|
||||
pixels: [UInt8], width: Int, height: Int, bytesPerRow: Int,
|
||||
componentX: Int, componentY: Int
|
||||
) -> (Float, Float, Float) {
|
||||
@@ -116,7 +116,7 @@ enum BlurHashEncoder {
|
||||
|
||||
// MARK: - sRGB <-> Linear
|
||||
|
||||
static func sRGBToLinear(_ value: UInt8) -> Float {
|
||||
nonisolated static func sRGBToLinear(_ value: UInt8) -> Float {
|
||||
let v = Float(value) / 255
|
||||
if v <= 0.04045 {
|
||||
return v / 12.92
|
||||
@@ -125,7 +125,7 @@ enum BlurHashEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
static func linearToSRGB(_ value: Float) -> Int {
|
||||
nonisolated static func linearToSRGB(_ value: Float) -> Int {
|
||||
let v = max(0, min(1, value))
|
||||
if v <= 0.0031308 {
|
||||
return Int(v * 12.92 * 255 + 0.5)
|
||||
@@ -136,31 +136,31 @@ enum BlurHashEncoder {
|
||||
|
||||
// MARK: - DC / AC Encoding
|
||||
|
||||
private static func encodeDC(_ value: (Float, Float, Float)) -> Int {
|
||||
private nonisolated static func encodeDC(_ value: (Float, Float, Float)) -> Int {
|
||||
let r = linearToSRGB(value.0)
|
||||
let g = linearToSRGB(value.1)
|
||||
let b = linearToSRGB(value.2)
|
||||
return (r << 16) + (g << 8) + b
|
||||
}
|
||||
|
||||
private static func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int {
|
||||
private nonisolated static func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int {
|
||||
let r = max(0, min(18, Int(floor(signPow(value.0 / maximumValue) * 9 + 9.5))))
|
||||
let g = max(0, min(18, Int(floor(signPow(value.1 / maximumValue) * 9 + 9.5))))
|
||||
let b = max(0, min(18, Int(floor(signPow(value.2 / maximumValue) * 9 + 9.5))))
|
||||
return r * 19 * 19 + g * 19 + b
|
||||
}
|
||||
|
||||
private static func signPow(_ value: Float) -> Float {
|
||||
private nonisolated static func signPow(_ value: Float) -> Float {
|
||||
return copysign(pow(abs(value), 0.5), value)
|
||||
}
|
||||
|
||||
// MARK: - Base83 Encoding
|
||||
|
||||
private static let base83Characters: [Character] = Array(
|
||||
private nonisolated static let base83Characters: [Character] = Array(
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
||||
)
|
||||
|
||||
private static func encode83(value: Int, length: Int) -> String {
|
||||
private nonisolated static func encode83(value: Int, length: Int) -> String {
|
||||
var result = ""
|
||||
for i in 1...length {
|
||||
let digit = (value / pow83(length - i)) % 83
|
||||
@@ -169,7 +169,7 @@ enum BlurHashEncoder {
|
||||
return result
|
||||
}
|
||||
|
||||
private static func pow83(_ exponent: Int) -> Int {
|
||||
private nonisolated static func pow83(_ exponent: Int) -> Int {
|
||||
var result = 1
|
||||
for _ in 0..<exponent {
|
||||
result *= 83
|
||||
@@ -198,9 +198,19 @@ enum BlurHashDecoder {
|
||||
/// - height: Output image height in pixels (default 32).
|
||||
/// - punch: Color intensity multiplier (default 1). Android parity.
|
||||
/// - Returns: A UIImage placeholder, or `nil` if decoding fails.
|
||||
static func decode(blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||
nonisolated static func decode(blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||
guard blurHash.count >= 6 else { return nil }
|
||||
|
||||
if let nativeRGB = NativeBlurHashBridge.decodeBlurHash(
|
||||
blurHash,
|
||||
width: width,
|
||||
height: height,
|
||||
punch: punch
|
||||
),
|
||||
let nativeImage = makeImageFromRGBData(nativeRGB, width: width, height: height) {
|
||||
return nativeImage
|
||||
}
|
||||
|
||||
let sizeFlag = decodeBase83(blurHash, from: 0, length: 1)
|
||||
let numY = (sizeFlag / 9) + 1
|
||||
let numX = (sizeFlag % 9) + 1
|
||||
@@ -250,30 +260,19 @@ enum BlurHashDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||
guard let cgImage = CGImage(
|
||||
width: width, height: height,
|
||||
bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: bitmapInfo,
|
||||
provider: provider,
|
||||
decode: nil, shouldInterpolate: true, intent: .defaultIntent
|
||||
) else { return nil }
|
||||
|
||||
return UIImage(cgImage: cgImage)
|
||||
return makeImageFromRGBData(data as Data, width: width, height: height)
|
||||
}
|
||||
|
||||
// MARK: - DC / AC Decoding
|
||||
|
||||
private static func decodeDC(_ value: Int) -> (Float, Float, Float) {
|
||||
private nonisolated static func decodeDC(_ value: Int) -> (Float, Float, Float) {
|
||||
let r = value >> 16
|
||||
let g = (value >> 8) & 255
|
||||
let b = value & 255
|
||||
return (sRGBToLinear(r), sRGBToLinear(g), sRGBToLinear(b))
|
||||
}
|
||||
|
||||
private static func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
|
||||
private nonisolated static func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
|
||||
let quantR = value / (19 * 19)
|
||||
let quantG = (value / 19) % 19
|
||||
let quantB = value % 19
|
||||
@@ -284,11 +283,11 @@ enum BlurHashDecoder {
|
||||
)
|
||||
}
|
||||
|
||||
private static func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||
private nonisolated static func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||
copysign(pow(abs(value), exp), value)
|
||||
}
|
||||
|
||||
private static func sRGBToLinear<T: BinaryInteger>(_ value: T) -> Float {
|
||||
private nonisolated static func sRGBToLinear<T: BinaryInteger>(_ value: T) -> Float {
|
||||
let v = Float(Int64(value)) / 255
|
||||
if v <= 0.04045 { return v / 12.92 }
|
||||
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||
@@ -296,7 +295,7 @@ enum BlurHashDecoder {
|
||||
|
||||
// MARK: - Base83 Decoding (string-index based, matching canonical)
|
||||
|
||||
private static let base83Lookup: [Character: Int] = {
|
||||
private nonisolated static let base83Lookup: [Character: Int] = {
|
||||
let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
||||
var lookup = [Character: Int]()
|
||||
for (i, ch) in chars.enumerated() {
|
||||
@@ -305,7 +304,7 @@ enum BlurHashDecoder {
|
||||
return lookup
|
||||
}()
|
||||
|
||||
private static func decodeBase83(_ string: String, from start: Int, length: Int) -> Int {
|
||||
private nonisolated static func decodeBase83(_ string: String, from start: Int, length: Int) -> Int {
|
||||
let startIdx = string.index(string.startIndex, offsetBy: start)
|
||||
let endIdx = string.index(startIdx, offsetBy: length)
|
||||
var value = 0
|
||||
@@ -314,6 +313,26 @@ enum BlurHashDecoder {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private nonisolated static func makeImageFromRGBData(_ data: Data, width: Int, height: Int) -> UIImage? {
|
||||
let bytesPerRow = width * 3
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||
guard let provider = CGDataProvider(data: data as CFData) else { return nil }
|
||||
guard let cgImage = CGImage(
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 24,
|
||||
bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: bitmapInfo,
|
||||
provider: provider,
|
||||
decode: nil,
|
||||
shouldInterpolate: true,
|
||||
intent: .defaultIntent
|
||||
) else { return nil }
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIImage Extension
|
||||
@@ -327,7 +346,7 @@ extension UIImage {
|
||||
|
||||
/// Creates a UIImage from a BlurHash string.
|
||||
/// Canonical woltapp/blurhash decoder with punch parameter (Android parity).
|
||||
static func fromBlurHash(_ blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||
nonisolated static func fromBlurHash(_ blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||
return BlurHashDecoder.decode(blurHash: blurHash, width: width, height: height, punch: punch)
|
||||
}
|
||||
}
|
||||
|
||||
173
Rosetta/Core/Utils/NativeBlurHash.cpp
Normal file
173
Rosetta/Core/Utils/NativeBlurHash.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "NativeBlurHash.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kBase83Chars[] =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
constexpr std::array<std::int8_t, 128> make_base83_lookup() {
|
||||
std::array<std::int8_t, 128> lookup = {};
|
||||
for (std::size_t i = 0; i < lookup.size(); i++) {
|
||||
lookup[i] = -1;
|
||||
}
|
||||
for (int i = 0; i < 83; i++) {
|
||||
lookup[static_cast<unsigned char>(kBase83Chars[i])] = static_cast<std::int8_t>(i);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
constexpr auto kBase83Lookup = make_base83_lookup();
|
||||
|
||||
int base83_index(char c) {
|
||||
const auto uc = static_cast<unsigned char>(c);
|
||||
if (uc >= kBase83Lookup.size()) {
|
||||
return 0;
|
||||
}
|
||||
const int index = kBase83Lookup[uc];
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
int decode_base83(const std::string &str, std::size_t start, std::size_t length) {
|
||||
int value = 0;
|
||||
for (std::size_t i = 0; i < length; i++) {
|
||||
value = value * 83 + base83_index(str[start + i]);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
float srgb_to_linear(int value) {
|
||||
const float v = static_cast<float>(value) / 255.0f;
|
||||
if (v <= 0.04045f) {
|
||||
return v / 12.92f;
|
||||
}
|
||||
return std::pow((v + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
|
||||
int linear_to_srgb(float value) {
|
||||
const float v = std::clamp(value, 0.0f, 1.0f);
|
||||
if (v <= 0.0031308f) {
|
||||
return static_cast<int>(v * 12.92f * 255.0f + 0.5f);
|
||||
}
|
||||
return static_cast<int>((1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f) * 255.0f + 0.5f);
|
||||
}
|
||||
|
||||
float sign_pow(float value, float exp) {
|
||||
return std::copysign(std::pow(std::abs(value), exp), value);
|
||||
}
|
||||
|
||||
std::array<float, 3> decode_dc(int value) {
|
||||
const int r = value >> 16;
|
||||
const int g = (value >> 8) & 255;
|
||||
const int b = value & 255;
|
||||
return {srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)};
|
||||
}
|
||||
|
||||
std::array<float, 3> decode_ac(int value, float maximum_value) {
|
||||
const int quant_r = value / (19 * 19);
|
||||
const int quant_g = (value / 19) % 19;
|
||||
const int quant_b = value % 19;
|
||||
return {
|
||||
sign_pow((static_cast<float>(quant_r) - 9.0f) / 9.0f, 2.0f) * maximum_value,
|
||||
sign_pow((static_cast<float>(quant_g) - 9.0f) / 9.0f, 2.0f) * maximum_value,
|
||||
sign_pow((static_cast<float>(quant_b) - 9.0f) / 9.0f, 2.0f) * maximum_value
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace rosetta::nativeimage {
|
||||
|
||||
bool decode_blurhash_to_rgb(
|
||||
const std::string &blurhash,
|
||||
int width,
|
||||
int height,
|
||||
float punch,
|
||||
std::vector<std::uint8_t> &rgb
|
||||
) {
|
||||
if (blurhash.size() < 6 || width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int size_flag = decode_base83(blurhash, 0, 1);
|
||||
const int num_y = (size_flag / 9) + 1;
|
||||
const int num_x = (size_flag % 9) + 1;
|
||||
const std::size_t expected_length = static_cast<std::size_t>(4 + 2 * num_x * num_y);
|
||||
if (blurhash.size() != expected_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int quantized_maximum = decode_base83(blurhash, 1, 1);
|
||||
const float maximum_value = static_cast<float>(quantized_maximum + 1) / 166.0f;
|
||||
|
||||
std::vector<std::array<float, 3>> colors;
|
||||
colors.reserve(static_cast<std::size_t>(num_x * num_y));
|
||||
for (int i = 0; i < num_x * num_y; i++) {
|
||||
if (i == 0) {
|
||||
colors.push_back(decode_dc(decode_base83(blurhash, 2, 4)));
|
||||
} else {
|
||||
colors.push_back(decode_ac(
|
||||
decode_base83(blurhash, static_cast<std::size_t>(4 + i * 2), 2),
|
||||
maximum_value * punch
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t pixel_count = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
rgb.assign(pixel_count * 3, 0);
|
||||
|
||||
std::vector<float> cos_x;
|
||||
std::vector<float> cos_y;
|
||||
cos_x.resize(static_cast<std::size_t>(width) * static_cast<std::size_t>(num_x));
|
||||
cos_y.resize(static_cast<std::size_t>(height) * static_cast<std::size_t>(num_y));
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int i = 0; i < num_x; i++) {
|
||||
cos_x[static_cast<std::size_t>(x * num_x + i)] = std::cos(
|
||||
kPi * static_cast<float>(x) * static_cast<float>(i)
|
||||
/ static_cast<float>(width)
|
||||
);
|
||||
}
|
||||
}
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int j = 0; j < num_y; j++) {
|
||||
cos_y[static_cast<std::size_t>(y * num_y + j)] = std::cos(
|
||||
kPi * static_cast<float>(y) * static_cast<float>(j)
|
||||
/ static_cast<float>(height)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
float r = 0;
|
||||
float g = 0;
|
||||
float b = 0;
|
||||
|
||||
for (int j = 0; j < num_y; j++) {
|
||||
const float basis_y = cos_y[static_cast<std::size_t>(y * num_y + j)];
|
||||
for (int i = 0; i < num_x; i++) {
|
||||
const float basis_x = cos_x[static_cast<std::size_t>(x * num_x + i)];
|
||||
const float basis = basis_x * basis_y;
|
||||
const auto &color = colors[static_cast<std::size_t>(i + j * num_x)];
|
||||
r += color[0] * basis;
|
||||
g += color[1] * basis;
|
||||
b += color[2] * basis;
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t offset = static_cast<std::size_t>((y * width + x) * 3);
|
||||
rgb[offset] = static_cast<std::uint8_t>(std::clamp(linear_to_srgb(r), 0, 255));
|
||||
rgb[offset + 1] = static_cast<std::uint8_t>(std::clamp(linear_to_srgb(g), 0, 255));
|
||||
rgb[offset + 2] = static_cast<std::uint8_t>(std::clamp(linear_to_srgb(b), 0, 255));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rosetta::nativeimage
|
||||
18
Rosetta/Core/Utils/NativeBlurHash.hpp
Normal file
18
Rosetta/Core/Utils/NativeBlurHash.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace rosetta::nativeimage {
|
||||
|
||||
bool decode_blurhash_to_rgb(
|
||||
const std::string &blurhash,
|
||||
int width,
|
||||
int height,
|
||||
float punch,
|
||||
std::vector<std::uint8_t> &rgb
|
||||
);
|
||||
|
||||
} // namespace rosetta::nativeimage
|
||||
15
Rosetta/Core/Utils/NativeBlurHashBridge.h
Normal file
15
Rosetta/Core/Utils/NativeBlurHashBridge.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Objective-C++ bridge exposing native C++ BlurHash decoder to Swift.
|
||||
@interface NativeBlurHashBridge : NSObject
|
||||
|
||||
+ (nullable NSData *)decodeBlurHash:(NSString *)blurHash
|
||||
width:(NSInteger)width
|
||||
height:(NSInteger)height
|
||||
punch:(float)punch;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
33
Rosetta/Core/Utils/NativeBlurHashBridge.mm
Normal file
33
Rosetta/Core/Utils/NativeBlurHashBridge.mm
Normal file
@@ -0,0 +1,33 @@
|
||||
#import "NativeBlurHashBridge.h"
|
||||
|
||||
#include "NativeBlurHash.hpp"
|
||||
|
||||
@implementation NativeBlurHashBridge
|
||||
|
||||
+ (nullable NSData *)decodeBlurHash:(NSString *)blurHash
|
||||
width:(NSInteger)width
|
||||
height:(NSInteger)height
|
||||
punch:(float)punch {
|
||||
if (blurHash.length < 6 || width <= 0 || height <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> rgb;
|
||||
const bool ok = rosetta::nativeimage::decode_blurhash_to_rgb(
|
||||
std::string(blurHash.UTF8String ?: ""),
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
punch,
|
||||
rgb
|
||||
);
|
||||
if (!ok) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (rgb.empty()) {
|
||||
return [NSData data];
|
||||
}
|
||||
return [NSData dataWithBytes:rgb.data() length:rgb.size()];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1038,7 +1038,7 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
||||
photoLoadTasks[attachmentId] = Task { [weak self] in
|
||||
await ImageLoadLimiter.shared.acquire()
|
||||
let loaded = await Task.detached(priority: .userInitiated) {
|
||||
await AttachmentCache.shared.loadImage(forAttachmentId: attachmentId)
|
||||
AttachmentCache.shared.loadImage(forAttachmentId: attachmentId)
|
||||
}.value
|
||||
await ImageLoadLimiter.shared.release()
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
@@ -727,7 +727,7 @@ extension NativeMessageListController: ComposerViewDelegate {
|
||||
/// When `preCalculatedHeight` is set, `preferredLayoutAttributesFitting` returns it
|
||||
/// immediately — skipping the expensive SwiftUI self-sizing layout pass.
|
||||
final class PreSizedCell: UICollectionViewCell {
|
||||
/// Height from C++ MessageLayout engine. Set in cell registration closure.
|
||||
/// Precomputed layout height for the message cell. Set in cell registration closure.
|
||||
var preCalculatedHeight: CGFloat?
|
||||
|
||||
override func preferredLayoutAttributesFitting(
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
// Exposes C++ layout engine via Objective-C++ wrappers.
|
||||
//
|
||||
|
||||
#import "Core/Layout/MessageLayoutBridge.h"
|
||||
#import "Core/Crypto/NativeCryptoBridge.h"
|
||||
#import "Core/Utils/NativeBlurHashBridge.h"
|
||||
|
||||
Reference in New Issue
Block a user