mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 03:06:37 +00:00
Add support for linking new devices in the QRCodeLoginScreen. (#4891)
* Adds the remaining parts for showing/scanning a QR code to link a new device. * Refactor the QRCodeLoginService to work the same way as the LinkNewDeviceService.
This commit is contained in:
@@ -7,130 +7,179 @@
|
||||
//
|
||||
|
||||
// Helpers to remove ECI headers from QR Code raw data
|
||||
// https://gist.github.com/PetrusM/267e2ee8c1d8b5dca17eac085afa7d7c
|
||||
// Originally based on https://gist.github.com/PetrusM/267e2ee8c1d8b5dca17eac085afa7d7c
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
enum AVMetadataBinaryValueError: Error {
|
||||
case unhandledQRSegmentMode(UInt8)
|
||||
case bitError(BitError)
|
||||
case unknown(Error)
|
||||
}
|
||||
|
||||
extension AVMetadataMachineReadableCodeObject {
|
||||
var binaryValue: Data? {
|
||||
switch type {
|
||||
case .qr:
|
||||
guard let binaryValueWithProtocol,
|
||||
let symbolVersion = (descriptor as? CIQRCodeDescriptor)?.symbolVersion else {
|
||||
return nil
|
||||
}
|
||||
return Self.removeQrProtocolData(binaryValueWithProtocol, symbolVersion: symbolVersion)
|
||||
case .aztec:
|
||||
guard let string = stringValue
|
||||
else { return nil }
|
||||
return string.data(using: String.Encoding.isoLatin1)
|
||||
default:
|
||||
return nil
|
||||
var qrBinaryValue: Data? {
|
||||
get throws(AVMetadataBinaryValueError) {
|
||||
guard type == .qr else { return nil }
|
||||
|
||||
guard let qrCodeDescriptor = descriptor as? CIQRCodeDescriptor else { return nil }
|
||||
return try Self.removeQRProtocolData(qrCodeDescriptor.errorCorrectedPayload, symbolVersion: qrCodeDescriptor.symbolVersion)
|
||||
}
|
||||
}
|
||||
|
||||
var binaryValueWithProtocol: Data? {
|
||||
guard let descriptor else {
|
||||
return nil
|
||||
}
|
||||
switch type {
|
||||
case .qr:
|
||||
return (descriptor as? CIQRCodeDescriptor)?.errorCorrectedPayload
|
||||
case .aztec:
|
||||
return (descriptor as? CIAztecCodeDescriptor)?.errorCorrectedPayload
|
||||
case .pdf417:
|
||||
return (descriptor as? CIPDF417CodeDescriptor)?.errorCorrectedPayload
|
||||
case .dataMatrix:
|
||||
return (descriptor as? CIDataMatrixCodeDescriptor)?.errorCorrectedPayload
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
static func removeQRProtocolData(_ input: Data, symbolVersion: Int) throws(AVMetadataBinaryValueError) -> Data? {
|
||||
var bits = input.bits()
|
||||
var segment: [UInt8]
|
||||
var output: [UInt8] = []
|
||||
|
||||
repeat {
|
||||
segment = try takeSegment(&bits, version: symbolVersion)
|
||||
output.append(contentsOf: segment)
|
||||
} while !segment.isEmpty
|
||||
|
||||
return Data(output)
|
||||
}
|
||||
|
||||
static func removeQrProtocolData(_ input: Data, symbolVersion: Int) -> Data? {
|
||||
var halves = input.halfBytes()
|
||||
var batch = takeBatch(&halves, version: symbolVersion)
|
||||
var output = batch
|
||||
while !batch.isEmpty {
|
||||
batch = takeBatch(&halves, version: symbolVersion)
|
||||
output.append(contentsOf: batch)
|
||||
}
|
||||
let data = Data(output)
|
||||
return data
|
||||
}
|
||||
|
||||
private static func takeBatch(_ input: inout [HalfByte], version: Int) -> [UInt8] {
|
||||
let characterCountLength = version > 9 ? 16 : 8
|
||||
let mode = input.remove(at: 0)
|
||||
var output = [UInt8]()
|
||||
switch mode.value {
|
||||
// If there is not only binary in the QRCode, then cases should be added here.
|
||||
case 0x04: // Binary
|
||||
let charactersCount: UInt16
|
||||
if characterCountLength == 8 {
|
||||
charactersCount = UInt16(input.takeUInt8())
|
||||
} else {
|
||||
charactersCount = UInt16(input.takeUInt16())
|
||||
|
||||
private static func takeSegment(_ input: inout [Bit], version: Int) throws(AVMetadataBinaryValueError) -> [UInt8] {
|
||||
do {
|
||||
let mode = try input.takeBits(4)
|
||||
|
||||
return switch mode {
|
||||
case 0x02: try input.takeAlphanumericSegment(version) // Alphanumeric
|
||||
case 0x04: try input.takeBinarySegment(version) // Binary
|
||||
case 0x00: [] // End of data
|
||||
default: throw AVMetadataBinaryValueError.unhandledQRSegmentMode(mode)
|
||||
}
|
||||
for _ in 0..<charactersCount {
|
||||
output.append(input.takeUInt8())
|
||||
}
|
||||
return output
|
||||
case 0x00: // End of data
|
||||
return []
|
||||
default:
|
||||
return []
|
||||
} catch let error as BitError {
|
||||
throw .bitError(error)
|
||||
} catch let error as AVMetadataBinaryValueError {
|
||||
throw error
|
||||
} catch {
|
||||
throw .unknown(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct HalfByte {
|
||||
enum BitError: Error {
|
||||
case moreThan8BitsTaken
|
||||
case moreThan16BitsTaken
|
||||
case unhandledAlphanumericCharacter(UInt8)
|
||||
}
|
||||
|
||||
private struct Bit {
|
||||
let value: UInt8
|
||||
}
|
||||
|
||||
private extension [HalfByte] {
|
||||
mutating func takeUInt8() -> UInt8 {
|
||||
let left = remove(at: 0)
|
||||
let right = remove(at: 0)
|
||||
return UInt8(left, right)
|
||||
private extension [Bit] {
|
||||
mutating func takeBits(_ count: Int) throws(BitError) -> UInt8 {
|
||||
guard count <= 8 else { throw .moreThan8BitsTaken }
|
||||
|
||||
var value: UInt8 = 0
|
||||
for _ in 0..<count {
|
||||
value = (value << 1) | remove(at: 0).value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
mutating func takeUInt16() -> UInt16 {
|
||||
let first = remove(at: 0)
|
||||
let second = remove(at: 0)
|
||||
let third = remove(at: 0)
|
||||
let fourth = remove(at: 0)
|
||||
return UInt16(first, second, third, fourth)
|
||||
mutating func takeBits16(_ count: Int) throws(BitError) -> UInt16 {
|
||||
guard count <= 16 else { throw .moreThan16BitsTaken }
|
||||
|
||||
var value: UInt16 = 0
|
||||
for _ in 0..<count {
|
||||
value = (value << 1) | UInt16(remove(at: 0).value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
mutating func takeUInt8() throws(BitError) -> UInt8 {
|
||||
try takeBits(8)
|
||||
}
|
||||
|
||||
mutating func takeUInt16() throws(BitError) -> UInt16 {
|
||||
try takeBits16(16)
|
||||
}
|
||||
|
||||
mutating func takeBinarySegment(_ version: Int) throws(BitError) -> [UInt8] {
|
||||
let characterCountLength = version > 9 ? 16 : 8
|
||||
let charactersCount = try takeBits16(characterCountLength)
|
||||
|
||||
var output = [UInt8]()
|
||||
for _ in 0..<charactersCount {
|
||||
try output.append(takeUInt8())
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
mutating func takeAlphanumericSegment(_ version: Int) throws(BitError) -> [UInt8] {
|
||||
let characterCountLength = version > 9 ? (version > 26 ? 13 : 11) : 9
|
||||
let charactersCount = try takeBits16(characterCountLength)
|
||||
|
||||
var output = [UInt8]()
|
||||
var charactersRemaining = charactersCount
|
||||
while charactersRemaining > 1 {
|
||||
if count < 11 {
|
||||
// done
|
||||
return output
|
||||
}
|
||||
// read the 11 bits
|
||||
let nextTwoCharacters = try takeBits16(11)
|
||||
// split into the two characters
|
||||
try output.append(Self.alphaToByte(UInt8(nextTwoCharacters / 45)))
|
||||
try output.append(Self.alphaToByte(UInt8(nextTwoCharacters % 45)))
|
||||
charactersRemaining -= 2
|
||||
}
|
||||
|
||||
if charactersRemaining == 1 {
|
||||
if count < 6 {
|
||||
// done
|
||||
return output
|
||||
}
|
||||
let nextCharacter = try takeBits(6)
|
||||
try output.append(Self.alphaToByte(nextCharacter))
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
private static func alphaToByte(_ input: UInt8) throws(BitError) -> UInt8 {
|
||||
let value: UInt8? = switch input {
|
||||
case 0...9: input + 0x30 // 0-9
|
||||
case 10...35: input - 10 + 0x41 // A-Z
|
||||
case 36: " ".utf8.first
|
||||
case 37: "$".utf8.first
|
||||
case 38: "%".utf8.first
|
||||
case 39: "*".utf8.first
|
||||
case 40: "+".utf8.first
|
||||
case 41: "-".utf8.first
|
||||
case 42: ".".utf8.first
|
||||
case 43: "/".utf8.first
|
||||
case 44: ":".utf8.first
|
||||
default: nil
|
||||
}
|
||||
|
||||
guard let value else { throw .unhandledAlphanumericCharacter(input) }
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
func halfBytes() -> [HalfByte] {
|
||||
var result = [HalfByte]()
|
||||
func bits() -> [Bit] {
|
||||
var result = [Bit]()
|
||||
forEach { (byte: UInt8) in
|
||||
result.append(contentsOf: byte.halfBytes())
|
||||
result.append(contentsOf: byte.bits())
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private extension UInt8 {
|
||||
func halfBytes() -> [HalfByte] {
|
||||
[HalfByte(value: self >> 4), HalfByte(value: self & 0x0F)]
|
||||
}
|
||||
|
||||
init(_ left: HalfByte, _ right: HalfByte) {
|
||||
self.init((left.value << 4) + (right.value & 0x0F))
|
||||
}
|
||||
}
|
||||
|
||||
private extension UInt16 {
|
||||
init(_ first: HalfByte, _ second: HalfByte, _ third: HalfByte, _ fourth: HalfByte) {
|
||||
let first = UInt16(first.value) << 12
|
||||
let second = UInt16(second.value) << 8
|
||||
let third = UInt16(third.value) << 4
|
||||
let fourth = UInt16(fourth.value) & 0x0F
|
||||
let result = first + second + third + fourth
|
||||
self.init(result)
|
||||
func bits() -> [Bit] {
|
||||
var bits: [Bit] = []
|
||||
for i in 0..<8 {
|
||||
let bitValue = (self >> (7 - i)) & 1
|
||||
bits.append(Bit(value: bitValue))
|
||||
}
|
||||
return bits
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user