CVE-2021-21301: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Wire is an open-source collaboration platform. In Wire for iOS (iPhone and iPad) before version 3.75 there is a vulnerability where the video capture isn't stopped in a scenario where a user first has their camera enabled and then disables it. It's a privacy issue because video is streamed to the call when the user believes it is disabled. It impacts all users in video calls. This is fixed in version 3.75.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 4.3
Published: 2021-02-11T18:15Z
Repository: https://github.com/wireapp/wire-ios
Language: Swift
Files changed: Wire-iOS Tests/Calling/SelfVideoPreviewViewTests.swift	, Wire-iOS.xcodeproj/project.pbxproj, Wire-iOS/Sources/UserInterface/Calling/VideoGridView/SelfVideoPreviewView.swift, Wire-iOS/Sources/UserInterface/Calling/VideoGridView/VideoGridViewController.swift
Commit date: 2021-02-02 09:15:56 +0100
Commit message: [Fix] video capture state (#4879)

* stop capture when there is no self video stream

* update capture state when needed

* update capture state on didMoveToWindow

* add tests
Diff: diff --git a/Wire-iOS Tests/Calling/SelfVideoPreviewViewTests.swift b/Wire-iOS Tests/Calling/SelfVideoPreviewViewTests.swift
new file mode 100644
index 000000000..a5701952d
--- /dev/null
+++ b/Wire-iOS Tests/Calling/SelfVideoPreviewViewTests.swift	
@@ -0,0 +1,109 @@
+//
+// Wire
+// Copyright (C) 2021 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+import XCTest
+import avs
+
+@testable import Wire
+
+class MockAVSVideoPreview: AVSVideoPreview {
+    var isCapturing: Bool = false
+    
+    override func startVideoCapture() {
+        isCapturing = true
+    }
+    
+    override func stopVideoCapture() {
+        isCapturing = false
+    }
+}
+
+class SelfVideoPreviewViewTests: XCTestCase {
+    var sut: SelfVideoPreviewView!
+    var stubProvider = VideoStreamStubProvider()
+    var previewViewMock = MockAVSVideoPreview()
+    
+    override func setUp() {
+        super.setUp()
+        
+        let stream = stubProvider.videoStream().stream
+        sut = SelfVideoPreviewView(stream: stream, isCovered: false, shouldShowActiveSpeakerFrame: false)
+        sut.previewView = previewViewMock
+    }
+    
+    override func tearDown() {
+        sut = nil
+        super.tearDown()
+    }
+    
+    func testCapturerState_ForVideoState_Started() {
+        // Given
+        previewViewMock.isCapturing = false
+
+        // When
+        sut.stream = stubProvider.videoStream(videoState: .started).stream
+
+        // Then
+        XCTAssertTrue(previewViewMock.isCapturing)
+    }
+    
+    func testCapturerState_ForVideoState_Stopped() {
+        // Given
+        previewViewMock.isCapturing = false
+        
+        // When
+        sut.stream = stubProvider.videoStream(videoState: .stopped).stream
+        
+        // Then
+        XCTAssertFalse(previewViewMock.isCapturing)
+    }
+    
+    func testCapturerState_ForVideoState_Paused() {
+        // Given
+        previewViewMock.isCapturing = false
+        
+        // When
+        sut.stream = stubProvider.videoStream(videoState: .paused).stream
+        
+        // Then
+        XCTAssertFalse(previewViewMock.isCapturing)
+    }
+    
+    func testCapturerState_ForVideoState_BadConnection() {
+        // Given
+        previewViewMock.isCapturing = false
+        
+        // When
+        sut.stream = stubProvider.videoStream(videoState: .badConnection).stream
+        
+        // Then
+        XCTAssertFalse(previewViewMock.isCapturing)
+    }
+    
+    func testCapturerState_ForVideoState_ScreenSharing() {
+        // Given
+        previewViewMock.isCapturing = false
+        
+        // When
+        sut.stream = stubProvider.videoStream(videoState: .screenSharing).stream
+        
+        // Then
+        XCTAssertFalse(previewViewMock.isCapturing)
+    }
+}
diff --git a/Wire-iOS.xcodeproj/project.pbxproj b/Wire-iOS.xcodeproj/project.pbxproj
index a8406132a..249b264e6 100644
--- a/Wire-iOS.xcodeproj/project.pbxproj
+++ b/Wire-iOS.xcodeproj/project.pbxproj
@@ -451,10 +451,11 @@
 		636AD79A23A916CB004A3050 /* AppLockPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636AD79923A916CB004A3050 /* AppLockPresenter.swift */; };
 		636AD79C23A91A57004A3050 /* AppLockInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636AD79B23A91A57004A3050 /* AppLockInteractor.swift */; };
 		636B716C23C39D6700B624D6 /* AppLockViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636B716923C39D0600B624D6 /* AppLockViewControllerSnapshotTests.swift */; };
 		636E57BD2386BFD9005B4FD8 /* AvailabilityStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636E57BC2386BFD9005B4FD8 /* AvailabilityStringBuilderTests.swift */; };
 		6370DA212577934C00B5C8F1 /* PulsingIconImageViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6370DA202577934C00B5C8F1 /* PulsingIconImageViewTests.swift */; };
