Skip to content

Commit 4cb23ca

Browse files
committed
feat(sync): add SyncServer module with PairingCode and golden vectors
Introduce the SyncServer SPM module (phase 22-1). PairingCode implements the six-digit verification code and confirm proof from sync-protocol.md section 4, validated against the golden vectors copied byte-identical from the Android repo so both implementations provably agree. Wire the module into project.yml, add a test-sync-server Makefile target and a run-tests.sh stage.
1 parent e82fccc commit 4cb23ca

9 files changed

Lines changed: 376 additions & 37 deletions

File tree

Bocan.xcodeproj/project.pbxproj

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
24FEF0F7E262A623A7D0B435 /* PerShowSettingsConventionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerShowSettingsConventionTests.swift; sourceTree = "<group>"; };
183183
2A2583AD98028864A547B9DB /* PlaylistRowDropHighlightTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistRowDropHighlightTests.swift; sourceTree = "<group>"; };
184184
2BA58EBDD5E2E1B686608222 /* AppPodcastSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPodcastSearch.swift; sourceTree = "<group>"; };
185-
2DB88DBD9C7383F071E14E0A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
185+
2DB88DBD9C7383F071E14E0A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
186186
2FC6007E8F4CBAB6F0EB637F /* UserAgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentTests.swift; sourceTree = "<group>"; };
187187
30F7FC1DB9D43070D64142C0 /* PlaylistSidebarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistSidebarViewModelTests.swift; sourceTree = "<group>"; };
188188
335B0C06DAF35F96240EF261 /* QueueReorderGripTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueReorderGripTests.swift; sourceTree = "<group>"; };
@@ -254,8 +254,9 @@
254254
A3964066EC248C5C8FA25D56 /* LibraryDestinationPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryDestinationPersistenceTests.swift; sourceTree = "<group>"; };
255255
A3ABBFFC5CC427A5F3C2AE6B /* LibraryViewModelClearQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModelClearQueueTests.swift; sourceTree = "<group>"; };
256256
A48BC953E0F409019C6F855F /* NoticesWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticesWindowView.swift; sourceTree = "<group>"; };
257+
A53BD4E14EFFC852676A7BC3 /* SyncServer */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SyncServer; path = Modules/SyncServer; sourceTree = SOURCE_ROOT; };
257258
A7C07D1BD23764E524C5E631 /* libdav1d.7.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libdav1d.7.dylib; sourceTree = "<group>"; };
258-
A9999948FAD8FF8EE311BC9A /* fpcalc */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = fpcalc; sourceTree = "<group>"; };
259+
A9999948FAD8FF8EE311BC9A /* fpcalc */ = {isa = PBXFileReference; path = fpcalc; sourceTree = "<group>"; };
259260
B4F9A42729E5EFAA6A91F042 /* BocanApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BocanApp.swift; sourceTree = "<group>"; };
260261
B5F151905EDE76E79038D4A9 /* MenuAnimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuAnimationTests.swift; sourceTree = "<group>"; };
261262
B6A9FE99C1399ECBBA9D4681 /* Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lifecycle.swift; sourceTree = "<group>"; };
@@ -304,7 +305,7 @@
304305
F92A54888932177D777FBB3C /* LogConsoleViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConsoleViewModelTests.swift; sourceTree = "<group>"; };
305306
F9FA03B30FC5E8E7FB6D2DE8 /* LogStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStoreTests.swift; sourceTree = "<group>"; };
306307
FD18A2E69E819F5845E511F3 /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateController.swift; sourceTree = "<group>"; };
307-
"TEMP_21781820-FA70-49CD-878D-6B69F5AD23EC" /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = "<group>"; };
308+
"TEMP_482CBF3E-08F6-4CAE-BD04-A98CE2AC0275" /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = "<group>"; };
308309
/* End PBXFileReference section */
309310

