Add support for username hashing
This commit is contained in:
parent
fc4ec188e8
commit
bcfd291f9c
3
Podfile
3
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'
|
||||
|
||||
15
Podfile.lock
15
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
|
||||
|
||||
@ -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 */,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
64
SignalServiceKit/Usernames/Usernames+HashedUsername.swift
Normal file
64
SignalServiceKit/Usernames/Usernames+HashedUsername.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user