mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 04:51:49 +00:00
fix commands, ui
This commit is contained in:
+53
-1
@@ -2022,7 +2022,8 @@ data class LocalProfile(
|
||||
override val localAlias: String,
|
||||
val contactLink: String? = null,
|
||||
val preferences: ChatPreferences? = null,
|
||||
val peerType: ChatPeerType? = null
|
||||
val peerType: ChatPeerType? = null,
|
||||
val localBadge: LocalBadge? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
|
||||
|
||||
@@ -2046,6 +2047,57 @@ enum class ChatPeerType {
|
||||
@SerialName("bot") Bot
|
||||
}
|
||||
|
||||
// Supporter badge. The credential/proof bytes stay core-side; the UI only sees the disclosed type + status.
|
||||
// Unknown types keep their string so a verified badge's real name can be shown, while the icon falls back to supporter.
|
||||
@Serializable(with = BadgeTypeSerializer::class)
|
||||
sealed class BadgeType {
|
||||
@Serializable @SerialName("supporter") object Supporter: BadgeType()
|
||||
@Serializable @SerialName("legend") object Legend: BadgeType()
|
||||
@Serializable @SerialName("investor") object Investor: BadgeType()
|
||||
@Serializable @SerialName("unknown") data class Unknown(val type: String): BadgeType()
|
||||
|
||||
// the disclosed (signed) type name, shown to the user for verified badges
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is Supporter -> "supporter"
|
||||
is Legend -> "legend"
|
||||
is Investor -> "investor"
|
||||
is Unknown -> type
|
||||
}
|
||||
}
|
||||
|
||||
object BadgeTypeSerializer : KSerializer<BadgeType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BadgeType", PrimitiveKind.STRING)
|
||||
override fun deserialize(decoder: Decoder): BadgeType =
|
||||
when (val v = decoder.decodeString()) {
|
||||
"supporter" -> BadgeType.Supporter
|
||||
"legend" -> BadgeType.Legend
|
||||
"investor" -> BadgeType.Investor
|
||||
else -> BadgeType.Unknown(v)
|
||||
}
|
||||
override fun serialize(encoder: Encoder, value: BadgeType) = encoder.encodeString(value.text)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class BadgeStatus {
|
||||
@SerialName("active") Active,
|
||||
@SerialName("expired") Expired,
|
||||
@SerialName("failed") Failed
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BadgeInfo(
|
||||
val badgeType: BadgeType,
|
||||
val badgeExpiry: Instant? = null,
|
||||
val badgeExtra: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LocalBadge(
|
||||
val badge: BadgeInfo,
|
||||
val status: BadgeStatus
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserProfileUpdateSummary(
|
||||
val updateSuccesses: Int,
|
||||
|
||||
+1
-1
@@ -708,7 +708,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight, tappableBadge = true)
|
||||
val displayName = contact.profile.displayName.trim()
|
||||
val text = buildAnnotatedString {
|
||||
if (contact.verified) {
|
||||
|
||||
+14
-10
@@ -733,7 +733,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight, tappableBadge = true)
|
||||
val displayName = member.displayName.trim() // alias if set
|
||||
val text = buildAnnotatedString {
|
||||
if (member.verified) {
|
||||
@@ -900,16 +900,20 @@ fun MemberProfileImage(
|
||||
mem: GroupMember,
|
||||
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||
backgroundColor: Color? = null,
|
||||
async: Boolean = false
|
||||
async: Boolean = false,
|
||||
tappableBadge: Boolean = false
|
||||
) {
|
||||
ProfileImage(
|
||||
size = size,
|
||||
image = mem.image,
|
||||
color = color,
|
||||
backgroundColor = backgroundColor,
|
||||
blurred = mem.blocked,
|
||||
async = async
|
||||
)
|
||||
val badge = mem.memberProfile.localBadge
|
||||
BadgedProfileImage(size, badge, onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null) {
|
||||
ProfileImage(
|
||||
size = size,
|
||||
image = mem.image,
|
||||
color = color,
|
||||
backgroundColor = backgroundColor,
|
||||
blurred = mem.blocked,
|
||||
async = async
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onFailure: () -> Unit = {}, onSuccess: () -> Unit = {}) {
|
||||
|
||||
+4
-4
@@ -464,10 +464,10 @@ fun UserProfileRow(u: User, enabled: Boolean = remember { chatModel.chatRunning
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp * fontSizeSqrtMultiplier
|
||||
)
|
||||
val avatarSize = 54.dp * fontSizeSqrtMultiplier
|
||||
BadgedProfileImage(avatarSize, u.profile.localBadge) {
|
||||
ProfileImage(image = u.image, size = avatarSize)
|
||||
}
|
||||
Text(
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
|
||||
+75
-2
@@ -3,6 +3,7 @@ package chat.simplex.common.views.helpers
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.Icon
|
||||
@@ -14,10 +15,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.unit.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeStatus
|
||||
import chat.simplex.common.model.BadgeType
|
||||
import chat.simplex.common.model.ChatInfo
|
||||
import chat.simplex.common.model.LocalBadge
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -25,7 +30,7 @@ import dev.icerock.moko.resources.ImageResource
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) {
|
||||
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false, tappableBadge: Boolean = false) {
|
||||
val icon =
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Group -> chatInfo.groupInfo.chatIconName
|
||||
@@ -33,7 +38,10 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
|
||||
is ChatInfo.Direct -> chatInfo.contact.chatIconName
|
||||
else -> MR.images.ic_account_circle_filled
|
||||
}
|
||||
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
|
||||
val badge = if (chatInfo is ChatInfo.Direct) chatInfo.contact.profile.localBadge else null
|
||||
BadgedProfileImage(size, badge, onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null) {
|
||||
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -103,6 +111,71 @@ fun ProfileImage(
|
||||
}
|
||||
}
|
||||
|
||||
// Overlays a supporter badge on an avatar with zero layout impact: a custom Layout measures the badge but
|
||||
// reports only the avatar's size, so the badge overflows bottom-right (not clamped) and nothing around it shifts.
|
||||
@Composable
|
||||
fun BadgedProfileImage(size: Dp, badge: LocalBadge?, onBadgeClick: (() -> Unit)? = null, avatar: @Composable () -> Unit) {
|
||||
if (badge == null) {
|
||||
avatar()
|
||||
return
|
||||
}
|
||||
Layout(content = {
|
||||
avatar()
|
||||
ProfileBadge(size, badge, onBadgeClick)
|
||||
}) { measurables, constraints ->
|
||||
val a = measurables[0].measure(constraints)
|
||||
val b = measurables[1].measure(Constraints())
|
||||
layout(a.width, a.height) {
|
||||
a.place(0, 0)
|
||||
// phone center sits 0.33*S right and down of the avatar center, overflowing the avatar bounds
|
||||
val off = (0.33f * a.width).toInt()
|
||||
b.place(x = a.width / 2 + off - b.width / 2, y = a.height / 2 + off - b.height / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the phone glyph (or warning triangle) scales inversely to the avatar so it stays readable when the avatar is small.
|
||||
@Composable
|
||||
private fun ProfileBadge(size: Dp, badge: LocalBadge, onBadgeClick: (() -> Unit)?) {
|
||||
val s = size.value
|
||||
val mult = 1f + 0.5f * ((192f - s) / 156f).coerceIn(0f, 1f)
|
||||
val phoneH = 0.28f * size * mult
|
||||
val phoneW = phoneH * 0.7617f
|
||||
val mod = Modifier.size(width = phoneW, height = phoneH).let { if (onBadgeClick != null) it.clickable(onClick = onBadgeClick) else it }
|
||||
if (badge.status == BadgeStatus.Failed) {
|
||||
Icon(painterResource(MR.images.ic_warning_filled), contentDescription = null, tint = WarningOrange, modifier = mod)
|
||||
} else {
|
||||
Image(
|
||||
painterResource(badgeImage(badge.badge.badgeType)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
alpha = if (badge.status == BadgeStatus.Expired) 0.4f else 1f,
|
||||
modifier = mod
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showBadgeInfoAlert(badge: LocalBadge) {
|
||||
if (badge.status == BadgeStatus.Failed) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badge_unverified_title),
|
||||
text = generalGetString(MR.strings.badge_unverified_desc)
|
||||
)
|
||||
} else {
|
||||
// a verified badge's type is signed and can't be faked, so the real (possibly unknown) type name is shown
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = badge.badge.badgeType.text.replaceFirstChar { it.uppercase() },
|
||||
text = generalGetString(MR.strings.badge_verified_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun badgeImage(t: BadgeType): ImageResource = when (t) {
|
||||
is BadgeType.Legend -> MR.images.badge_legend
|
||||
is BadgeType.Investor -> MR.images.badge_investor
|
||||
else -> MR.images.badge_supporter // Supporter + Unknown
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileImage(size: Dp, image: ImageResource) {
|
||||
Image(
|
||||
|
||||
@@ -3105,4 +3105,7 @@
|
||||
<string name="tray_tooltip_unread">SimpleX — %d unread</string>
|
||||
<string name="appearance_minimize_to_tray">Minimize to tray when closing window</string>
|
||||
<string name="appearance_minimize_to_tray_desc">Keep SimpleX running in the background to receive messages.</string>
|
||||
<string name="badge_verified_desc">This is a verified badge - its type is signed and cannot be faked.</string>
|
||||
<string name="badge_unverified_title">Unverified badge</string>
|
||||
<string name="badge_unverified_desc">This badge could not be verified and may not be genuine.</string>
|
||||
</resources>
|
||||
+42
-39
@@ -46,13 +46,10 @@ module Simplex.Chat.Badges
|
||||
rowToBadge,
|
||||
) where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Aeson.Types (Parser)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Text (Text)
|
||||
@@ -138,28 +135,27 @@ deriving instance Eq (Badge 'BCCredential)
|
||||
|
||||
deriving instance Eq (Badge 'BCProof)
|
||||
|
||||
-- Local badge: a stored badge (own credential or peer proof) plus its display status.
|
||||
-- Existential - the inner Badge constructor is the discriminator.
|
||||
data LocalBadge = forall b. LocalBadge (Badge b) BadgeStatus
|
||||
|
||||
sameBadgeCrypto :: Badge x -> Badge y -> Bool
|
||||
sameBadgeCrypto (BadgeCredential mk1 sg1 i1) (BadgeCredential mk2 sg2 i2) = mk1 == mk2 && sg1 == sg2 && i1 == i2
|
||||
sameBadgeCrypto (BadgeProof ph1 p1 i1) (BadgeProof ph2 p2 i2) = ph1 == ph2 && p1 == p2 && i1 == i2
|
||||
sameBadgeCrypto _ _ = False
|
||||
|
||||
instance Show LocalBadge where
|
||||
show (LocalBadge b st) = "LocalBadge (" <> show b <> ") " <> show st
|
||||
|
||||
instance Eq LocalBadge where
|
||||
LocalBadge b1 s1 == LocalBadge b2 s2 = s1 == s2 && sameBadgeCrypto b1 b2
|
||||
-- Local badge: a stored badge plus its display status.
|
||||
-- OwnBadge - the user's own credential (loaded from the DB).
|
||||
-- PeerBadge - a verified peer proof (from the DB, or received over the wire).
|
||||
-- ShownBadge - decoded from a crypto-free profile JSON for display only: no crypto, so it cannot be sent.
|
||||
data LocalBadge
|
||||
= OwnBadge (Badge 'BCCredential) BadgeStatus
|
||||
| PeerBadge (Badge 'BCProof) BadgeStatus
|
||||
| ShownBadge BadgeInfo BadgeStatus
|
||||
deriving (Eq, Show)
|
||||
|
||||
localBadgeInfo :: LocalBadge -> BadgeInfo
|
||||
localBadgeInfo (LocalBadge b _) = case b of
|
||||
BadgeCredential _ _ i -> i
|
||||
BadgeProof _ _ i -> i
|
||||
localBadgeInfo = \case
|
||||
OwnBadge (BadgeCredential _ _ i) _ -> i
|
||||
PeerBadge (BadgeProof _ _ i) _ -> i
|
||||
ShownBadge i _ -> i
|
||||
|
||||
localBadgeStatus :: LocalBadge -> BadgeStatus
|
||||
localBadgeStatus (LocalBadge _ st) = st
|
||||
localBadgeStatus = \case
|
||||
OwnBadge _ st -> st
|
||||
PeerBadge _ st -> st
|
||||
ShownBadge _ st -> st
|
||||
|
||||
localBadgeVerified :: Maybe LocalBadge -> Maybe Bool
|
||||
localBadgeVerified = fmap $ \lb -> localBadgeStatus lb /= BSFailed
|
||||
@@ -295,16 +291,18 @@ type BadgeRow = (Maybe ByteString, Maybe ByteString, Maybe UTCTime, Maybe Text,
|
||||
|
||||
-- receive/store sites have a wire proof + a computed verified flag
|
||||
badgeToRow :: Maybe (Badge 'BCProof) -> Bool -> BadgeRow
|
||||
badgeToRow badge verified = localBadgeToRow $ (\b -> LocalBadge b (if verified then BSActive else BSFailed)) <$> badge
|
||||
badgeToRow badge verified = localBadgeToRow $ (\b -> PeerBadge b (if verified then BSActive else BSFailed)) <$> badge
|
||||
|
||||
localBadgeToRow :: Maybe LocalBadge -> BadgeRow
|
||||
localBadgeToRow (Just (LocalBadge b st)) = case b of
|
||||
BadgeCredential (BadgeMasterKey mk) (BBSSignature sg) BadgeInfo {badgeType, badgeExpiry, badgeExtra} ->
|
||||
(Nothing, Nothing, badgeExpiry, Just (textEncode badgeType), Just (BI verified), Just badgeExtra, Just mk, Just sg)
|
||||
BadgeProof (BBSPresHeader ph) (BBSProof p) BadgeInfo {badgeType, badgeExpiry, badgeExtra} ->
|
||||
(Just p, Just ph, badgeExpiry, Just (textEncode badgeType), Just (BI verified), Just badgeExtra, Nothing, Nothing)
|
||||
localBadgeToRow (Just lb) = case lb of
|
||||
OwnBadge (BadgeCredential (BadgeMasterKey mk) (BBSSignature sg) BadgeInfo {badgeType, badgeExpiry, badgeExtra}) st ->
|
||||
(Nothing, Nothing, badgeExpiry, Just (textEncode badgeType), Just (BI (active st)), Just badgeExtra, Just mk, Just sg)
|
||||
PeerBadge (BadgeProof (BBSPresHeader ph) (BBSProof p) BadgeInfo {badgeType, badgeExpiry, badgeExtra}) st ->
|
||||
(Just p, Just ph, badgeExpiry, Just (textEncode badgeType), Just (BI (active st)), Just badgeExtra, Nothing, Nothing)
|
||||
ShownBadge BadgeInfo {badgeType, badgeExpiry, badgeExtra} st ->
|
||||
(Nothing, Nothing, badgeExpiry, Just (textEncode badgeType), Just (BI (active st)), Just badgeExtra, Nothing, Nothing)
|
||||
where
|
||||
verified = st /= BSFailed
|
||||
active st = st /= BSFailed
|
||||
localBadgeToRow Nothing = (Nothing, Nothing, Nothing, Nothing, Just (BI False), Nothing, Nothing, Nothing)
|
||||
|
||||
rowToBadge :: UTCTime -> BadgeRow -> Maybe LocalBadge
|
||||
@@ -315,9 +313,9 @@ rowToBadge now (p_, ph_, badgeExpiry, type_, verified_, extra_, mk_, sg_) = do
|
||||
verified = maybe False unBI verified_
|
||||
st = mkBadgeStatus now verified info
|
||||
case (mk_, sg_, p_, ph_) of
|
||||
(Just mk, Just sg, _, _) -> Just $ LocalBadge (BadgeCredential (BadgeMasterKey mk) (BBSSignature sg) info) st
|
||||
(_, _, Just p, Just ph) -> Just $ LocalBadge (BadgeProof (BBSPresHeader ph) (BBSProof p) info) st
|
||||
_ -> Nothing
|
||||
(Just mk, Just sg, _, _) -> Just $ OwnBadge (BadgeCredential (BadgeMasterKey mk) (BBSSignature sg) info) st
|
||||
(_, _, Just p, Just ph) -> Just $ PeerBadge (BadgeProof (BBSPresHeader ph) (BBSProof p) info) st
|
||||
_ -> Just $ ShownBadge info st
|
||||
|
||||
-- JSON
|
||||
|
||||
@@ -357,13 +355,18 @@ instance FromJSON (Badge 'BCCredential) where
|
||||
JBadgeCredential mk sg i -> pure (BadgeCredential mk sg i)
|
||||
_ -> fail "expected badge credential"
|
||||
|
||||
-- LocalBadge round-trips (the inner Badge tags which crypto it is).
|
||||
-- LocalBadge is sent to the UI/clients WITHOUT crypto - only disclosed info + status. The credential/proof
|
||||
-- bytes stay core-side. FromJSON reconstructs a display-only badge (empty proof) for read-only consumers
|
||||
-- (remote host, UI echoes); the authoritative badge is loaded from the DB (rowToBadge), never from this JSON.
|
||||
data JSONBadge = JSONBadge {badge :: BadgeInfo, status :: BadgeStatus}
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''JSONBadge)
|
||||
|
||||
instance ToJSON LocalBadge where
|
||||
toJSON (LocalBadge b st) = J.object ["badge" .= b, "status" .= st]
|
||||
toJSON lb = toJSON $ JSONBadge (localBadgeInfo lb) (localBadgeStatus lb)
|
||||
toEncoding lb = toEncoding $ JSONBadge (localBadgeInfo lb) (localBadgeStatus lb)
|
||||
|
||||
instance FromJSON LocalBadge where
|
||||
parseJSON = J.withObject "LocalBadge" $ \o -> do
|
||||
st <- o .: "status"
|
||||
bv <- o .: "badge"
|
||||
(flip LocalBadge st <$> (parseJSON bv :: Parser (Badge 'BCProof)))
|
||||
<|> (flip LocalBadge st <$> (parseJSON bv :: Parser (Badge 'BCCredential)))
|
||||
parseJSON v = do
|
||||
JSONBadge info st <- parseJSON v
|
||||
pure $ ShownBadge info st
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Offline operator tooling for supporter badges, invoked as `simplex-chat badge ...`.
|
||||
-- `keygen` prints a base64url keypair (the public key is hardcoded into the app config);
|
||||
-- `sign` mints a credential as one-line JSON to paste into the app via `/badge add`.
|
||||
-- keygen - the issuer keypair (the "secret" signs; the "public" is hardcoded into the app config).
|
||||
-- master-key - the user's master secret (their unlinkability secret; generated client-side in the real flow).
|
||||
-- sign - bind a user master secret to a badge with the issuer secret, printed as one-line JSON for `/badge add`.
|
||||
module Simplex.Chat.Badges.CLI (runBadgeCommand) where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
@@ -16,19 +17,26 @@ import Data.Time.Format (defaultTimeLocale, parseTimeM)
|
||||
import Options.Applicative
|
||||
import Simplex.Chat.Badges
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen)
|
||||
import Simplex.Messaging.Crypto.BBS (BBSPublicKey (..), BBSSecretKey (..), bbsKeyGen)
|
||||
import Simplex.Messaging.Encoding.String (strDecode, strEncode, textDecode)
|
||||
import System.Exit (die)
|
||||
|
||||
-- the issuer keypair is secret (32 bytes) <> public (96 bytes).
|
||||
bbsSecretLen, bbsPublicLen :: Int
|
||||
bbsSecretLen = 32
|
||||
bbsPublicLen = 96
|
||||
|
||||
data BadgeCommand
|
||||
= Keygen
|
||||
| Sign BBSSecretKey BBSPublicKey BadgeType (Maybe UTCTime)
|
||||
| MasterKey
|
||||
| Sign BBSSecretKey BBSPublicKey BadgeMasterKey BadgeType (Maybe UTCTime)
|
||||
|
||||
runBadgeCommand :: [String] -> IO ()
|
||||
runBadgeCommand args =
|
||||
handleParseResult (execParserPure defaultPrefs badgeInfo args) >>= \case
|
||||
Keygen -> keygen
|
||||
Sign sk pk badgeType badgeExpiry -> sign sk pk badgeType badgeExpiry
|
||||
MasterKey -> genMasterKey
|
||||
Sign sk pk ms badgeType badgeExpiry -> sign sk pk ms badgeType badgeExpiry
|
||||
where
|
||||
badgeInfo = info (helper <*> hsubparser badgeCmd) fullDesc
|
||||
badgeCmd = command "badge" (info (helper <*> badgeCommandP) (progDesc "SimpleX supporter badge tooling"))
|
||||
@@ -36,16 +44,21 @@ runBadgeCommand args =
|
||||
badgeCommandP :: Parser BadgeCommand
|
||||
badgeCommandP =
|
||||
hsubparser $
|
||||
command "keygen" (info (pure Keygen) (progDesc "generate a BBS issuer keypair (base64url)"))
|
||||
<> command "sign" (info signP (progDesc "sign a badge credential, printed as one-line JSON"))
|
||||
command "keygen" (info (pure Keygen) (progDesc "generate an issuer keypair (issuer secret + public, base64url)"))
|
||||
<> command "master-key" (info (pure MasterKey) (progDesc "generate a user master secret (base64url)"))
|
||||
<> command "sign" (info signP (progDesc "sign a badge for a user master secret, printed as one-line JSON"))
|
||||
where
|
||||
signP =
|
||||
Sign
|
||||
<$> keyOpt "secret" "SK" "issuer secret key (base64url)"
|
||||
<*> keyOpt "key" "PK" "issuer public key (base64url)"
|
||||
<*> option (eitherReader badgeTypeR) (long "type" <> metavar "TYPE" <> help "badge type (supporter, business, ...)")
|
||||
(\(sk, pk) -> Sign sk pk)
|
||||
<$> option (eitherReader issuerKeyR) (long "secret" <> metavar "ISSUER_KEY" <> help "issuer keypair from keygen (base64url)")
|
||||
<*> option (eitherReader (strDecode . B.pack)) (long "master" <> metavar "MASTER" <> help "user master secret from master-key (base64url)")
|
||||
<*> option (eitherReader badgeTypeR) (long "type" <> metavar "TYPE" <> help "badge type (supporter, legend, investor)")
|
||||
<*> option (eitherReader expireR) (long "expire" <> metavar "lifetime|YYYY-MM-DD" <> help "expiry date, or 'lifetime'")
|
||||
keyOpt l m h = option (eitherReader $ strDecode . B.pack) (long l <> metavar m <> help h)
|
||||
issuerKeyR s = do
|
||||
kp <- strDecode (B.pack s)
|
||||
if B.length kp == bbsSecretLen + bbsPublicLen
|
||||
then let (sk, pk) = B.splitAt bbsSecretLen kp in Right (BBSSecretKey sk, BBSPublicKey pk)
|
||||
else Left "bad issuer key - use the 'secret' value from keygen"
|
||||
badgeTypeR = maybe (Left "invalid badge type") Right . textDecode . T.pack
|
||||
expireR = \case
|
||||
"lifetime" -> Right Nothing
|
||||
@@ -55,16 +68,20 @@ keygen :: IO ()
|
||||
keygen =
|
||||
bbsKeyGen >>= \case
|
||||
Left e -> die $ "keygen failed: " <> e
|
||||
Right (sk, pk) -> do
|
||||
B.putStrLn $ "secret " <> strEncode sk
|
||||
Right (BBSSecretKey sk, BBSPublicKey pk) -> do
|
||||
B.putStrLn $ "secret " <> strEncode (sk <> pk)
|
||||
B.putStrLn $ "public " <> strEncode pk
|
||||
|
||||
sign :: BBSSecretKey -> BBSPublicKey -> BadgeType -> Maybe UTCTime -> IO ()
|
||||
sign secretKey publicKey badgeType badgeExpiry = do
|
||||
genMasterKey :: IO ()
|
||||
genMasterKey = do
|
||||
drg <- C.newRandom
|
||||
masterKey <- generateMasterKey drg
|
||||
let req = VerifiedBadgeRequest BadgeRequest {masterKey, badgeInfo = BadgeInfo {badgeType, badgeExpiry, badgeExtra = ""}}
|
||||
mk <- generateMasterKey drg
|
||||
B.putStrLn $ strEncode mk
|
||||
|
||||
sign :: BBSSecretKey -> BBSPublicKey -> BadgeMasterKey -> BadgeType -> Maybe UTCTime -> IO ()
|
||||
sign secretKey publicKey masterKey' badgeType badgeExpiry = do
|
||||
let req = VerifiedBadgeRequest BadgeRequest {masterKey = masterKey', badgeInfo = BadgeInfo {badgeType, badgeExpiry, badgeExtra = ""}}
|
||||
issueBadge secretKey publicKey req >>= \case
|
||||
Left e -> die $ "sign failed: " <> e
|
||||
-- single-line JSON, pasted into the app via `/badge add`
|
||||
-- single-line JSON (master secret + signature + info), pasted into the app via `/badge add`
|
||||
Right cred -> LB.putStrLn $ J.encode cred
|
||||
|
||||
@@ -4650,7 +4650,7 @@ addUserBadge user cred = do
|
||||
key <- asks $ badgePublicKey . config
|
||||
verified <- liftIO $ verifyCredential key cred
|
||||
unless verified $ throwCmdError "badge credential does not verify against configured key"
|
||||
user' <- withFastStore' $ \db -> setUserBadge db user (Just (LocalBadge cred BSActive))
|
||||
user' <- withFastStore' $ \db -> setUserBadge db user (Just (OwnBadge cred BSActive))
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||
cxt <- asks $ mkStoreCxt . config
|
||||
contacts <- withFastStore' $ \db -> getUserContacts db cxt user'
|
||||
|
||||
@@ -1900,7 +1900,7 @@ sendDirectContactMessages' user ct events = do
|
||||
-- only own credentials present (peers carry proofs, which are not re-presented). callers must not present on incognito sends.
|
||||
presentUserBadge :: User -> Profile -> CM Profile
|
||||
presentUserBadge User {profile = LocalProfile {localBadge}} p = case localBadge of
|
||||
Just (LocalBadge cred@BadgeCredential {} _) -> do
|
||||
Just (OwnBadge cred _) -> do
|
||||
key <- asks $ badgePublicKey . config
|
||||
liftIO (badgeProof key cred PHTest) >>= \case
|
||||
Right proof -> pure p {badge = Just proof}
|
||||
|
||||
@@ -782,16 +782,18 @@ toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Bool -> Local
|
||||
toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} localAlias now verified =
|
||||
LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias}
|
||||
where
|
||||
localBadge = (\b@(BadgeProof _ _ info) -> LocalBadge b (mkBadgeStatus now verified info)) <$> badge
|
||||
localBadge = (\b@(BadgeProof _ _ info) -> PeerBadge b (mkBadgeStatus now verified info)) <$> badge
|
||||
|
||||
fromLocalProfile :: LocalProfile -> Profile
|
||||
fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge} =
|
||||
Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge}
|
||||
where
|
||||
-- only a verified peer proof rides the wire; the own credential is presented fresh, and a display-only badge never sends
|
||||
wireBadge :: LocalBadge -> Maybe (Badge 'BCProof)
|
||||
wireBadge (LocalBadge b _) = case b of
|
||||
BadgeProof {} -> Just b
|
||||
BadgeCredential {} -> Nothing
|
||||
wireBadge = \case
|
||||
PeerBadge b _ -> Just b
|
||||
OwnBadge _ _ -> Nothing
|
||||
ShownBadge _ _ -> Nothing
|
||||
|
||||
profileBadgeVerified :: BBSPublicKey -> LocalProfile -> Profile -> IO Bool
|
||||
profileBadgeVerified key LocalProfile {localBadge} Profile {badge = newBadge} =
|
||||
|
||||
Reference in New Issue
Block a user