+		6372A0F225C8438B000CCB2D /* SelfVideoPreviewViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */; };
 		6372AAA72492719100C5F307 /* GridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6372AAA62492719100C5F307 /* GridCell.swift */; };
 		6372AAAA249A4F2600C5F307 /* Int+Even.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6372AAA9249A4F2600C5F307 /* Int+Even.swift */; };
 		638332EA250282D200D18F42 /* OrientationDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638332E9250282D200D18F42 /* OrientationDelta.swift */; };
 		6391628D24F6A4470054962E /* VideoPreviewViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6391628B24F6A39D0054962E /* VideoPreviewViewTests.swift */; };
 		639290A2252633E600046171 /* UIAlertController+DegradedCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639290A1252633E600046171 /* UIAlertController+DegradedCall.swift */; };
@@ -2070,10 +2071,11 @@
 		636AD79923A916CB004A3050 /* AppLockPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockPresenter.swift; sourceTree = "<group>"; };
 		636AD79B23A91A57004A3050 /* AppLockInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockInteractor.swift; sourceTree = "<group>"; };
 		636B716923C39D0600B624D6 /* AppLockViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
 		636E57BC2386BFD9005B4FD8 /* AvailabilityStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailabilityStringBuilderTests.swift; sourceTree = "<group>"; };
 		6370DA202577934C00B5C8F1 /* PulsingIconImageViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulsingIconImageViewTests.swift; sourceTree = "<group>"; };
+		6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfVideoPreviewViewTests.swift; sourceTree = "<group>"; };
 		6372AAA62492719100C5F307 /* GridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCell.swift; sourceTree = "<group>"; };
 		6372AAA9249A4F2600C5F307 /* Int+Even.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Even.swift"; sourceTree = "<group>"; };
 		638332E9250282D200D18F42 /* OrientationDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationDelta.swift; sourceTree = "<group>"; };
 		6391628B24F6A39D0054962E /* VideoPreviewViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreviewViewTests.swift; sourceTree = "<group>"; };
 		639290A1252633E600046171 /* UIAlertController+DegradedCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+DegradedCall.swift"; sourceTree = "<group>"; };
@@ -4241,10 +4243,11 @@
 			children = (
 				EF6251D521395F7E00943708 /* VideoGridViewControllerSnapshotTests.swift */,
 				6340EF95237AE93500EF8506 /* VoiceChannelVideoStreamArrangmentTests.swift */,
 				6340660825B204B100C0B4BA /* VideoStreamStubProvider.swift */,
 				6391628B24F6A39D0054962E /* VideoPreviewViewTests.swift */,
+				6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */,
 				16A94AF5209B04A5008A5718 /* GridViewTests.swift */,
 				164B533B20A0A5E800EC8265 /* CallInfoConfigurationTests.swift */,
 				D58192042091F34E003BA7EC /* CallActionsViewTests.swift */,
 				1658C19920A1F00500148F6D /* CallInfoTestFixture.swift */,
 				1658C19720A1E55200148F6D /* CallInfoRootViewControllerTests.swift */,
@@ -8522,10 +8525,11 @@
 				A91833132419347F00CBD0B7 /* TokenFieldTests.swift in Sources */,
 				5EF7BA6C221B273900815625 /* ProfileDetailsViewControllerTests.swift in Sources */,
 				EFB23D0B1FC853CF0055B866 /* AccessoryTextFieldValidationTests.swift in Sources */,
 				EEAC654C206BD64700365C0C /* LinkInteractionTextViewTests.swift in Sources */,
 				5501FFAB22B28F330050D8FC /* MockSelfLegalHoldSubject.swift in Sources */,
+				6372A0F225C8438B000CCB2D /* SelfVideoPreviewViewTests.swift in Sources */,
 				5E8FFC0621ECCC7C0052DF03 /* AuthenticationInterfaceBuilderTests.swift in Sources */,
 				5EE115E1204EEC7D002AD6C2 /* CallQualityControllerTests.swift in Sources */,
 				EFE043992149CE8D00BC3079 /* ContactsViewControllerSnapshotTests.swift in Sources */,
 				EF4C5D4A23351CF00092CA38 /* ConversationListViewModelTests.swift in Sources */,
 				1658C19820A1E55200148F6D /* CallInfoRootViewControllerTests.swift in Sources */,