310311
/* Begin PBXFrameworksBuildPhase section */
@@ -387,7 +388,6 @@
387388
A9C0239AEA134BDEE9F90258 /* UITests */,
388389
8DE7AE70CABE9E93BDBE5955 /* NOTICES.md */,
389390
13C2660B7453561F1366516C /* Products */,
390-
83A4A8242FFB204600A8C5E3 /* Recovered References */,
391391
);
392392
sourceTree = "<group>";
393393
};
@@ -487,14 +487,6 @@
487487
path = ViewModelTests;
488488
sourceTree = "<group>";
489489
};
490-
83A4A8242FFB204600A8C5E3 /* Recovered References */ = {
491-
isa = PBXGroup;
492-
children = (
493-
"TEMP_21781820-FA70-49CD-878D-6B69F5AD23EC" /* Secrets.xcconfig */,
494-
);
495-
name = "Recovered References";
496-
sourceTree = "<group>";
497-
};
498490
8A6EBFBDE9871CAA33430EC6 /* Observability */ = {
499491
isa = PBXGroup;
500492
children = (
@@ -515,6 +507,7 @@
515507
53E595DDA2F51C36CA1A31AE /* Podcasts */,
516508
EA774AB72CC95F420281D172 /* Scrobble */,
517509
531C8B2E4746B8942918520D /* Subsonic */,
510+
A53BD4E14EFFC852676A7BC3 /* SyncServer */,
518511
D9B5B3110CE7BC30E8122BE8 /* UI */,
519512
);
520513
name = Packages;
@@ -637,6 +630,14 @@
637630
path = AppTests;
638631
sourceTree = "<group>";
639632
};
633+
"TEMP_B868991A-90FF-4BCD-BD59-57B21DA540A0" /* bocan-music */ = {
634+
isa = PBXGroup;
635+
children = (
636+
"TEMP_482CBF3E-08F6-4CAE-BD04-A98CE2AC0275" /* Secrets.xcconfig */,
637+
);
638+
path = "bocan-music";
639+
sourceTree = "<group>";
640+
};
640641
/* End PBXGroup section */
641642

