| … | … | @@ -1116,16 +1116,32 @@ async function createInvite(identity) { |
| 1116 | 1116 | throw new Error('Storage not initialized') |
| 1117 | 1117 | } |
| 1118 | 1118 | |
| 1119 | | // Create invite with autobase bootstrap key |
| 1120 | | const invite = { |
| 1119 | const now = Date.now() |
| 1120 | const INVITE_EXPIRATION = 15 * 60 * 1000 // 15 minutes |
| 1121 | |
| 1122 | // Create invite data (without signature first) |
| 1123 | const inviteData = { |
| 1121 | 1124 | discoveryKey: b4a.toString(crypto.discoveryKey(identityKeyPair.publicKey), 'hex'), |
| 1122 | 1125 | identityKey: b4a.toString(identityKeyPair.publicKey, 'hex'), |
| 1123 | 1126 | autobaseKey: b4a.toString(autobase.key, 'hex'), |
| 1124 | | timestamp: Date.now() |
| 1127 | timestamp: now, |
| 1128 | expiresAt: now + INVITE_EXPIRATION |
| 1129 | } |
| 1130 | |
| 1131 | // Sign the invite data with identity secret key |
| 1132 | const message = b4a.from(JSON.stringify(inviteData)) |
| 1133 | const signature = crypto.sign(message, identityKeyPair.secretKey) |
| 1134 | |
| 1135 | // Add signature to invite |
| 1136 | const signedInvite = { |
| 1137 | ...inviteData, |
| 1138 | signature: b4a.toString(signature, 'hex') |
| 1125 | 1139 | } |
| 1126 | 1140 | |
| 1127 | 1141 | // Encode as base64 |
| 1128 | | const inviteStr = b4a.toString(b4a.from(JSON.stringify(invite)), 'base64') |
| 1142 | const inviteStr = b4a.toString(b4a.from(JSON.stringify(signedInvite)), 'base64') |
| 1143 | |
| 1144 | console.log(`✅ Created signed invite (expires in 15 minutes)`) |
| 1129 | 1145 | return inviteStr |
| 1130 | 1146 | } |
| 1131 | 1147 | |
| … | … | @@ -1141,8 +1157,39 @@ async function createInvite(identity) { |
| 1141 | 1157 | */ |
| 1142 | 1158 | async function joinWallet(inviteCode, deviceName) { |
| 1143 | 1159 | // Decode invite |
| 1144 | | const inviteJson = b4a.toString(b4a.from(inviteCode, 'base64')) |
| 1145 | | const invite = JSON.parse(inviteJson) |
| 1160 | let invite |
| 1161 | try { |
| 1162 | const inviteJson = b4a.toString(b4a.from(inviteCode, 'base64')) |
| 1163 | invite = JSON.parse(inviteJson) |
| 1164 | } catch (err) { |
| 1165 | throw new Error('Invalid invite code format') |
| 1166 | } |
| 1167 | |
| 1168 | // Verify invite has required fields |
| 1169 | if (!invite.discoveryKey || !invite.identityKey || !invite.autobaseKey || |
| 1170 | !invite.timestamp || !invite.expiresAt || !invite.signature) { |
| 1171 | throw new Error('Invalid invite: missing required fields') |
| 1172 | } |
| 1173 | |
| 1174 | // Check expiration (15 minutes) |
| 1175 | const now = Date.now() |
| 1176 | if (now > invite.expiresAt) { |
| 1177 | const expirationAge = Math.floor((now - invite.expiresAt) / 1000 / 60) // minutes ago |
| 1178 | throw new Error(`Invite code expired ${expirationAge} minutes ago. Request a new invite.`) |
| 1179 | } |
| 1180 | |
| 1181 | // Verify signature |
| 1182 | const { signature, ...inviteData } = invite |
| 1183 | const message = b4a.from(JSON.stringify(inviteData)) |
| 1184 | const sig = b4a.from(signature, 'hex') |
| 1185 | const pubKey = b4a.from(invite.identityKey, 'hex') |
| 1186 | |
| 1187 | if (!crypto.verify(message, sig, pubKey)) { |
| 1188 | throw new Error('Invalid invite signature - invite may be forged or corrupted') |
| 1189 | } |
| 1190 | |
| 1191 | console.log('✅ Invite signature verified') |
| 1192 | console.log(`⏰ Invite expires in ${Math.floor((invite.expiresAt - now) / 1000 / 60)} minutes`) |
| 1146 | 1193 | |
| 1147 | 1194 | // Initialize Corestore for temporary writer key |
| 1148 | 1195 | if (!store) { |