fix(app): accept incoming calls, restore from recovery phrase, duress session isolation, message loss
Calls: `call` was missing from the known content types, so every offer was acknowledged and dropped; the call screen column now fills the width (the avatar jumped left after hang-up); a ring started from a push ends after 12 s when no offer arrives; STUN fallback when no TURN credentials. Registration: X-Sepot-Identity-Proof lets a new device reclaim an existing identity; retry backs off from 30 s to 10 min and exposes a conflict flag. Prekeys: PrekeysLow triggers a batch of 50 one-time prekeys via POST /v1/keys; the signed prekey rotates weekly, the Kyber prekey monthly. Messaging: crypto operations run serially (concurrent encrypts derived the same message key); stop() waits for in-flight deliveries (auto-lock between decrypt and persist lost messages); disappearing-message cleanup no longer overwrites fresh messages; a push wake-up reconnects stale connections; the outbox carries polls and mentions; length limits on wire input. Storage: a duress session never exposes the real identity or phrase, does not start networking and cannot touch the keystore; deleting data in a duress session leaves the real account alone; PIN guessing is throttled (5 misses, then 30 s doubling up to 15 min); the duress blob shares the salt so unlock time does not reveal which PIN was entered; iOS keychain items are this-device only; biometrics are biometric-only; screen security defaults to on and is applied after unlock; tapjacking filter in MainActivity. Tests: content types, shared-salt unwrap, duress isolation.
This commit is contained in:
@@ -81,6 +81,32 @@ class SepotSealedMessage {
|
||||
}
|
||||
|
||||
/// Jeden one-time prekey k publikaci na relay.
|
||||
/// Nový signed prekey k nahrání na relay (rotace).
|
||||
class SepotSignedPreKey {
|
||||
SepotSignedPreKey({
|
||||
required this.id,
|
||||
required this.publicKey,
|
||||
required this.signature,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final Uint8List publicKey;
|
||||
final Uint8List signature;
|
||||
}
|
||||
|
||||
/// Nový last-resort Kyber prekey k nahrání na relay.
|
||||
class SepotKyberPreKey {
|
||||
SepotKyberPreKey({
|
||||
required this.id,
|
||||
required this.publicKey,
|
||||
required this.signature,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final Uint8List publicKey;
|
||||
final Uint8List signature;
|
||||
}
|
||||
|
||||
class SepotOneTimePreKey {
|
||||
SepotOneTimePreKey({required this.id, required this.publicKey});
|
||||
|
||||
@@ -420,4 +446,64 @@ class SepotCrypto {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Nový signed prekey (rotace). Starý zůstává ve store — relace založené
|
||||
/// z bundle těsně před rotací ho ještě potřebují.
|
||||
Future<SepotSignedPreKey> generateSignedPreKey({
|
||||
required int id,
|
||||
required int timestampMs,
|
||||
}) async {
|
||||
final identityPrivate = PrivateKey.deserialize(
|
||||
bytes: _identity.privateKey.toList(),
|
||||
);
|
||||
final private = PrivateKey.generate();
|
||||
final public = private.getPublicKey();
|
||||
final signature = identityPrivate.sign(
|
||||
message: public.serialize().toList(),
|
||||
);
|
||||
await _stores.signedPreKey.storeSignedPreKey(
|
||||
id,
|
||||
SignedPreKeyRecord(
|
||||
id: id,
|
||||
publicKey: public,
|
||||
privateKey: private,
|
||||
signature: signature.toList(),
|
||||
timestamp: BigInt.from(timestampMs),
|
||||
),
|
||||
);
|
||||
return SepotSignedPreKey(
|
||||
id: id,
|
||||
publicKey: Uint8List.fromList(public.serialize().toList()),
|
||||
signature: Uint8List.fromList(signature.toList()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Nový last-resort Kyber prekey (rotace PQXDH kotvy).
|
||||
Future<SepotKyberPreKey> generateKyberPreKey({
|
||||
required int id,
|
||||
required int timestampMs,
|
||||
}) async {
|
||||
final identityPrivate = PrivateKey.deserialize(
|
||||
bytes: _identity.privateKey.toList(),
|
||||
);
|
||||
final keyPair = KyberKeyPair.generate();
|
||||
final public = keyPair.getPublicKey();
|
||||
final signature = identityPrivate.sign(
|
||||
message: public.serialize().toList(),
|
||||
);
|
||||
await _stores.kyberPreKey.storeKyberPreKey(
|
||||
id,
|
||||
KyberPreKeyRecord.create(
|
||||
id: id,
|
||||
keyPair: keyPair,
|
||||
signature: signature.toList(),
|
||||
timestamp: BigInt.from(timestampMs),
|
||||
),
|
||||
);
|
||||
return SepotKyberPreKey(
|
||||
id: id,
|
||||
publicKey: Uint8List.fromList(public.serialize().toList()),
|
||||
signature: Uint8List.fromList(signature.toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user