Fix a permissions issue related to validating media tier attachments.

This commit is contained in:
Pete Walters 2026-02-06 15:18:24 -06:00 committed by GitHub
parent df5c951d10
commit b198b9e8a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 180 additions and 52 deletions

View File

@ -49,7 +49,10 @@ private extension CVComponentState {
let imageMetadata = DataImageSource(avatarData).imageMetadata()
guard let imageMetadata else {
let cachedAvatar = GroupInviteLinkCachedAvatar(
cacheFileUrl: OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true),
cacheFileUrl: OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
),
imageSizePixels: .zero,
isValid: false,
)

View File

@ -26,7 +26,10 @@ final class VoiceMessageInProgressDraft: VoiceMessageSendableDraft {
init(thread: TSThread, audioSession: AudioSession, sleepManager: any DeviceSleepManager) {
self.threadUniqueId = thread.uniqueId
self.audioFileUrl = OWSFileSystem.temporaryFileUrl(fileExtension: "m4a")
self.audioFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: "m4a",
isAvailableWhileDeviceLocked: false,
)
self.audioActivity = AudioActivity(audioDescription: "Voice Message Recording", behavior: .playAndRecord)
self.audioSession = audioSession
self.sleepManager = sleepManager

View File

@ -71,7 +71,10 @@ final class VoiceMessageInterruptedDraft: VoiceMessageSendableDraft {
// MARK: -
func prepareForSending() throws -> URL {
let temporaryAudioFileUrl = OWSFileSystem.temporaryFileUrl()
let temporaryAudioFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
try FileManager.default.copyItem(at: audioFileUrl, to: temporaryAudioFileUrl)
return temporaryAudioFileUrl
}

View File

@ -424,7 +424,11 @@ class DeviceTransferService: NSObject, DeviceTransferServiceProtocol {
throw OWSAssertionError("Unknown db file being copied")
}
owsAssertDebug(databaseFile.relativePath.hasSuffix(newFileExtension))
return OWSFileSystem.temporaryFileUrl(fileName: newFileName, fileExtension: newFileExtension)
return OWSFileSystem.temporaryFileUrl(
fileName: newFileName,
fileExtension: newFileExtension,
isAvailableWhileDeviceLocked: false,
)
}
private static func makeLocalCopy(

View File

@ -539,7 +539,10 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
let assetFilePath = asset.filePath
let assetTypeIdentifier = giphyAsset.type.utiType
let consumableFilePath = OWSFileSystem.temporaryFilePath(fileExtension: assetFileExtension)
let consumableFilePath = OWSFileSystem.temporaryFilePath(
fileExtension: assetFileExtension,
isAvailableWhileDeviceLocked: false,
)
try FileManager.default.copyItem(atPath: assetFilePath, toPath: consumableFilePath)
let dataSource = DataSourcePath(filePath: consumableFilePath, ownership: .owned)

View File

@ -1195,7 +1195,10 @@ private class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
// main thread
func beginRecording(aspectRatio: CGFloat, includeAudio: Bool) throws {
let outputURL = OWSFileSystem.temporaryFileUrl(fileExtension: "mp4")
let outputURL = OWSFileSystem.temporaryFileUrl(
fileExtension: "mp4",
isAvailableWhileDeviceLocked: false,
)
let assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
guard

View File

@ -110,7 +110,10 @@ public class GroupLinkQRCodeViewController: OWSViewController {
return
}
let fileUrl = OWSFileSystem.temporaryFileUrl(fileExtension: "png")
let fileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: "png",
isAvailableWhileDeviceLocked: false,
)
try imageData.write(to: fileUrl)
AttachmentSharing.showShareUI(for: fileUrl, sender: sender)

View File

@ -238,7 +238,10 @@ private class GenericStreamProvider {
transforms: [any StreamTransform],
exportProgress: BackupArchiveExportProgress?,
) -> ProtoStream.OpenOutputStreamResult<URL> {
let fileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let fileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
guard let outputStream = OutputStream(url: fileUrl, append: false) else {
return .unableToOpenFileStream
}

View File

@ -18,7 +18,10 @@ enum ContactSyncAttachmentBuilder {
return nil
}
let fileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let fileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
guard let outputStream = OutputStream(url: fileUrl, append: false) else {
owsFailDebug("Could not open outputStream.")
return nil

View File

@ -125,10 +125,14 @@ public class AttachmentStream {
tmpURL = OWSFileSystem.temporaryFileUrl(
fileName: normalizedFilename,
fileExtension: pathExtension,
isAvailableWhileDeviceLocked: false,
)
try OWSFileSystem.deleteFileIfExists(url: tmpURL)
} else {
tmpURL = OWSFileSystem.temporaryFileUrl(fileExtension: pathExtension)
tmpURL = OWSFileSystem.temporaryFileUrl(
fileExtension: pathExtension,
isAvailableWhileDeviceLocked: false,
)
}
// hmac and digest are validated at download time; no need to revalidate every read.
try Cryptography.decryptFileWithoutValidating(

View File

@ -153,7 +153,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
// This temp file becomes the new attachment source, and will
// be owned by that part of the process and doesn't need to be
// cleaned up here.
let tmpFileUrl = OWSFileSystem.temporaryFileUrl()
let tmpFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try Cryptography.decryptFile(
at: fileUrl,
metadata: outerDecryptionData,
@ -598,7 +601,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
// compression applied to the source video, and we want a high fidelity still frame.
.jpegData(compressionQuality: 1)
.map { thumbnailData in
let thumbnailTmpFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let thumbnailTmpFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
let (encryptedThumbnail, _) = try Cryptography.encrypt(thumbnailData, attachmentKey: input.attachmentKey)
try encryptedThumbnail.write(to: thumbnailTmpFile)
return PendingFile(tmpFileUrl: thumbnailTmpFile, isTmpFileEncrypted: true)
@ -723,7 +729,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
)
}
let outputWaveformFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let outputWaveformFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
let waveformData = try waveform.archive()
let (encryptedWaveform, _) = try Cryptography.encrypt(waveformData, attachmentKey: attachmentKey)
@ -937,7 +946,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
attachmentKey: input.attachmentKey,
applyExtraPadding: true,
)
let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let outputFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try encryptedData.write(to: outputFile)
return (
PendingFile(
@ -947,7 +959,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
encryptionMetadata,
)
case .unencryptedFile(let fileUrl):
let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let outputFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
let encryptionMetadata = try Cryptography.encryptAttachment(
at: fileUrl,
output: outputFile,
@ -991,7 +1006,10 @@ public class AttachmentContentValidatorImpl: AttachmentContentValidator {
)
} else {
let fileHandle = try Cryptography.encryptedFileHandle(at: fileUrl, attachmentKey: inputAttachmentKey)
let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let outputFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
let encryptionMetadata = try Cryptography.reencryptFileHandle(
at: fileHandle,
attachmentKey: input.attachmentKey,
@ -1045,7 +1063,10 @@ extension AttachmentContentValidatorImpl.PendingFile {
return self
}
let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let outputFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
// Encrypt _without_ custom padding; we never send these files
// and just use them locally, so no need for custom padding
// that later requires out-of-band plaintext length tracking

View File

@ -1734,8 +1734,11 @@ public class AttachmentDownloadManagerImpl: AttachmentDownloadManager {
guard fileSize <= maxDownloadSizeBytes else {
throw OWSGenericError("Attachment download length exceeds max size.")
}
let tmpFile = OWSFileSystem.temporaryFileUrl()
try OWSFileSystem.copyFile(from: downloadUrl, to: tmpFile)
let tmpFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
try OWSFileSystem.moveFile(from: downloadUrl, to: tmpFile)
return tmpFile
} onError: { error, attemptCount in
Logger.warn("Error: \(error)")
@ -1821,7 +1824,10 @@ public class AttachmentDownloadManagerImpl: AttachmentDownloadManager {
return try await decryptionQueue.run {
do {
// Transient attachments decrypt to a tmp file.
let outputUrl = OWSFileSystem.temporaryFileUrl()
let outputUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
try Cryptography.decryptAttachment(
at: encryptedFileUrl,

View File

@ -100,7 +100,10 @@ public class AttachmentViewOnceManagerImpl: AttachmentViewOnceManager {
owsFailDebug("Missing attachment file.")
return nil
}
let tempFileUrl = OWSFileSystem.temporaryFileUrl()
let tempFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
guard !OWSFileSystem.fileOrFolderExists(url: tempFileUrl) else {
owsFailDebug("Temp file unexpectedly already exists.")
return nil

View File

@ -31,7 +31,10 @@ enum CDNDownloadOperation {
let downloadUrl = response.downloadUrl
do {
let temporaryFileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let temporaryFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try OWSFileSystem.moveFile(from: downloadUrl, to: temporaryFileUrl)
return temporaryFileUrl
} catch {

View File

@ -989,7 +989,10 @@ public class StickerManager: NSObject {
let stickerKey = try hkdf(outputLength: stickerKeyLength, inputKeyMaterial: packKey, salt: [], info: Data("Sticker Pack".utf8))
let attachmentKey = try! AttachmentKey(combinedKey: stickerKey)
let temporaryDecryptedFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let temporaryDecryptedFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try Cryptography.decryptFile(at: url, metadata: DecryptionMetadata(key: attachmentKey), output: temporaryDecryptedFile)
return temporaryDecryptedFile
}

View File

@ -250,7 +250,10 @@ extension OWSURLSessionProtocol {
ignoreAppExpiry: Bool = false,
progress: OWSProgressSource? = nil,
) async throws -> HTTPResponse {
let multipartBodyFileURL = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let multipartBodyFileURL = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
defer {
do {
try OWSFileSystem.deleteFileIfExists(url: multipartBodyFileURL)

View File

@ -1641,7 +1641,10 @@ extension OWSProfileManager {
Logger.info("")
let urlSession = await SSKEnvironment.shared.signalServiceRef.sharedUrlSessionForCdn(cdnNumber: 0, maxResponseSize: nil)
let response = try await urlSession.performDownload(avatarUrlPath, method: .get)
let decryptedFileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let decryptedFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try Self.decryptAvatar(at: response.downloadUrl, to: decryptedFileUrl, profileKey: profileKey)
guard (try? DataImageSource.forPath(decryptedFileUrl.path))?.ows_isValidImage ?? false else {
throw OWSGenericError("Couldn't validate avatar")

View File

@ -213,7 +213,10 @@ public enum DatabaseRecovery {
private static func temporaryDatabaseFileUrl() -> URL {
logger.info("Creating temporary database file...")
let result = OWSFileSystem.temporaryFileUrl()
let result = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
logger.info("Created at \(result)")
return result
}

View File

@ -57,7 +57,10 @@ public struct _Upload_AttachmentEncrypterWrapper: Upload.Shims.AttachmentEncrypt
public struct _Upload_FileSystemWrapper: Upload.Shims.FileSystem {
public func temporaryFileUrl() -> URL {
return OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
return OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
}
public func fileOrFolderExists(url: URL) -> Bool {

View File

@ -55,7 +55,10 @@ extension Upload.CDN0 {
throw AppExpiredError()
}
let dataFileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
let dataFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
try data.write(to: dataFileUrl)
let cdn0UrlSession = await SSKEnvironment.shared.signalServiceRef.sharedUrlSessionForCdn(cdnNumber: 0, maxResponseSize: nil)

View File

@ -341,10 +341,10 @@ public extension OWSFileSystem {
public extension OWSFileSystem {
static func temporaryFileUrl(
fileExtension: String? = nil,
isAvailableWhileDeviceLocked: Bool = false,
fileExtension: String?,
isAvailableWhileDeviceLocked: Bool,
) -> URL {
return URL(fileURLWithPath: temporaryFilePath(
return URL(fileURLWithPath: _temporaryFilePath(
fileName: nil,
fileExtension: fileExtension,
isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked,
@ -353,10 +353,10 @@ public extension OWSFileSystem {
static func temporaryFileUrl(
fileName: String,
fileExtension: String? = nil,
isAvailableWhileDeviceLocked: Bool = false,
fileExtension: String?,
isAvailableWhileDeviceLocked: Bool,
) -> URL {
return URL(fileURLWithPath: temporaryFilePath(
return URL(fileURLWithPath: _temporaryFilePath(
fileName: fileName,
fileExtension: fileExtension,
isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked,
@ -364,20 +364,32 @@ public extension OWSFileSystem {
}
static func temporaryFilePath(
fileName: String? = nil,
fileExtension: String? = nil,
fileName: String,
fileExtension: String?,
isAvailableWhileDeviceLocked: Bool,
) -> String {
temporaryFilePath(
return _temporaryFilePath(
fileName: fileName,
fileExtension: fileExtension,
isAvailableWhileDeviceLocked: false,
isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked,
)
}
static func temporaryFilePath(
fileName: String? = nil,
fileExtension: String? = nil,
isAvailableWhileDeviceLocked: Bool = false,
fileExtension: String,
isAvailableWhileDeviceLocked: Bool,
) -> String {
return _temporaryFilePath(
fileName: nil,
fileExtension: fileExtension,
isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked,
)
}
private static func _temporaryFilePath(
fileName: String?,
fileExtension: String?,
isAvailableWhileDeviceLocked: Bool,
) -> String {
let tempDirPath = tempDirPath(availableWhileDeviceLocked: isAvailableWhileDeviceLocked)
var fileName = fileName ?? UUID().uuidString

View File

@ -126,7 +126,10 @@ class StickerManagerTest2: SSKBaseTest {
let stickerInfo = StickerInfo.defaultValue
let stickerData = Randomness.generateRandomBytes(1)
let temporaryFile = OWSFileSystem.temporaryFileUrl()
let temporaryFile = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
try! stickerData.write(to: temporaryFile)
let success = StickerManager.installSticker(

View File

@ -13,7 +13,10 @@ public class AttachmentV2MigrationTest: XCTestCase {
private var db: SDSDatabaseStorage!
override public func setUp() async throws {
self.dbFileURL = OWSFileSystem.temporaryFileUrl()
self.dbFileURL = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
)
self.db = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: dbFileURL,

View File

@ -360,7 +360,10 @@ final class DatabaseRecoveryTest: SSKBaseTest {
func newDatabase() throws -> SDSDatabaseStorage {
return try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
),
keychainStorage: MockKeychainStorage(),
)
}

View File

@ -14,7 +14,10 @@ class GRDBSchemaMigratorTest: XCTestCase {
// TODO: Reuse initializeSampleDatabase when it doesn't need globals.
let databaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: false,
),
keychainStorage: MockKeychainStorage(),
)
// Run all schema migrations. This should succeed without globals!

View File

@ -42,7 +42,10 @@ final class SqliteUtilTest: XCTestCase {
}
func testCipherProvider() throws {
let databasePath = OWSFileSystem.temporaryFilePath(fileExtension: "sqlite")
let databasePath = OWSFileSystem.temporaryFilePath(
fileExtension: "sqlite",
isAvailableWhileDeviceLocked: false,
)
let databaseQueue = try DatabaseQueue(path: databasePath)
defer { try? databaseQueue.close() }
@ -56,7 +59,10 @@ final class SqliteUtilTest: XCTestCase {
}
func testCipherIntegrityCheck() throws {
let databasePath = OWSFileSystem.temporaryFilePath(fileExtension: "sqlite")
let databasePath = OWSFileSystem.temporaryFilePath(
fileExtension: "sqlite",
isAvailableWhileDeviceLocked: false,
)
let databaseQueue = try DatabaseQueue(path: databasePath)
defer { try? databaseQueue.close() }
@ -68,7 +74,10 @@ final class SqliteUtilTest: XCTestCase {
func testQuickCheck() throws {
guard #available(iOS 16, *) else { throw XCTSkip() }
let databasePath = OWSFileSystem.temporaryFilePath(fileExtension: "sqlite")
let databasePath = OWSFileSystem.temporaryFilePath(
fileExtension: "sqlite",
isAvailableWhileDeviceLocked: false,
)
let databaseUrl = URL(filePath: databasePath)
let happyResult: SqliteUtil.IntegrityCheckResult = try {

View File

@ -75,7 +75,10 @@ public struct NormalizedImage {
/// Save an image to disk.
private static func saveImage(_ image: CGImage, containerType: ContainerType) throws(SignalAttachmentError) -> DataSourcePath {
let tempFileUrl = OWSFileSystem.temporaryFileUrl(fileExtension: containerType.fileExtension)
let tempFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: containerType.fileExtension,
isAvailableWhileDeviceLocked: false,
)
let destination = CGImageDestinationCreateWithURL(tempFileUrl as CFURL, containerType.dataType.identifier as CFString, 1, nil)
guard let destination else {
throw .couldNotConvertImage

View File

@ -389,7 +389,10 @@ public struct TypedItemProvider {
throw OWSAssertionError("Unexpectedly not a file URL: \(fileUrl)")
}
let copiedUrl = OWSFileSystem.temporaryFileUrl(fileExtension: fileUrl.pathExtension)
let copiedUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: fileUrl.pathExtension,
isAvailableWhileDeviceLocked: false,
)
try FileManager.default.copyItem(at: fileUrl, to: copiedUrl)
let dataSource = DataSourcePath(fileUrl: copiedUrl, ownership: .owned)

View File

@ -246,7 +246,10 @@ class ImageEditorModel: NSObject {
func temporaryFilePath(fileExtension: String) -> String {
AssertIsOnMainThread()
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: fileExtension)
let filePath = OWSFileSystem.temporaryFilePath(
fileExtension: fileExtension,
isAvailableWhileDeviceLocked: false,
)
temporaryFilePaths.append(filePath)
return filePath
}

View File

@ -155,7 +155,10 @@ class VideoEditorModel: NSObject {
let startTime = MonotonicDate()
let asset = AVURLAsset(url: URL(fileURLWithPath: self.srcVideoPath))
let exportUrl = OWSFileSystem.temporaryFileUrl(fileExtension: "mp4")
let exportUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: "mp4",
isAvailableWhileDeviceLocked: false,
)
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else {
throw OWSAssertionError("couldn't create export session")