642643
/* Begin PBXNativeTarget section */
@@ -745,6 +746,7 @@
745746
E9A576D858110977195D7E3F /* XCLocalSwiftPackageReference "Modules/Podcasts" */,
746747
3B5F2EF875A6F6FFF55F7ACB /* XCLocalSwiftPackageReference "Modules/Scrobble" */,
747748
83DC56AE955B2425894EBA99 /* XCLocalSwiftPackageReference "Modules/Subsonic" */,
749+
8B93AF4010DB834422D8458F /* XCLocalSwiftPackageReference "Modules/SyncServer" */,
748750
FE6D5E7CC473CB3FE993F62B /* XCLocalSwiftPackageReference "Modules/UI" */,
749751
);
750752
preferredProjectObjectVersion = 77;
@@ -984,7 +986,7 @@
984986
};
985987
3E79DA1DC07FE3812FB54C0A /* Debug */ = {
986988
isa = XCBuildConfiguration;
987-
baseConfigurationReference = "TEMP_21781820-FA70-49CD-878D-6B69F5AD23EC" /* Secrets.xcconfig */;
989+
baseConfigurationReference = "TEMP_482CBF3E-08F6-4CAE-BD04-A98CE2AC0275" /* Secrets.xcconfig */;
988990
buildSettings = {
989991
ALWAYS_SEARCH_USER_PATHS = NO;
990992
ARCHS = arm64;
@@ -1040,17 +1042,9 @@
10401042
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
10411043
GCC_WARN_UNUSED_FUNCTION = YES;
10421044
GCC_WARN_UNUSED_VARIABLE = YES;
1043-
HEADER_SEARCH_PATHS = (
1044-
/opt/homebrew/include,
1045-
/opt/homebrew/opt/ffmpeg/include,
1046-
/opt/homebrew/opt/taglib/include/taglib,
1047-
"$(inherited)",
1048-
);
1045+
HEADER_SEARCH_PATHS = "/opt/homebrew/include /opt/homebrew/opt/ffmpeg/include /opt/homebrew/opt/taglib/include/taglib $(inherited)";
10491046
INFOPLIST_FILE = Resources/Info.plist;
1050-
LIBRARY_SEARCH_PATHS = (
1051-
/opt/homebrew/lib,
1052-
"$(inherited)",
1053-
);
1047+
LIBRARY_SEARCH_PATHS = "/opt/homebrew/lib $(inherited)";
10541048
MACOSX_DEPLOYMENT_TARGET = 15.0;
10551049
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
10561050
MTL_FAST_MATH = YES;
@@ -1120,7 +1114,6 @@
11201114
"$(inherited)",
11211115
"@executable_path/../Frameworks",
11221116
);
1123-
MACOSX_DEPLOYMENT_TARGET = 15.0;
11241117
PRODUCT_MODULE_NAME = Bocan;
11251118
PRODUCT_NAME = Bocan;
11261119
SDKROOT = macosx;
@@ -1129,7 +1122,7 @@
11291122
};
11301123
D3B7E2D650BF9AC5F77082BA /* Release */ = {
11311124
isa = XCBuildConfiguration;
1132-
baseConfigurationReference = "TEMP_21781820-FA70-49CD-878D-6B69F5AD23EC" /* Secrets.xcconfig */;
1125+
baseConfigurationReference = "TEMP_482CBF3E-08F6-4CAE-BD04-A98CE2AC0275" /* Secrets.xcconfig */;
11331126
buildSettings = {
11341127
ALWAYS_SEARCH_USER_PATHS = NO;
11351128
ARCHS = arm64;
@@ -1180,17 +1173,9 @@
11801173
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
11811174
GCC_WARN_UNUSED_FUNCTION = YES;
11821175
GCC_WARN_UNUSED_VARIABLE = YES;
1183-
HEADER_SEARCH_PATHS = (
1184-
/opt/homebrew/include,
1185-
/opt/homebrew/opt/ffmpeg/include,
1186-
/opt/homebrew/opt/taglib/include/taglib,
1187-
"$(inherited)",
1188-
);
1176+
HEADER_SEARCH_PATHS = "/opt/homebrew/include /opt/homebrew/opt/ffmpeg/include /opt/homebrew/opt/taglib/include/taglib $(inherited)";
11891177
INFOPLIST_FILE = Resources/Info.plist;
1190-
LIBRARY_SEARCH_PATHS = (
1191-
/opt/homebrew/lib,
1192-
"$(inherited)",
1193-
);
1178+
LIBRARY_SEARCH_PATHS = "/opt/homebrew/lib $(inherited)";
11941179
MACOSX_DEPLOYMENT_TARGET = 15.0;
11951180
MTL_ENABLE_DEBUG_INFO = NO;
11961181
MTL_FAST_MATH = YES;
@@ -1222,7 +1207,6 @@
12221207
"$(inherited)",
12231208
"@executable_path/../Frameworks",
12241209
);
1225-
MACOSX_DEPLOYMENT_TARGET = 15.0;
12261210
PRODUCT_MODULE_NAME = Bocan;
12271211
PRODUCT_NAME = Bocan;
12281212
SDKROOT = macosx;
@@ -1295,6 +1279,10 @@
12951279
isa = XCLocalSwiftPackageReference;
12961280
relativePath = Modules/Acoustics;
12971281
};
1282+
8B93AF4010DB834422D8458F /* XCLocalSwiftPackageReference "Modules/SyncServer" */ = {
1283+
isa = XCLocalSwiftPackageReference;
1284+
relativePath = Modules/SyncServer;
1285+
};
12981286
A17D0542DA503F381C619312 /* XCLocalSwiftPackageReference "Modules/AudioEngine" */ = {
12991287
isa = XCLocalSwiftPackageReference;
13001288
relativePath = Modules/AudioEngine;

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help bootstrap bundle-fpcalc embed-deps brew-bundle doctor open generate build tests test test-coverage coverage-all test-audio-engine test-persistence test-metadata test-library test-acoustics test-ui test-playback test-scrobble test-subsonic test-podcasts test-observability uitest lint format pseudolocale format-check install-hooks clean downloads
1+
.PHONY: help bootstrap bundle-fpcalc embed-deps brew-bundle doctor open generate build tests test test-coverage coverage-all test-audio-engine test-persistence test-metadata test-library test-acoustics test-ui test-playback test-scrobble test-subsonic test-podcasts test-sync-server test-observability uitest lint format pseudolocale format-check install-hooks clean downloads
22

33
# Pinned SwiftLint version. CI installs this exact release; `doctor` fails when
44
# the local install differs. SwiftLint's force_unwrapping/superfluous_disable
@@ -205,6 +205,13 @@ test-podcasts:
205205
@echo "=============================="
206206
cd Modules/Podcasts && swift test --enable-code-coverage
207207

208+
## test-sync-server: Run SyncServer SPM package tests
209+
test-sync-server:
210+
@echo "=============================="
211+
@echo "= Executing SyncServer Test"
212+
@echo "=============================="
213+
cd Modules/SyncServer && swift test --enable-code-coverage
214+
208215
## test-observability: Run Observability SPM package tests
209216
test-observability:
210217
@echo "=============================="

Modules/SyncServer/Package.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version: 6.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "SyncServer",
7+
platforms: [
8+
.macOS(.v15),
9+
],
10+
products: [
11+
.library(name: "SyncServer", targets: ["SyncServer"]),
12+
],
13+
dependencies: [
14+
.package(path: "../Observability"),
15+
],
16+
targets: [
17+
.target(
18+
name: "SyncServer",
19+
dependencies: [
20+
.product(name: "Observability", package: "Observability"),
21+
],
22+
swiftSettings: [
23+
.enableExperimentalFeature("StrictConcurrency"),
24+
]
25+
),
26+
.testTarget(
27+
name: "SyncServerTests",
28+
dependencies: [
29+
"SyncServer",
30+
],
31+
resources: [.copy("Fixtures")],
32+
swiftSettings: [
33+
.enableExperimentalFeature("StrictConcurrency"),
34+
]
35+
),
36+
]
37+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import CryptoKit
2+
import Foundation
3+
4+
/// Derivation of the six-digit pairing verification code and the confirm proof,
5+
/// exactly per `sync-protocol.md` section 4. The math is shared byte-for-byte
6+
/// with the Android client; the golden vectors in `PairingCodeTests` (copied
7+
/// byte-identical from the Android repo) are the contract that keeps the two
8+
/// implementations in agreement.
9+
///
10+
/// The code is a *verification* code, not a secret: it is derived from both
11+
/// certificate fingerprints and both nonces, so a man-in-the-middle that
12+
/// terminates TLS on one side holds a different certificate pair, computes a
13+
/// different code, and the code the user copies from the Mac fails on the phone.
14+
/// Matching is the proof; there is no secret entropy to brute force.
15+
public enum PairingCode {
16+
/// ASCII literal mixed into the code derivation (protocol section 4).
17+
private static let label = "bocan-pair-v1"
18+
19+
/// The six-digit verification code, zero-padded (for example `"704898"`).
20+
///
21+
/// - Parameters:
22+
/// - fpMac: lowercase hex SHA-256 of the Mac certificate DER (64 chars).
23+
/// - fpPhone: lowercase hex SHA-256 of the phone certificate DER (64 chars).
24+
/// - noncePhone: the phone's 32 random bytes.
25+
/// - nonceMac: the Mac's 32 random bytes.
26+
/// - Returns: the six-digit code as a zero-padded string.
27+
public static func code(
28+
fpMac: String,
29+
fpPhone: String,
30+
noncePhone: Data,
31+
nonceMac: Data
32+
) -> String {
33+
// Order independent: both sides sort the fingerprints lexicographically,
34+
// so the ceremony yields the same code regardless of which peer is the
35+
// "Mac". For equal-length lowercase hex strings this ordering matches
36+
// byte order.
37+
let fpLo = min(fpMac, fpPhone)
38+
let fpHi = max(fpMac, fpPhone)
39+
40+
// HMAC key = noncePhone || nonceMac (phone nonce first), raw bytes.
41+
var key = Data()
42+
key.append(noncePhone)
43+
key.append(nonceMac)
44+
45+
// msg = "bocan-pair-v1" || fpLo || fpHi, ASCII bytes, no separators.
46+
var message = Data(Self.label.utf8)
47+
message.append(Data(fpLo.utf8))
48+
message.append(Data(fpHi.utf8))
49+
50+
let mac = HMAC<SHA256>.authenticationCode(
51+
for: message,
52+
using: SymmetricKey(data: key)
53+
)
54+
55+
// First 8 bytes as an unsigned big-endian UInt64, then mod 1_000_000.
56+
var value: UInt64 = 0
57+
for byte in Data(mac).prefix(8) {
58+
value = (value << 8) | UInt64(byte)
59+
}
60+
return String(format: "%06d", value % 1_000_000)
61+
}
62+
63+
/// The base64 confirm proof for a session id (protocol section 4 step 5):
64+
/// `HMAC-SHA256(key = code as ASCII bytes, msg = sessionId as ASCII bytes)`.
65+
///
66+
/// - Parameters:
67+
/// - code: the six-digit code string (the zero-padded output of `code(...)`).
68+
/// - sessionId: the pairing session id (a UUID string).
69+
/// - Returns: the standard base64 encoding (with padding) of the HMAC output.
70+
public static func proof(code: String, sessionId: String) -> String {
71+
let mac = HMAC<SHA256>.authenticationCode(
72+
for: Data(sessionId.utf8),
73+
using: SymmetricKey(data: Data(code.utf8))
74+
)
75+
return Data(mac).base64EncodedString()
76+
}
77+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
/// The single error type for the `SyncServer` module. Cases carry context (never
4+
/// bare) and grow as later phase-22 slices add identity, listener, pairing,
5+
/// manifest, and file-serving failure modes.
6+
public enum SyncServerError: Error, Sendable {
7+
/// A pairing-ceremony step failed. `reason` is a short, non-sensitive token
8+
/// describing which invariant was violated (never a code, nonce, or proof).
9+
case pairing(reason: String)
10+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"comment": "Golden vectors for sync-protocol.md section 4. Shared byte-for-byte with the Mac repo. Regenerate with scripts/gen-pairing-vectors.py; never hand-edit.",
3+
"label": "bocan-pair-v1",
4+
"vectors": [
5+
{
6+
"fpMac": "76e0aa4b6ed22c0a297e46c8173d0c41f8d203b74a3ec56043940746364f6515",
7+
"fpPhone": "996e247ab6ce772a2db8a6683eabca806c386d6f2e51b6281ddd13994e8cda08",
8+
"noncePhoneBase64": "wMyfZARXwcnF+ME5NbngatVT6txszUqDSyykjdekS0Y=",
9+
"nonceMacBase64": "UaEZrwkiDitz+4MpyCCa5sZskiw0rvGbl0/fLYUN1Zk=",
10+
"expectedCode": "704898",
11+
"sessionId": "11111111-1111-4111-8111-111111111111",
12+
"expectedProofBase64": "sawRxerSZnFSbfVgfsboE+l4FdMdczDPEww7zVeH4hs="
13+
},
14+
{
15+
"fpMac": "df42e15156038cefa92b4f1ca718659a8f6b207f5b8e3450e9567d98fbd05924",
16+
"fpPhone": "6292c5d71996f9de565229a68593bb5cfc4a1a6671daf59b35586c1618f04709",
17+
"noncePhoneBase64": "PNn4btg5q3rzg39x5M/UWkg89Gc2gCZoawZPEfCcAB8=",
18+
"nonceMacBase64": "aFJa8rOo+Gs20F99fr2qaahidcPXO0OilG3q31CzwuM=",
19+
"expectedCode": "660831",
20+
"sessionId": "22222222-2222-4222-8222-222222222222",
21+
"expectedProofBase64": "rogiSWdP5sMHuhr1oH+aRINmdS++pxqfi+UU9Nvtql4="
22+
},
23+
{
24+
"fpMac": "2785c0bc3bfd8fc2602d1b1603d0db4deaecbc723024ff2d4f824c55c68a39b5",
25+
"fpPhone": "b92e5402f4d74402d510fe1c5eaf91a0fb54abab42863dd82da7af381c91b24d",
26+
"noncePhoneBase64": "jcLJvotV8hjOwsWvIxOuEBHjc2VHLpJNM7zcIP53T9g=",
27+
"nonceMacBase64": "eMNrRdGydZSchHN3aMpTw6Wa2LUW3di2Y4hunbEPzWE=",
28+
"expectedCode": "995406",
29+
"sessionId": "33333333-3333-4333-8333-333333333333",
30+
"expectedProofBase64": "ou1dkAcS9aYpnA1LK8m1QMfYLAlPvvekJ9uqE82CFzs="
31+
},
32+
{
33+
"fpMac": "e394fc454f571424c30d855534728908a7cad739abeb493e5a8723b9decfa5cc",
34+
"fpPhone": "5abb88c7d6a6aa980d19c6e9d7744b501128021711ca4e5290975bd53f2d3644",
35+
"noncePhoneBase64": "DMFTtpSCAKR5qBOs3ijfMIy5B2GbkyF1CT96bp21fmI=",
36+
"nonceMacBase64": "/29mC48FIRml95p1ItvUSa+VX9bHXW2fhzHvaHoTdXk=",
37+
"expectedCode": "360898",
38+
"sessionId": "44444444-4444-4444-8444-444444444444",
39+
"expectedProofBase64": "769MMv4lPRbLy/FMjErZbmpaatc6mfeyHO9wtBqUftw="
40+
},
41+
{
42+
"fpMac": "70623bce0ac5d25ae7b5e1b857b4b20791614f7d84486a882ae36213bceb4888",
43+
"fpPhone": "eeee7d877b96a1efbda9a89a5a0edaf3170cc7af5bf94e57fd774d780104854a",
44+
"noncePhoneBase64": "7JjyyBqa5nnGOUUkJ0DK6GOq3lHlKzq/M0goPyMTsFk=",
45+
"nonceMacBase64": "vdD/fPZ/VwjJQk0EYxU/d8DKjXj9nDYqBQLcn7knLRE=",
46+
"expectedCode": "904517",
47+
"sessionId": "55555555-5555-4555-8555-555555555555",
48+
"expectedProofBase64": "1Bp7ckVE3W2WhWuzk7aDjP/HD3RSfKn2Yr+m4ArXGXw="
49+
}
50+
]
51+
}

0 commit comments

Comments
 (0)