diff --git a/Podfile b/Podfile index 683e41b425..b12e522774 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,8 @@ pod 'SwiftProtobuf', ">= 1.14.0" pod 'SignalCoreKit', git: 'git@github.com:signalapp/SignalCoreKit', testspecs: ["Tests"] # pod 'SignalCoreKit', path: '../SignalCoreKit', testspecs: ["Tests"] -pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal-client.git', testspecs: ["Tests"] +ENV['LIBSIGNAL_FFI_PREBUILD_CHECKSUM'] = '3fd72aa93ddf582f0aaecc3e649081df36ae562f5be22d18b69d2d3233bfbe96' +pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal-client.git', tag: 'v0.22.0', testspecs: ["Tests"] # pod 'LibSignalClient', path: '../libsignal-client', testspecs: ["Tests"] pod 'Curve25519Kit', git: 'ssh://git@github.com/signalapp/Curve25519Kit', testspecs: ["Tests"], branch: 'feature/SignalClient-adoption' diff --git a/Podfile.lock b/Podfile.lock index 0c1e73e5fd..386ddfa9aa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -17,9 +17,9 @@ PODS: - LibMobileCoin/CoreHTTP (4.0.0-pre3): - SwiftProtobuf (~> 1.5) - libPhoneNumber-iOS (0.9.15) - - LibSignalClient (0.21.0): + - LibSignalClient (0.22.0): - SignalCoreKit - - LibSignalClient/Tests (0.21.0): + - LibSignalClient/Tests (0.22.0): - SignalCoreKit - libwebp (1.2.4): - libwebp/demux (= 1.2.4) @@ -87,8 +87,8 @@ DEPENDENCIES: - GRDB.swift/SQLCipher - LibMobileCoin/CoreHTTP (from `https://github.com/signalapp/libmobilecoin-ios-artifacts.git`, commit `586a60aa38eb7667ddb74b666c6bb6f7107288fc`) - libPhoneNumber-iOS (from `https://github.com/signalapp/libPhoneNumber-iOS`, branch `signal-master`) - - LibSignalClient (from `https://github.com/signalapp/libsignal-client.git`) - - LibSignalClient/Tests (from `https://github.com/signalapp/libsignal-client.git`) + - LibSignalClient (from `https://github.com/signalapp/libsignal-client.git`, tag `v0.22.0`) + - LibSignalClient/Tests (from `https://github.com/signalapp/libsignal-client.git`, tag `v0.22.0`) - libwebp - lottie-ios - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) @@ -140,6 +140,7 @@ EXTERNAL SOURCES: :git: https://github.com/signalapp/libPhoneNumber-iOS LibSignalClient: :git: https://github.com/signalapp/libsignal-client.git + :tag: v0.22.0 Mantle: :branch: signal-master :git: https://github.com/signalapp/Mantle @@ -181,8 +182,8 @@ CHECKOUT OPTIONS: :commit: 8b6f552682be0e66cf25cd0f0db5e1732a1e9f15 :git: https://github.com/signalapp/libPhoneNumber-iOS LibSignalClient: - :commit: d94f1b3d70140302b452aa07309f2ed7a00ae493 :git: https://github.com/signalapp/libsignal-client.git + :tag: v0.22.0 Mantle: :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :git: https://github.com/signalapp/Mantle @@ -220,7 +221,7 @@ SPEC CHECKSUMS: GRDB.swift: 1395cb3556df6b16ed69dfc74c3886abc75d2825 LibMobileCoin: af8d6580227c14b787370209f5ed4c4a4fc90b9f libPhoneNumber-iOS: 2d26d0a38933eee2702962a4dbdec2fc20e5ef9f - LibSignalClient: d5fc5e3c791013339932a8574c8b5e81e04a5455 + LibSignalClient: 78bb0ab7bff27b0e0b42a6ae9fb3c03f9e37d6e0 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef Logging: beeb016c9c80cf77042d62e83495816847ef108b lottie-ios: 016449b5d8be0c3dcbcfa0a9988469999cd04c5d @@ -240,6 +241,6 @@ SPEC CHECKSUMS: SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 -PODFILE CHECKSUM: 9bf4ff92161c0782d385958042d9f3b402d4f953 +PODFILE CHECKSUM: 7239b2391636b3760140fbf40a31d37da7804bb2 COCOAPODS: 1.11.3 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index e609463790..ac88e105b5 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1221,6 +1221,7 @@ D9C7CECF28ECC043001E87B6 /* NSAttributedString+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C7CECE28ECC043001E87B6 /* NSAttributedString+SSK.swift */; }; D9E335A12993143500825677 /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E335A02993143500825677 /* AppSetup.swift */; }; D9E335A42993269100825677 /* MockDBV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66138FAE2982F4C4002E0CFE /* MockDBV2.swift */; }; + D9E335A929933B1A00825677 /* Usernames+HashedUsername.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E335A829933B1A00825677 /* Usernames+HashedUsername.swift */; }; E1368CBE18A1C36B00109378 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; E44AD4E624E98F440035D7B8 /* PhotoCaptureDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */; }; E75DD3E02810CDBD00E32C36 /* SubscriptionManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75DD3DF2810CDBD00E32C36 /* SubscriptionManagerTest.swift */; }; @@ -3698,6 +3699,7 @@ D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeManifest.swift; sourceTree = ""; }; D9C7CECE28ECC043001E87B6 /* NSAttributedString+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+SSK.swift"; sourceTree = ""; }; D9E335A02993143500825677 /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; + D9E335A829933B1A00825677 /* Usernames+HashedUsername.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Usernames+HashedUsername.swift"; sourceTree = ""; }; DBCD4214999408EBC915EE0F /* Pods_SignalServiceKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalServiceKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DDBCB034D1DFAF85ADFD5E76 /* Pods-SignalUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUITests/Pods-SignalUITests.debug.xcconfig"; sourceTree = ""; }; DE2DD605305BC6EFAD731723 /* Pods-Signal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.debug.xcconfig"; sourceTree = ""; }; @@ -7310,6 +7312,7 @@ D925F55E298C743E00158EE4 /* UsernameLookupManager.swift */, D925F557298B39D800158EE4 /* UsernameLookupRecord.swift */, D99840C5297A0F9500F7ED6D /* Usernames+API.swift */, + D9E335A829933B1A00825677 /* Usernames+HashedUsername.swift */, D99840BC297A04DA00F7ED6D /* Usernames+NicknameValidator.swift */, D99840C3297A0ECE00F7ED6D /* Usernames+ParsedUsername.swift */, D925F552298349EF00158EE4 /* Usernames+ShareableUsername.swift */, @@ -11855,6 +11858,7 @@ D925F55F298C743E00158EE4 /* UsernameLookupManager.swift in Sources */, D925F55D298C6A7D00158EE4 /* UsernameLookupRecord.swift in Sources */, D925F562298D8F9400158EE4 /* Usernames+API.swift in Sources */, + D9E335A929933B1A00825677 /* Usernames+HashedUsername.swift in Sources */, D925F564298D8F9400158EE4 /* Usernames+NicknameValidator.swift in Sources */, D925F561298D8F9400158EE4 /* Usernames+ParsedUsername.swift in Sources */, D925F560298D8F9400158EE4 /* Usernames+ShareableUsername.swift in Sources */, diff --git a/Signal/src/Usernames/UsernameSelectionViewController.swift b/Signal/src/Usernames/UsernameSelectionViewController.swift index 83809a8aa4..26d14f8418 100644 --- a/Signal/src/Usernames/UsernameSelectionViewController.swift +++ b/Signal/src/Usernames/UsernameSelectionViewController.swift @@ -27,10 +27,10 @@ class UsernameSelectionViewController: OWSTableViewController2 { enum Constants { /// Minimum length for a nickname, in Unicode code points. - static let minNicknameCodepointLength: UInt = 3 + static let minNicknameCodepointLength: UInt32 = RemoteConfig.minNicknameLength /// Maximum length for a nickname, in Unicode code points. - static let maxNicknameCodepointLength: UInt = 32 + static let maxNicknameCodepointLength: UInt32 = RemoteConfig.maxNicknameLength /// Amount of time to wait after the username text field is edited /// before kicking off a reservation attempt. @@ -522,7 +522,7 @@ private extension UsernameSelectionViewController { switch usernameState { case let .reservationSuccessful(reservation): self.confirmReservationBehindModalActivityIndicator( - reservation: reservation + reservedUsername: reservation.hashedUsername ) case .shouldDelete: self.deleteCurrentUsernameBehindActivityIndicator() @@ -541,7 +541,7 @@ private extension UsernameSelectionViewController { /// Confirm the given reservation, with an activity indicator blocking the /// UI. private func confirmReservationBehindModalActivityIndicator( - reservation: API.SuccessfulReservation + reservedUsername: Usernames.HashedUsername ) { ModalActivityIndicatorViewController.present( fromViewController: self, @@ -550,7 +550,7 @@ private extension UsernameSelectionViewController { UsernameLogger.shared.info("Confirming username.") firstly { () -> Promise in - self.apiManager.attemptToConfirm(reservation: reservation) + self.apiManager.attemptToConfirm(reservedUsername: reservedUsername) }.done(on: DispatchQueue.main) { result -> Void in switch result { case let .success(confirmedUsername): @@ -757,6 +757,8 @@ private extension UsernameSelectionViewController { return self.apiManager.attemptToReserve( desiredNickname: desiredNickname, + minNicknameLength: Constants.minNicknameCodepointLength, + maxNicknameLength: Constants.maxNicknameCodepointLength, attemptId: thisAttemptId ) }.done(on: DispatchQueue.main) { [weak self] reservationResult -> Void in diff --git a/SignalServiceKit/Usernames/Usernames+API.swift b/SignalServiceKit/Usernames/Usernames+API.swift index e8c87eb9a9..2399935ae9 100644 --- a/SignalServiceKit/Usernames/Usernames+API.swift +++ b/SignalServiceKit/Usernames/Usernames+API.swift @@ -34,15 +34,11 @@ public extension Usernames { public extension Usernames.API { struct SuccessfulReservation: Equatable { - /// The raw reserved username, including a numeric discriminator suffix. - fileprivate let rawUsername: String - - /// A token representing the reservation, which is used to later - /// confirm the username. - fileprivate let reservationToken: String - /// The reserved username. public let username: Usernames.ParsedUsername + + /// The reserved username, hashed. + public let hashedUsername: Usernames.HashedUsername } struct ReservationResult { @@ -70,43 +66,65 @@ public extension Usernames.API { /// potentially-overlapping attempts. func attemptToReserve( desiredNickname: String, + minNicknameLength: UInt32, + maxNicknameLength: UInt32, attemptId: UUID ) -> Promise { - let request = OWSRequestFactory.reserveUsernameRequest( - desiredNickname: desiredNickname - ) - func makeReservationError(from error: Error) -> ReservationError { .init(attemptId: attemptId, underlying: error) } + let usernameCandidates: [Usernames.HashedUsername] + do { + usernameCandidates = try Usernames.HashedUsername.generateCandidates( + forNickname: desiredNickname, + minNicknameLength: minNicknameLength, + maxNicknameLength: maxNicknameLength + ) + } catch let error { + return .init(error: makeReservationError(from: error)) + } + + let request = OWSRequestFactory.reserveUsernameRequest( + usernameHashes: usernameCandidates.map { $0.hashString } + ) + func onRequestSuccess(response: HTTPResponse) throws -> ReservationResult { guard response.responseStatusCode == 200 else { - throw makeReservationError( - from: OWSAssertionError("Unexpected status code from successful request: \(response.responseStatusCode)") - ) + throw makeReservationError(from: OWSAssertionError( + "Unexpected status code from successful request: \(response.responseStatusCode)" + )) } guard let parser = ParamParser(responseObject: response.responseBodyJson) else { - throw makeReservationError( - from: OWSAssertionError("Unexpectedly missing JSON response body!") - ) + throw makeReservationError(from: OWSAssertionError( + "Unexpectedly missing JSON response body!" + )) } - let usernameString: String = try parser.required(key: "username") - let reservationToken: String = try parser.required(key: "reservationToken") + let usernameHash: String = try parser.required(key: "usernameHash") - guard let parsedUsername = Usernames.ParsedUsername(rawUsername: usernameString) else { - throw OWSAssertionError("Username string was not parseable!") + guard let acceptedCandidate = usernameCandidates.first(where: { candidate in + candidate.hashString == usernameHash + }) else { + throw makeReservationError(from: OWSAssertionError( + "Accepted username hash did not match any candidates!" + )) } - let successState: ReservationResult.State = .successful(reservation: SuccessfulReservation( - rawUsername: usernameString, - reservationToken: reservationToken, - username: parsedUsername - )) + guard let parsedUsername = Usernames.ParsedUsername(rawUsername: acceptedCandidate.usernameString) else { + throw makeReservationError(from: OWSAssertionError( + "Accepted username was not parseable!" + )) + } - return .init(attemptId: attemptId, state: successState) + return ReservationResult( + attemptId: attemptId, + state: .successful(reservation: SuccessfulReservation( + username: parsedUsername, + hashedUsername: acceptedCandidate + )) + ) } func onRequestFailure(error: Error) throws -> ReservationResult { @@ -117,10 +135,9 @@ public extension Usernames.API { let resultState = try { () throws -> ReservationResult.State in switch statusCode { case 422, 409: - // 422 indicates a nickname that failed to validate. + // 422 indicates that the given hashes failed to validate. // - // 409 indicates that either the server failed to generate a - // discriminator, or the desired username is forbidden. + // 409 indicates that none of the given hashes are available. // // Either way, the reservation has been rejected. return .rejected @@ -159,11 +176,11 @@ public extension Usernames.API { } func attemptToConfirm( - reservation: SuccessfulReservation + reservedUsername: Usernames.HashedUsername ) -> Promise { let request = OWSRequestFactory.confirmReservedUsernameRequest( - previouslyReservedUsername: reservation.rawUsername, - reservationToken: reservation.reservationToken + reservedUsernameHash: reservedUsername.hashString, + reservedUsernameZKProof: reservedUsername.proofString ) func onRequestSuccess(response: HTTPResponse) throws -> ConfirmationResult { @@ -171,7 +188,7 @@ public extension Usernames.API { throw OWSAssertionError("Unexpected status code from successful request: \(response.responseStatusCode)") } - return .success(confirmedUsername: reservation.rawUsername) + return .success(confirmedUsername: reservedUsername.usernameString) } func onRequestFailure(error: Error) throws -> ConfirmationResult { @@ -235,7 +252,16 @@ public extension Usernames.API { public extension Usernames.API { func attemptAciLookup(forUsername username: String) -> Promise { - let request = OWSRequestFactory.lookupAciUsernameRequest(usernameToLookup: username) + let hashedUsername: Usernames.HashedUsername + do { + hashedUsername = try Usernames.HashedUsername(forUsername: username) + } catch let error { + return .init(error: error) + } + + let request = OWSRequestFactory.lookupAciUsernameRequest( + usernameHashToLookup: hashedUsername.hashString + ) func onRequestSuccess(response: HTTPResponse) throws -> UUID { guard response.responseStatusCode == 200 else { diff --git a/SignalServiceKit/Usernames/Usernames+HashedUsername.swift b/SignalServiceKit/Usernames/Usernames+HashedUsername.swift new file mode 100644 index 0000000000..3b2edb3d72 --- /dev/null +++ b/SignalServiceKit/Usernames/Usernames+HashedUsername.swift @@ -0,0 +1,64 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import LibSignalClient + +extension Usernames { + public class HashedUsername { + private typealias LibSignalUsername = LibSignalClient.Username + + // MARK: Init + + private let libSignalUsername: LibSignalUsername + + convenience init(forUsername username: String) throws { + self.init(libSignalUsername: try .init(username)) + } + + private init(libSignalUsername: LibSignalUsername) { + self.libSignalUsername = libSignalUsername + } + + // MARK: Getters + + public var usernameString: String { + libSignalUsername.value + } + + public lazy var hashString: String = { + Data(libSignalUsername.hash).asBase64Url + }() + + public lazy var proofString: String = { + Data(libSignalUsername.generateProof()).asBase64Url + }() + } +} + +// MARK: - Generate candidates + +extension Usernames.HashedUsername { + static func generateCandidates( + forNickname nickname: String, + minNicknameLength: UInt32, + maxNicknameLength: UInt32 + ) throws -> [Usernames.HashedUsername] { + return try LibSignalUsername.candidates( + from: nickname, + withValidLengthWithin: minNicknameLength...maxNicknameLength + ).map { candidate -> Usernames.HashedUsername in + return .init(libsignalUsername: candidate) + } + } +} + +// MARK: - Equatable + +extension Usernames.HashedUsername: Equatable { + public static func == (lhs: Usernames.HashedUsername, rhs: Usernames.HashedUsername) -> Bool { + lhs.libsignalUsername.value == rhs.libsignalUsername.value + } +} diff --git a/SignalServiceKit/Usernames/Usernames+NicknameValidator.swift b/SignalServiceKit/Usernames/Usernames+NicknameValidator.swift index bd9c2748d4..6e570b3c52 100644 --- a/SignalServiceKit/Usernames/Usernames+NicknameValidator.swift +++ b/SignalServiceKit/Usernames/Usernames+NicknameValidator.swift @@ -21,12 +21,12 @@ public extension Usernames { private static let nicknameValidityRegex: NSRegularExpression = try! .init(pattern: "^[_a-zA-Z][_a-zA-Z0-9]*$") /// Minimum number of Unicode codepoints for a nickname. - private let minCodepoints: UInt + private let minCodepoints: UInt32 /// Maximum number of Unicode codepoints for a nickname. - private let maxCodepoints: UInt + private let maxCodepoints: UInt32 - public init(minCodepoints: UInt, maxCodepoints: UInt) { + public init(minCodepoints: UInt32, maxCodepoints: UInt32) { self.minCodepoints = minCodepoints self.maxCodepoints = maxCodepoints } diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory+Usernames.swift b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory+Usernames.swift index 691d3445cc..9427204284 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory+Usernames.swift +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory+Usernames.swift @@ -7,25 +7,24 @@ import Foundation public extension OWSRequestFactory { private enum UsernameApiPaths { - static let reserve = "v1/accounts/username/reserved" - static let confirm = "v1/accounts/username/confirm" - static let delete = "v1/accounts/username" - static func aciLookup(forUsername username: String) -> String { - "v1/accounts/username/\(username)" + static let reserve = "v1/accounts/username_hash/reserve" + static let confirm = "v1/accounts/username_hash/confirm" + static let delete = "v1/accounts/username_hash" + static func aciLookup(forUsernameHash usernameHash: String) -> String { + "v1/accounts/username_hash/\(usernameHash)" } } - /// Attempt to reserve the given username. If successful, will return the - /// username plus a numeric discriminator, along with a reservation token - /// which can be subsequently used to confirm the reservation. + /// Attempt to reserve one of the given username hashes. If successful, + /// will return the reserved hash. /// /// A successful reservation is valid for 5 minutes. static func reserveUsernameRequest( - desiredNickname: String + usernameHashes: [String] ) -> TSRequest { let url = URL(string: UsernameApiPaths.reserve)! let params: [String: Any] = [ - "nickname": desiredNickname + "usernameHashes": usernameHashes ] return TSRequest( @@ -35,21 +34,22 @@ public extension OWSRequestFactory { ) } - /// Confirm the given previously-reserved username. + /// Confirm a previously-reserved username hash. /// - /// - Parameter previouslyReservedUsername - /// A username returned by a prior call to reserve a desired username, - /// which should include a numeric discriminator. - /// - Parameter reservationToken - /// A token returned by a prior call to reserve a username. + /// - Parameter reservedUsernameHash + /// The hash string from a hashed username returned by a prior call to + /// reserve a desired nickname. + /// - Parameter reservedUsernameZKProof + /// The zkproof string from a hashed username returned by a prior call to + /// reserve a desired nickname. static func confirmReservedUsernameRequest( - previouslyReservedUsername reservedUsername: String, - reservationToken: String + reservedUsernameHash hashString: String, + reservedUsernameZKProof proofString: String ) -> TSRequest { let url = URL(string: UsernameApiPaths.confirm)! let params: [String: Any] = [ - "usernameToConfirm": reservedUsername, - "reservationToken": reservationToken + "usernameHash": hashString, + "zkProof": proofString ] return TSRequest( @@ -59,7 +59,7 @@ public extension OWSRequestFactory { ) } - /// Delete the user's existing username. + /// Delete the user's server-stored username hash. static func deleteExistingUsernameRequest() -> TSRequest { let url = URL(string: UsernameApiPaths.delete)! @@ -70,15 +70,22 @@ public extension OWSRequestFactory { ) } + /// Look up the ACI for the given username hash. static func lookupAciUsernameRequest( - usernameToLookup username: String + usernameHashToLookup usernameHash: String ) -> TSRequest { - let url = URL(string: UsernameApiPaths.aciLookup(forUsername: username))! + let url = URL(string: UsernameApiPaths.aciLookup( + forUsernameHash: usernameHash + ))! - return TSRequest( + let request = TSRequest( url: url, method: HTTPMethod.get.methodName, parameters: nil ) + + request.shouldHaveAuthorizationHeaders = false + + return request } } diff --git a/SignalServiceKit/src/Util/RemoteConfigManager.swift b/SignalServiceKit/src/Util/RemoteConfigManager.swift index 896a671284..0e4c41ba1e 100644 --- a/SignalServiceKit/src/Util/RemoteConfigManager.swift +++ b/SignalServiceKit/src/Util/RemoteConfigManager.swift @@ -45,44 +45,12 @@ public class RemoteConfig: BaseFlags { @objc public static var groupsV2MaxGroupSizeRecommended: UInt { - let defaultValue: UInt = 151 - guard AppReadiness.isAppReady else { - owsFailDebug("Storage is not yet ready.") - return defaultValue - } - guard let rawValue: AnyObject = value(.groupsV2MaxGroupSizeRecommended) else { - return defaultValue - } - guard let stringValue = rawValue as? String else { - owsFailDebug("Unexpected value.") - return defaultValue - } - guard let uintValue = UInt(stringValue) else { - owsFailDebug("Invalid value.") - return defaultValue - } - return uintValue + getUIntValue(forFlag: .groupsV2MaxGroupSizeRecommended, defaultValue: 151) } @objc public static var groupsV2MaxGroupSizeHardLimit: UInt { - let defaultValue: UInt = 1001 - guard AppReadiness.isAppReady else { - owsFailDebug("Storage is not yet ready.") - return defaultValue - } - guard let rawValue: AnyObject = value(.groupsV2MaxGroupSizeHardLimit) else { - return defaultValue - } - guard let stringValue = rawValue as? String else { - owsFailDebug("Unexpected value.") - return defaultValue - } - guard let uintValue = UInt(stringValue) else { - owsFailDebug("Invalid value.") - return defaultValue - } - return uintValue + getUIntValue(forFlag: .groupsV2MaxGroupSizeHardLimit, defaultValue: 1001) } public static var groupsV2MaxBannedMembers: UInt { @@ -270,23 +238,7 @@ public class RemoteConfig: BaseFlags { } public static var maxGroupCallRingSize: UInt { - let defaultValue: UInt = 16 - guard AppReadiness.isAppReady else { - owsFailDebug("Storage is not yet ready.") - return defaultValue - } - guard let rawValue: AnyObject = value(.maxGroupCallRingSize) else { - return defaultValue - } - guard let stringValue = rawValue as? String else { - owsFailDebug("Unexpected value.") - return defaultValue - } - guard let uintValue = UInt(stringValue) else { - owsFailDebug("Invalid value.") - return defaultValue - } - return uintValue + getUIntValue(forFlag: .maxGroupCallRingSize, defaultValue: 16) } public static var contactDiscoveryV2KillSwitch: Bool { @@ -297,6 +249,64 @@ public class RemoteConfig: BaseFlags { return isEnabled(.enableAutoAPNSRotation, defaultValue: false) } + /// The minimum length for a valid nickname, in Unicode codepoints. + public static var minNicknameLength: UInt32 { + getUInt32Value(forFlag: .minNicknameLength, defaultValue: 3) + } + + /// The maximum length for a valid nickname, in Unicode codepoints. + public static var maxNicknameLength: UInt32 { + getUInt32Value(forFlag: .maxNicknameLength, defaultValue: 32) + } + + // MARK: UInt values + + private static func getUIntValue( + forFlag flag: Flags.SupportedValuesFlags, + defaultValue: UInt + ) -> UInt { + getStringConvertibleValue( + forFlag: flag, + defaultValue: defaultValue + ) + } + + private static func getUInt32Value( + forFlag flag: Flags.SupportedValuesFlags, + defaultValue: UInt32 + ) -> UInt32 { + getStringConvertibleValue( + forFlag: flag, + defaultValue: defaultValue + ) + } + + private static func getStringConvertibleValue( + forFlag flag: Flags.SupportedValuesFlags, + defaultValue: V + ) -> V where V: LosslessStringConvertible { + guard AppReadiness.isAppReady else { + owsFailDebug("Storage is not yet ready.") + return defaultValue + } + + guard let rawValue: AnyObject = value(flag) else { + return defaultValue + } + + guard let stringValue = rawValue as? String else { + owsFailDebug("Unexpected value.") + return defaultValue + } + + guard let value = V(stringValue) else { + owsFailDebug("Invalid value.") + return defaultValue + } + + return value + } + // MARK: - Country code buckets /// Determine if a country-code-dependent flag is enabled for the current @@ -555,6 +565,8 @@ private struct Flags { case creditAndDebitCardDisabledRegions case paypalDisabledRegions case maxGroupCallRingSize + case minNicknameLength + case maxNicknameLength } } @@ -579,6 +591,8 @@ private extension FlagType { case "creditAndDebitCardDisabledRegions": return "global.donations.ccDisabledRegions" case "paypalDisabledRegions": return "global.donations.paypalDisabledRegions" case "maxGroupCallRingSize": return "global.calling.maxGroupCallRingSize" + case "minNicknameLength": return "global.nicknames.min" + case "maxNicknameLength": return "global.nicknames.max" default: return Flags.prefix + rawValue } } diff --git a/SignalServiceKit/tests/Network/OWSRequestFactoryTest.swift b/SignalServiceKit/tests/Network/OWSRequestFactoryTest.swift index 5f9810b411..c297170b99 100644 --- a/SignalServiceKit/tests/Network/OWSRequestFactoryTest.swift +++ b/SignalServiceKit/tests/Network/OWSRequestFactoryTest.swift @@ -227,25 +227,25 @@ class OWSRequestFactoryTest: SSKBaseTestSwift { // MARK: - Usernames func testReserveUsername() { - let request = OWSRequestFactory.reserveUsernameRequest(desiredNickname: "foobar") + let request = OWSRequestFactory.reserveUsernameRequest(usernameHashes: ["boba", "fett"]) - XCTAssertEqual(request.url?.path, "v1/accounts/username/reserved") + XCTAssertEqual(request.url?.path, "v1/accounts/username_hash/reserve") XCTAssertEqual(request.httpMethod, "PUT") - XCTAssertEqual(request.parameters as! [String: String], ["nickname": "foobar"]) + XCTAssertEqual(request.parameters as! [String: [String]], ["usernameHashes": ["boba", "fett"]]) XCTAssertTrue(request.shouldHaveAuthorizationHeaders) } func testConfirmReservedUsername() { let request = OWSRequestFactory.confirmReservedUsernameRequest( - previouslyReservedUsername: "foobar", - reservationToken: "d4a0efe9-6ffc-488a-b33d-6aa340279175" + reservedUsernameHash: "jango", + reservedUsernameZKProof: "fett" ) - XCTAssertEqual(request.url?.path, "v1/accounts/username/confirm") + XCTAssertEqual(request.url?.path, "v1/accounts/username_hash/confirm") XCTAssertEqual(request.httpMethod, "PUT") XCTAssertEqual(request.parameters as! [String: String], [ - "usernameToConfirm": "foobar", - "reservationToken": "d4a0efe9-6ffc-488a-b33d-6aa340279175" + "usernameHash": "jango", + "zkProof": "fett" ]) XCTAssertTrue(request.shouldHaveAuthorizationHeaders) } @@ -253,9 +253,18 @@ class OWSRequestFactoryTest: SSKBaseTestSwift { func testDeleteExistingUsername() { let request = OWSRequestFactory.deleteExistingUsernameRequest() - XCTAssertEqual(request.url?.path, "v1/accounts/username") + XCTAssertEqual(request.url?.path, "v1/accounts/username_hash") XCTAssertEqual(request.httpMethod, "DELETE") XCTAssertEqual(request.parameters as! [String: String], [:]) XCTAssertTrue(request.shouldHaveAuthorizationHeaders) } + + func testLookupAciForUsername() { + let request = OWSRequestFactory.lookupAciUsernameRequest(usernameHashToLookup: "obi-wan") + + XCTAssertEqual(request.url?.path, "v1/accounts/username_hash/obi-wan") + XCTAssertEqual(request.httpMethod, "GET") + XCTAssertEqual(request.parameters as! [String: String], [:]) + XCTAssertTrue(!request.shouldHaveAuthorizationHeaders) + } }