Add support for username hashing

This commit is contained in:
Sasha Weiss 2023-02-08 16:02:02 -08:00 committed by GitHub
parent fc4ec188e8
commit bcfd291f9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 263 additions and 135 deletions

View File

@ -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'

View File

@ -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

View File

@ -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 = "<group>"; };
D9C7CECE28ECC043001E87B6 /* NSAttributedString+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+SSK.swift"; sourceTree = "<group>"; };
D9E335A02993143500825677 /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = "<group>"; };
D9E335A829933B1A00825677 /* Usernames+HashedUsername.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Usernames+HashedUsername.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -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 */,

View File

@ -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<API.ConfirmationResult> 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

View File

@ -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<ReservationResult> {
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<ConfirmationResult> {
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<UUID?> {
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 {

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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<V>(
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
}
}

View File

@ -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)
}
}