diff --git a/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/SelfVideoPreviewView.swift b/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/SelfVideoPreviewView.swift
index 614cb1360..6f4c1ca01 100644
--- a/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/SelfVideoPreviewView.swift
+++ b/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/SelfVideoPreviewView.swift
@@ -20,12 +20,19 @@ import Foundation
 import UIKit
 import avs
 
 final class SelfVideoPreviewView: BaseVideoPreviewView {
     
-    private let previewView = AVSVideoPreview()
+    var previewView = AVSVideoPreview()
         
+    override var stream: Stream {
+        didSet {
+            guard stream != oldValue else { return }
+            updateCaptureState()
+        }
+    }
+    
     deinit {
         stopCapture()
     }
     
     override func setupViews() {
@@ -52,14 +59,18 @@ final class SelfVideoPreviewView: BaseVideoPreviewView {
 
     override func didMoveToWindow() {
         super.didMoveToWindow()
         
         if window != nil {
-            startCapture()
+            updateCaptureState()
         }
     }
     
+    private func updateCaptureState() {
+        stream.videoState == .some(.started) ? startCapture() : stopCapture()
+    }
+    
     func startCapture() {
         previewView.startVideoCapture()
     }
     
     func stopCapture() {
diff --git a/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/VideoGridViewController.swift b/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/VideoGridViewController.swift
index 709c9801c..352bacf2a 100644
--- a/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/VideoGridViewController.swift
+++ b/Wire-iOS/Sources/UserInterface/Calling/VideoGridView/VideoGridViewController.swift
@@ -182,14 +182,14 @@ final class VideoGridViewController: UIViewController {
 
         Log.calling.debug("\nUpdated video configuration to:\n\(videoConfigurationDescription())")
     }
 
     private func updateSelfPreview() {
-        guard
-            let selfStreamId = ZMUser.selfUser()?.selfStreamId,
-            let selfStream = stream(with: selfStreamId)
-        else {
+        guard let selfStreamId = ZMUser.selfUser()?.selfStreamId else { return }
+
+        guard let selfStream = stream(with: selfStreamId) else {
+            (viewCache[selfStreamId] as? SelfVideoPreviewView)?.stopCapture()
             return
         }
 
         if let view = viewCache[selfStreamId] as? SelfVideoPreviewView {
             view.stream = selfStream

Vulnerable code:     private let previewView = AVSVideoPreview()
            startCapture()
        guard
            let selfStreamId = ZMUser.selfUser()?.selfStreamId,
            let selfStream = stream(with: selfStreamId)
        else {
Fixed code: //
// Wire
// Copyright (C) 2021 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import XCTest
import avs

@testable import Wire

class MockAVSVideoPreview: AVSVideoPreview {
    var isCapturing: Bool = false
    
    override func startVideoCapture() {
        isCapturing = true
    }
    
    override func stopVideoCapture() {
        isCapturing = false
    }
}

class SelfVideoPreviewViewTests: XCTestCase {
    var sut: SelfVideoPreviewView!
    var stubProvider = VideoStreamStubProvider()
    var previewViewMock = MockAVSVideoPreview()
    
    override func setUp() {
        super.setUp()
        
        let stream = stubProvider.videoStream().stream
        sut = SelfVideoPreviewView(stream: stream, isCovered: false, shouldShowActiveSpeakerFrame: false)
        sut.previewView = previewViewMock
    }
    
    override func tearDown() {
        sut = nil
        super.tearDown()
    }
    
    func testCapturerState_ForVideoState_Started() {
        // Given
        previewViewMock.isCapturing = false

        // When
        sut.stream = stubProvider.videoStream(videoState: .started).stream

        // Then
        XCTAssertTrue(previewViewMock.isCapturing)
    }
    
    func testCapturerState_ForVideoState_Stopped() {
        // Given
        previewViewMock.isCapturing = false
        
        // When
        sut.stream = stubProvider.videoStream(videoState: .stopped).stream
        
        // Then
        XCTAssertFalse(previewViewMock.isCapturing)
    }
    
    func testCapturerState_ForVideoState_Paused() {
        // Given
        previewViewMock.isCapturing = false
        
        // When
        sut.stream = stubProvider.videoStream(videoState: .paused).stream
        
        // Then
        XCTAssertFalse(previewViewMock.isCapturing)
    }
    
    func testCapturerState_ForVideoState_BadConnection() {
        // Given
        previewViewMock.isCapturing = false
        
        // When
        sut.stream = stubProvider.videoStream(videoState: .badConnection).stream
        
        // Then
        XCTAssertFalse(previewViewMock.isCapturing)
    }
    
    func testCapturerState_ForVideoState_ScreenSharing() {
        // Given
        previewViewMock.isCapturing = false
        
        // When
        sut.stream = stubProvider.videoStream(videoState: .screenSharing).stream
        
        // Then
        XCTAssertFalse(previewViewMock.isCapturing)
    }
}
		6372A0F225C8438B000CCB2D /* SelfVideoPreviewViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */; };
		6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfVideoPreviewViewTests.swift; sourceTree = "<group>"; };
				6372A0F125C8438A000CCB2D /* SelfVideoPreviewViewTests.swift */,
				6372A0F225C8438B000CCB2D /* SelfVideoPreviewViewTests.swift in Sources */,
    var previewView = AVSVideoPreview()
    override var stream: Stream {
        didSet {
            guard stream != oldValue else { return }
            updateCaptureState()
        }
    }
    
            updateCaptureState()
    private func updateCaptureState() {
        stream.videoState == .some(.started) ? startCapture() : stopCapture()
    }
    
        guard let selfStreamId = ZMUser.selfUser()?.selfStreamId else { return }

        guard let selfStream = stream(with: selfStreamId) else {
            (viewCache[selfStreamId] as? SelfVideoPreviewView)?.stopCapture()