From cb87571cf40755a1ab8d4c42da36454cc848d4f7 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 10:55:47 +0200 Subject: [PATCH 01/16] gitignored --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2ec979105..7c0c6141a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*aptos_examples* +*APTOS*.md #Demos files /.demos_id* demos_peers @@ -108,4 +110,5 @@ architecture .DS_Store blocked_ips.json SMART_CONTRACTS_*.md -.gitbook* \ No newline at end of file +.gitbook* +TG_IDENTITY_PLAN.md From 4f092adb829af147e197a718b702cf340242cb0f Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 14:42:13 +0200 Subject: [PATCH 02/16] linting --- src/features/multichain/routines/executors/pay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/multichain/routines/executors/pay.ts b/src/features/multichain/routines/executors/pay.ts index 92e543284..8b274683b 100644 --- a/src/features/multichain/routines/executors/pay.ts +++ b/src/features/multichain/routines/executors/pay.ts @@ -126,9 +126,9 @@ async function genericJsonRpcPay( } try { - let signedTx = operation.task.signedPayloads[0]; + let signedTx = operation.task.signedPayloads[0] - signedTx = validateIfUint8Array(signedTx); + signedTx = validateIfUint8Array(signedTx) // INFO: Send payload and return the result const result = await instance.sendTransaction(signedTx) From a1793499ad8c33e74ad4217a6f811d5282ca37a5 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 14:42:22 +0200 Subject: [PATCH 03/16] linting --- src/utilities/validateUint8Array.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/validateUint8Array.ts b/src/utilities/validateUint8Array.ts index f7b545730..4303b1e89 100644 --- a/src/utilities/validateUint8Array.ts +++ b/src/utilities/validateUint8Array.ts @@ -1,9 +1,9 @@ export default function validateIfUint8Array(input: unknown): Uint8Array | unknown { - if (typeof input === 'object' && input !== null) { + if (typeof input === "object" && input !== null) { const txArray = Object.keys(input) .sort((a, b) => Number(a) - Number(b)) .map(k => input[k]) return Buffer.from(txArray) } - return input; + return input } From 5adefa4a4f62db0875f83ac4a2ccc4c271b4dc5b Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 14:42:43 +0200 Subject: [PATCH 04/16] telegram id verification tools --- package.json | 2 +- src/libs/identity/tools/telegram.ts | 306 ++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 src/libs/identity/tools/telegram.ts diff --git a/package.json b/package.json index a451e21a0..bc60f8ae7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@fastify/cors": "^9.0.1", "@fastify/swagger": "^8.15.0", "@fastify/swagger-ui": "^4.1.0", - "@kynesyslabs/demosdk": "^2.3.6", + "@kynesyslabs/demosdk": "^2.3.7", "@modelcontextprotocol/sdk": "^1.13.3", "@octokit/core": "^6.1.5", "@types/express": "^4.17.21", diff --git a/src/libs/identity/tools/telegram.ts b/src/libs/identity/tools/telegram.ts new file mode 100644 index 000000000..18c39fb0e --- /dev/null +++ b/src/libs/identity/tools/telegram.ts @@ -0,0 +1,306 @@ +import * as crypto from "crypto" +import log from "@/utilities/logger" +import Chain from "@/libs/blockchain/chain" +import { + TelegramVerificationRequest, + TelegramVerificationResponse, + TelegramChallengeResponse, +} from "@kynesyslabs/demosdk/types" +import { ucrypto, hexToUint8Array } from "@kynesyslabs/demosdk/encryption" + +/** + * Internal challenge storage interface + */ +interface TelegramChallenge { + challenge: string + demos_address: string + timestamp: number + used: boolean +} + +/** + * Telegram identity verification tool + * Handles challenge generation, bot authorization, and signature verification + */ +export default class Telegram { + private static instance: Telegram + private challenges: Map = new Map() + private authorizedBots: string[] = [] + private lastGenesisCheck = 0 + + private constructor() { + // Private constructor for singleton pattern + } + + /** + * Get the singleton instance of the Telegram tool + */ + static getInstance(): Telegram { + if (!Telegram.instance) { + Telegram.instance = new Telegram() + } + return Telegram.instance + } + + /** + * Load authorized bot addresses from genesis block + * Results are cached for 1 hour since genesis never changes + */ + async getAuthorizedBots(): Promise { + // Cache for 1 hour since genesis never changes + if (Date.now() - this.lastGenesisCheck < 3600000 && this.authorizedBots.length > 0) { + return this.authorizedBots + } + + try { + const genesisBlock = await Chain.getGenesisBlock() + if (!genesisBlock || !genesisBlock.content) { + log.error("Genesis block not found or has no content") + return [] + } + + const genesisData = JSON.parse(genesisBlock.content) + + // Extract addresses from balances array + this.authorizedBots = genesisData.balances?.map((balance: [string, string]) => + balance[0].toLowerCase(), + ) || [] + + this.lastGenesisCheck = Date.now() + + log.info(`Loaded ${this.authorizedBots.length} authorized Telegram bot addresses from genesis`) + return this.authorizedBots + } catch (error) { + log.error("Failed to load authorized bots from genesis:"+ error) + return [] + } + } + + /** + * Check if a bot address is authorized (from genesis block) + */ + async isAuthorizedBot(botAddress: string): Promise { + const authorizedBots = await this.getAuthorizedBots() + return authorizedBots.includes(botAddress.toLowerCase()) + } + + /** + * Generate a challenge for Telegram identity verification + * Format: DEMOS_TG_BIND___ + */ + generateChallenge(demosAddress: string): TelegramChallengeResponse { + const timestamp = Math.floor(Date.now() / 1000) + const nonce = crypto.randomBytes(16).toString("hex") + const challenge = `DEMOS_TG_BIND_${demosAddress}_${timestamp}_${nonce}` + + // Store challenge for 15 minutes + this.challenges.set(challenge, { + challenge, + demos_address: demosAddress.toLowerCase(), + timestamp, + used: false, + }) + + // Auto-cleanup after 15 minutes + setTimeout(() => { + this.challenges.delete(challenge) + }, 15 * 60 * 1000) + + log.info(`Generated Telegram challenge for address ${demosAddress}`) + + return { challenge } + } + + /** + * Parse a challenge to extract its components + */ + private parseChallenge(challenge: string): { + demosAddress: string + timestamp: number + nonce: string + } | null { + const parts = challenge.split("_") + if (parts.length !== 5 || parts[0] !== "DEMOS" || parts[1] !== "TG" || parts[2] !== "BIND") { + return null + } + + const timestamp = parseInt(parts[4]) + if (isNaN(timestamp)) { + return null + } + + return { + demosAddress: parts[3], + timestamp, + nonce: parts[5], + } + } + + /** + * Verify a Telegram verification request from a bot + * This includes both bot signature verification and user signature verification + */ + async verifyAttestation(request: TelegramVerificationRequest): Promise { + try { + // 1. Check if bot address is authorized (from genesis) + if (!(await this.isAuthorizedBot(request.bot_address))) { + log.warning(`Unauthorized bot address attempted verification: ${request.bot_address}`) + return { + success: false, + message: "Unauthorized bot address", + } + } + + // 2. Parse the challenge from user's signed message + const challengeData = this.parseChallenge(request.signed_challenge.split(":")[0] || request.signed_challenge) + if (!challengeData) { + return { + success: false, + message: "Invalid challenge format", + } + } + + // 3. Check if challenge exists and is not expired/used + const storedChallenge = this.challenges.get(`DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}`) + if (!storedChallenge) { + return { + success: false, + message: "Challenge not found or expired", + } + } + + if (storedChallenge.used) { + return { + success: false, + message: "Challenge already used", + } + } + + // 4. Verify bot signature + const attestationData = { + telegram_id: request.telegram_id, + username: request.username, + signed_challenge: request.signed_challenge, + timestamp: request.timestamp, + } + const attestationJson = JSON.stringify(attestationData, Object.keys(attestationData).sort()) + const attestationMessage = new TextEncoder().encode(attestationJson) + + const botSignatureValid = await ucrypto.verify({ + algorithm: "ed25519", + signature: hexToUint8Array(request.bot_signature), + publicKey: hexToUint8Array(request.bot_address), + message: attestationMessage, + }) + + if (!botSignatureValid) { + log.warning(`Invalid bot signature from ${request.bot_address}`) + return { + success: false, + message: "Invalid bot signature", + } + } + + // 5. Verify user signature against the original challenge + const originalChallenge = `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}` + const challengeMessage = new TextEncoder().encode(originalChallenge) + + // Extract signature from signed challenge (assuming format: "challenge:signature" or just signature) + const signaturePart = request.signed_challenge.includes(":") + ? request.signed_challenge.split(":")[1] + : request.signed_challenge + + const userSignatureValid = await ucrypto.verify({ + algorithm: "ed25519", + signature: hexToUint8Array(signaturePart), + publicKey: hexToUint8Array(challengeData.demosAddress), + message: challengeMessage, + }) + + if (!userSignatureValid) { + log.warning(`Invalid user signature for challenge from ${challengeData.demosAddress}`) + return { + success: false, + message: "Invalid user signature", + } + } + + // 6. Mark challenge as used + storedChallenge.used = true + + // 7. Return success with verification data + log.info(`Successfully verified Telegram identity: ${request.telegram_id} ↔ ${challengeData.demosAddress}`) + + return { + success: true, + message: "Telegram identity verified successfully", + demosAddress: challengeData.demosAddress, + telegramData: { + userId: request.telegram_id, + username: request.username, + timestamp: request.timestamp, + }, + } + + } catch (error) { + log.error("Error verifying Telegram attestation:" + error) + return { + success: false, + message: "Internal verification error", + } + } + } + + /** + * Get statistics about current challenges (for debugging/monitoring) + */ + getChallengeStats(): { + total: number + active: number + used: number + expired: number + } { + const now = Math.floor(Date.now() / 1000) + let active = 0 + let used = 0 + let expired = 0 + + for (const challenge of this.challenges.values()) { + if (challenge.used) { + used++ + } else if (now - challenge.timestamp > 900) { // 15 minutes + expired++ + } else { + active++ + } + } + + return { + total: this.challenges.size, + active, + used, + expired, + } + } + + /** + * Clean up expired challenges (called periodically) + */ + cleanupExpiredChallenges(): number { + const now = Math.floor(Date.now() / 1000) + let cleaned = 0 + + for (const [key, challenge] of this.challenges.entries()) { + if (now - challenge.timestamp > 900) { // 15 minutes + this.challenges.delete(key) + cleaned++ + } + } + + if (cleaned > 0) { + log.info(`Cleaned up ${cleaned} expired Telegram challenges`) + } + + return cleaned + } +} \ No newline at end of file From f9f497aacb7dfb59386c21685200c8cc7115e299 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 15:01:39 +0200 Subject: [PATCH 05/16] added telegram tg verify endpoints --- src/libs/network/server_rpc.ts | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/libs/network/server_rpc.ts b/src/libs/network/server_rpc.ts index 7de19dc19..eb83222d0 100644 --- a/src/libs/network/server_rpc.ts +++ b/src/libs/network/server_rpc.ts @@ -31,6 +31,8 @@ import { manageNativeBridge } from "./manageNativeBridge" import Chain from "../blockchain/chain" import { RateLimiter } from "./middleware/rateLimiter" import GCR from "../blockchain/gcr/gcr" +import Telegram from "../identity/tools/telegram" +import { TelegramChallengeRequest, TelegramVerificationRequest } from "@kynesyslabs/demosdk/types" // Reading the port from sharedState const noAuthMethods = ["nodeCall"] @@ -369,6 +371,60 @@ export async function serverRpcBun() { return jsonResponse(rateLimiter.getStats()) }) + // REVIEW: Telegram identity verification endpoints + // Generate challenge for Telegram verification + server.post("/api/tg-challenge", async req => { + try { + const payload = await req.json() + + // Validate request structure + if (!payload.demos_address || typeof payload.demos_address !== "string") { + return jsonResponse({ + error: "Invalid request: demos_address is required", + }, 400) + } + + const telegramTool = Telegram.getInstance() + const challengeResponse = telegramTool.generateChallenge(payload.demos_address) + + return jsonResponse(challengeResponse) + } catch (error) { + log.error("[Telegram] Error generating challenge: " + error) + return jsonResponse({ + error: "Internal error generating challenge", + }, 500) + } + }) + + // Verify Telegram attestation from bot + server.post("/api/tg-verify", async req => { + try { + const payload = await req.json() + + // Validate request structure - check all required fields + const requiredFields = ["telegram_id", "username", "signed_challenge", "timestamp", "bot_address", "bot_signature"] + for (const field of requiredFields) { + if (!payload[field]) { + return jsonResponse({ + error: `Invalid request: ${field} is required`, + }, 400) + } + } + + const telegramTool = Telegram.getInstance() + const verificationResponse = await telegramTool.verifyAttestation(payload as TelegramVerificationRequest) + + // Return appropriate HTTP status based on verification result + const statusCode = verificationResponse.success ? 200 : 400 + return jsonResponse(verificationResponse, statusCode) + } catch (error) { + log.error("[Telegram] Error verifying attestation: " + error) + return jsonResponse({ + error: "Internal error verifying attestation", + }, 500) + } + }) + // Main RPC endpoint server.post("/", async req => { try { From b062d811a430514043d7c3ce50db013e2421830c Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 15:11:45 +0200 Subject: [PATCH 06/16] added tx based support for tg identities (just as for x identities) --- src/libs/abstraction/web2/telegram.ts | 103 ++++++++++++++++++++++++++ src/libs/identity/tools/telegram.ts | 80 +++++++++++++++++++- src/libs/network/server_rpc.ts | 26 ++++++- 3 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 src/libs/abstraction/web2/telegram.ts diff --git a/src/libs/abstraction/web2/telegram.ts b/src/libs/abstraction/web2/telegram.ts new file mode 100644 index 000000000..6e713c620 --- /dev/null +++ b/src/libs/abstraction/web2/telegram.ts @@ -0,0 +1,103 @@ +import { Web2ProofParser } from "./parsers" +import Telegram from "@/libs/identity/tools/telegram" +import { SigningAlgorithm } from "@kynesyslabs/demosdk/types" + +/** + * TelegramProofParser - Parses and validates Telegram identity proofs + * + * This parser handles the verification of Telegram identity claims by processing + * bot-attested verifications. Unlike Twitter proofs which are parsed from tweets, + * Telegram proofs are validated through a challenge-response mechanism with + * authorized bot attestation. + * + * Flow: + * 1. Bot receives signed challenge from user + * 2. Bot creates attestation with Telegram user data + * 3. This parser extracts and validates the proof from bot attestation + * 4. Returns signature data for identity verification + */ +export class TelegramProofParser extends Web2ProofParser { + private static instance: TelegramProofParser + telegram: Telegram + + constructor() { + super() + this.telegram = Telegram.getInstance() + } + + /** + * Reads and validates Telegram identity proof data + * + * For Telegram, the "proof" is the bot's attestation containing: + * - User's signed challenge + * - Telegram user data (ID, username) + * - Bot's signature of the attestation + * + * @param proofData - JSON string containing bot attestation data + * @returns Parsed signature data for verification + */ + async readData(proofData: string): Promise<{ + message: string + signature: string + type: SigningAlgorithm + }> { + try { + // REVIEW: For Telegram, the "proof" is actually the bot attestation data + // Parse the bot attestation containing the user's signed challenge + const attestationData = JSON.parse(proofData) + + // Validate attestation structure + const requiredFields = ["telegram_id", "username", "signed_challenge", "timestamp", "bot_address", "bot_signature"] + for (const field of requiredFields) { + if (!attestationData[field]) { + throw new Error(`Missing required field: ${field}`) + } + } + + // Verify the bot attestation first (this validates both signatures) + const verificationResult = await this.telegram.verifyAttestation(attestationData) + + if (!verificationResult.success) { + throw new Error(`Telegram verification failed: ${verificationResult.message}`) + } + + // Extract the user's signature from the signed challenge + // The signed_challenge format is: "challenge:signature" or just "signature" + const signedChallenge = attestationData.signed_challenge + const signaturePart = signedChallenge.includes(":") + ? signedChallenge.split(":")[1] + : signedChallenge + + // Extract the original challenge message + const challengePart = signedChallenge.includes(":") + ? signedChallenge.split(":")[0] + : null + + if (!challengePart) { + throw new Error("Invalid signed challenge format - missing challenge part") + } + + // REVIEW: Return the signature data in the same format as TwitterProofParser + // This allows the identity system to verify the user's signature + return { + message: challengePart, // Original challenge that was signed + signature: signaturePart, // User's signature of the challenge + type: "ed25519" as SigningAlgorithm, // Demos uses ed25519 signatures + } + + } catch (error) { + throw new Error(`Failed to parse Telegram proof: ${error instanceof Error ? error.message : String(error)}`) + } + } + + /** + * Get singleton instance of TelegramProofParser + */ + static async getInstance() { + if (!this.instance) { + this.instance = new TelegramProofParser() + } + + return this.instance + } +} \ No newline at end of file diff --git a/src/libs/identity/tools/telegram.ts b/src/libs/identity/tools/telegram.ts index 18c39fb0e..fc021fb15 100644 --- a/src/libs/identity/tools/telegram.ts +++ b/src/libs/identity/tools/telegram.ts @@ -5,7 +5,9 @@ import { TelegramVerificationRequest, TelegramVerificationResponse, TelegramChallengeResponse, + Transaction, } from "@kynesyslabs/demosdk/types" +import { InferFromTelegramPayload } from "@kynesyslabs/demosdk/abstraction" import { ucrypto, hexToUint8Array } from "@kynesyslabs/demosdk/encryption" /** @@ -228,18 +230,34 @@ export default class Telegram { // 6. Mark challenge as used storedChallenge.used = true - // 7. Return success with verification data + // 7. Create unsigned identity transaction following Twitter pattern + const unsignedTransaction = this.createIdentityTransaction( + challengeData.demosAddress, + request.telegram_id, + request.username, + JSON.stringify({ + telegram_id: request.telegram_id, + username: request.username, + signed_challenge: request.signed_challenge, + timestamp: request.timestamp, + bot_address: request.bot_address, + bot_signature: request.bot_signature, + }), + ) + + // 8. Return success with unsigned transaction for user to sign log.info(`Successfully verified Telegram identity: ${request.telegram_id} ↔ ${challengeData.demosAddress}`) return { success: true, - message: "Telegram identity verified successfully", + message: "Telegram identity verified. Please sign the transaction to complete binding.", demosAddress: challengeData.demosAddress, telegramData: { userId: request.telegram_id, username: request.username, timestamp: request.timestamp, }, + unsignedTransaction: unsignedTransaction, } } catch (error) { @@ -251,6 +269,64 @@ export default class Telegram { } } + /** + * Creates an unsigned identity transaction for Telegram verification + * + * This follows the same pattern as Twitter identity transactions: + * - Transaction type: "identity" + * - Context: "web2" + * - Method: "web2_identity_assign" + * - Payload contains Telegram identity data and bot attestation proof + * + * @param demosAddress - User's Demos address + * @param telegramId - Telegram user ID + * @param username - Telegram username + * @param proofData - JSON string containing bot attestation + * @returns Unsigned transaction ready for user signature + */ + private createIdentityTransaction( + demosAddress: string, + telegramId: string, + username: string, + proofData: string, + ): Transaction { + // REVIEW: Create transaction following the exact Twitter pattern + // See DemosTransactions.empty() and Identities.inferWeb2Identity() + + const telegramPayload: InferFromTelegramPayload = { + context: "telegram", + proof: proofData, // Bot attestation containing all verification data + username: username, + userId: telegramId, + } + + // Create transaction skeleton (same structure as Twitter identity transactions) + const transaction: Transaction = { + hash: "", // Will be calculated when signed + content: { + type: "identity", + from_ed25519_address: demosAddress, + to: demosAddress, // Identity transactions are self-directed + amount: 0, // No tokens transferred for identity binding + data: [ + "identity", // Transaction data type identifier + { + context: "web2", // Web2 identity context + method: "web2_identity_assign", // Identity assignment method + payload: telegramPayload, // Telegram-specific payload + }, + ], + timestamp: Date.now(), + nonce: 0, // Will be set by transaction processing + gcr_edits: [], // Will be generated during transaction validation + }, + signature: null, // User must sign this + } + + log.info(`Created unsigned Telegram identity transaction for ${demosAddress} ↔ ${telegramId}`) + return transaction + } + /** * Get statistics about current challenges (for debugging/monitoring) */ diff --git a/src/libs/network/server_rpc.ts b/src/libs/network/server_rpc.ts index eb83222d0..d4700567d 100644 --- a/src/libs/network/server_rpc.ts +++ b/src/libs/network/server_rpc.ts @@ -396,7 +396,7 @@ export async function serverRpcBun() { } }) - // Verify Telegram attestation from bot + // Verify Telegram attestation from bot and create unsigned identity transaction server.post("/api/tg-verify", async req => { try { const payload = await req.json() @@ -414,9 +414,27 @@ export async function serverRpcBun() { const telegramTool = Telegram.getInstance() const verificationResponse = await telegramTool.verifyAttestation(payload as TelegramVerificationRequest) - // Return appropriate HTTP status based on verification result - const statusCode = verificationResponse.success ? 200 : 400 - return jsonResponse(verificationResponse, statusCode) + // REVIEW: New flow - return unsigned transaction for user to sign + // This follows the transaction-based pattern like Twitter identities + if (verificationResponse.success) { + log.info(`[Telegram] Verification successful, returning unsigned transaction for ${verificationResponse.demosAddress}`) + + return jsonResponse({ + success: true, + message: verificationResponse.message, + demosAddress: verificationResponse.demosAddress, + telegramData: verificationResponse.telegramData, + unsignedTransaction: verificationResponse.unsignedTransaction, + }, 200) + } else { + log.warning(`[Telegram] Verification failed: ${verificationResponse.message}`) + + return jsonResponse({ + success: false, + message: verificationResponse.message, + }, 400) + } + } catch (error) { log.error("[Telegram] Error verifying attestation: " + error) return jsonResponse({ From 5c025f5433d5b81df84f72fe1b39e910d79dfa8e Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 15:12:00 +0200 Subject: [PATCH 07/16] added experimental support for incentives and telegram --- src/libs/abstraction/index.ts | 22 +++++++++++++++-- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 18 +++++++++++++- .../gcr/gcr_routines/IncentiveManager.ts | 24 ++++++++++++++++++- .../gcr/gcr_routines/identityManager.ts | 8 ++++--- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index a8d06e23b..57e6db78c 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -1,5 +1,6 @@ import { GithubProofParser } from "./web2/github" import { TwitterProofParser } from "./web2/twitter" +import { TelegramProofParser } from "./web2/telegram" import { type Web2ProofParser } from "./web2/parsers" import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction" import { hexToUint8Array, ucrypto } from "@kynesyslabs/demosdk/encryption" @@ -11,11 +12,23 @@ import { Twitter } from "../identity/tools/twitter" * @param payload - The proof payload * @returns true if the proof is valid, false otherwise */ +/** + * Verifies Web2 identity proofs (Twitter, GitHub, Telegram) + * + * This function handles the verification of Web2 identity claims by: + * 1. Selecting the appropriate proof parser based on context + * 2. Performing context-specific validations (bot detection for Twitter) + * 3. Extracting and verifying the cryptographic signature + * + * @param payload - Web2 identity payload containing proof data + * @param sender - The Demos address claiming the identity + * @returns Verification result with success status and message + */ export async function verifyWeb2Proof( payload: Web2CoreTargetIdentityPayload, sender: string, ) { - let parser: typeof TwitterProofParser | typeof GithubProofParser + let parser: typeof TwitterProofParser | typeof GithubProofParser | typeof TelegramProofParser switch (payload.context) { case "twitter": @@ -24,6 +37,11 @@ export async function verifyWeb2Proof( case "github": parser = GithubProofParser break + case "telegram": + // REVIEW: Telegram proofs are handled differently - they come from bot attestations + // rather than user-posted content, but follow the same verification pattern + parser = TelegramProofParser + break default: return { success: false, @@ -89,4 +107,4 @@ export async function verifyWeb2Proof( } } -export { TwitterProofParser, Web2ProofParser } +export { TwitterProofParser, TelegramProofParser, Web2ProofParser } diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index 7ddf73027..adf535e79 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -234,6 +234,20 @@ export default class GCRIdentityRoutines { editOperation.referralCode, ) } + } else if (context === "telegram") { + const isFirst = await this.isFirstConnection( + "telegram", + { userId: data.userId }, + gcrMainRepository, + editOperation.account, + ) + if (isFirst) { + await IncentiveManager.telegramLinked( + editOperation.account, + data.userId, + editOperation.referralCode, + ) + } } else if (context === "github") { // Future implementation for GitHub log.info( @@ -278,10 +292,12 @@ export default class GCRIdentityRoutines { await gcrMainRepository.save(accountGCR) /** - * Deduct incentive points for Twitter unlinking + * Deduct incentive points for unlinking social accounts */ if (context === "twitter") { await IncentiveManager.twitterUnlinked(editOperation.account) + } else if (context === "telegram") { + await IncentiveManager.telegramUnlinked(editOperation.account) } } diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts index fa6333b00..ab5a8a872 100644 --- a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts @@ -3,7 +3,7 @@ import { PointSystem } from "@/features/incentive/PointSystem" /** * This class is used to manage the incentives for the user. - * It is used to award points to the user for linking their wallet and Twitter account. + * It is used to award points to the user for linking their wallet, Twitter account, and Telegram account. * It is also used to get the points for the user. */ export class IncentiveManager { @@ -62,6 +62,28 @@ export class IncentiveManager { return await this.pointSystem.deductTwitterPoints(userId) } + /** + * Hook to be called after Telegram linking + */ + static async telegramLinked( + userId: string, + telegramUserId: string, + referralCode?: string, + ): Promise { + return await this.pointSystem.awardTelegramPoints( + userId, + telegramUserId, + referralCode, + ) + } + + /** + * Hook to be called after Telegram unlinking + */ + static async telegramUnlinked(userId: string): Promise { + return await this.pointSystem.deductTelegramPoints(userId) + } + /** * Hook to get the points for a user */ diff --git a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts index c3bbe68ef..df04f979d 100644 --- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts @@ -70,14 +70,16 @@ export default class IdentityManager { sender: string, payload: InferFromSignaturePayload, ): Promise<{ success: boolean; message: string }> { - // INFO: Check if the user has a Twitter account + // INFO: Check if the user has a Twitter or Telegram account const account = await ensureGCRForUser(sender) const twitterAccounts = account.identities.web2["twitter"] || [] - if (twitterAccounts.length === 0) { + const telegramAccounts = account.identities.web2["telegram"] || [] + + if (twitterAccounts.length === 0 && telegramAccounts.length === 0) { return { success: false, message: - "Error: No Twitter account found. Please connect a Twitter account first", + "Error: No Twitter or Telegram account found. Please connect a social account first", } } From 78f4a0a543f8bd9307e99d6d69dc923564fcb6e1 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 15:16:58 +0200 Subject: [PATCH 08/16] updated sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc60f8ae7..82ee5f5fa 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@fastify/cors": "^9.0.1", "@fastify/swagger": "^8.15.0", "@fastify/swagger-ui": "^4.1.0", - "@kynesyslabs/demosdk": "^2.3.7", + "@kynesyslabs/demosdk": "^2.3.8", "@modelcontextprotocol/sdk": "^1.13.3", "@octokit/core": "^6.1.5", "@types/express": "^4.17.21", From ea192945832372c80ecf39fcb9c83456947c63a7 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Tue, 5 Aug 2025 15:22:04 +0200 Subject: [PATCH 09/16] added instructions for dapp/bot things --- TG_INTEGRATION.md | 455 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 TG_INTEGRATION.md diff --git a/TG_INTEGRATION.md b/TG_INTEGRATION.md new file mode 100644 index 000000000..51303e84f --- /dev/null +++ b/TG_INTEGRATION.md @@ -0,0 +1,455 @@ +# Telegram Identity Integration Guide +## For dApp and Bot Development Teams + +This document outlines how the **dApp** and **Bot** teams should work together to implement seamless Telegram identity verification using the Demos node APIs. + +## 🎯 **Goal: Seamless User Experience** + +Instead of users manually copying/pasting messages, we want: +1. **User clicks "Link Telegram" on dApp** +2. **Automatic coordination between dApp and bot** +3. **User signs transaction to complete** βœ… + +--- + +## πŸ—οΈ **Architecture Overview** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ dApp β”‚ β”‚ Bot β”‚ β”‚ Node β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ 1. Generate │───▢│ β”‚ β”‚ β”‚ +β”‚ Challenge β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ 2. Listen for β”‚ β”‚ β”‚ +β”‚ 3. Show Login β”‚ β”‚ User Auth β”‚ β”‚ β”‚ +β”‚ Button β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ 4. Create │───▢│ 5. Verify & β”‚ +β”‚ β”‚ β”‚ Attestation β”‚ β”‚ Create TX β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ 7. Show TX │◀───│ 6. Return TX │◀───│ β”‚ +β”‚ to Sign β”‚ β”‚ to dApp β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ 8. Submit │─────────────────────────▢│ 9. Process β”‚ +β”‚ Signed TX β”‚ β”‚ β”‚ β”‚ Identity β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 🌐 **Option 1: Telegram Login Widget (Recommended)** + +**Most Seamless Approach** - Use official Telegram Login Widget + +### **dApp Implementation**: + +```html + + +``` + +### **Flow**: +1. **User clicks Telegram Login Widget** +2. **Telegram authenticates user** (official OAuth-like flow) +3. **Telegram redirects to dApp** with auth data +4. **dApp receives**: `{ id, first_name, username, photo_url, auth_date, hash }` +5. **dApp generates challenge** via node API +6. **dApp signs challenge** with user's wallet +7. **dApp sends to bot** via Telegram Bot API (server-to-server) +8. **Bot creates attestation** and returns unsigned transaction +9. **dApp shows transaction** for user to sign +10. **User signs and submits** βœ… + +### **Advantages**: +- βœ… **Official Telegram integration** +- βœ… **No manual copy/paste** +- βœ… **Seamless UX** +- βœ… **Secure authentication** +- βœ… **Works on all devices** + +--- + +## πŸ€– **Option 2: Bot API Direct Integration** + +**Alternative Approach** - Direct bot communication + +### **Flow**: +1. **dApp generates unique session ID** +2. **dApp shows**: "Click here to verify with @DemosBot" +3. **Deep link**: `https://t.me/DemosBot?start=session_12345` +4. **Bot receives session ID** from deep link +5. **Bot calls dApp API**: "User started session_12345" +6. **dApp sends challenge** to bot for that session +7. **Bot gets user to sign** and creates attestation +8. **Bot returns unsigned transaction** to dApp +9. **dApp shows transaction** for user to sign + +### **Advantages**: +- βœ… **No OAuth complexity** +- βœ… **Direct bot control** +- βœ… **Custom flow possible** + +--- + +## πŸ“‹ **Recommended Implementation: Option 1** + +### **dApp Team Tasks**: + +#### **1. Frontend Integration** +```typescript +// telegram-auth.ts +interface TelegramAuthData { + id: number + first_name: string + username?: string + photo_url?: string + auth_date: number + hash: string +} + +async function handleTelegramAuth(authData: TelegramAuthData) { + // 1. Verify Telegram auth hash (security) + if (!verifyTelegramAuth(authData)) { + throw new Error('Invalid Telegram authentication') + } + + // 2. Generate challenge via node + const challengeResponse = await fetch('/api/tg-challenge', { + method: 'POST', + body: JSON.stringify({ demos_address: userWalletAddress }) + }) + const { challenge } = await challengeResponse.json() + + // 3. Sign challenge with user's wallet + const signedChallenge = await wallet.signMessage(challenge) + + // 4. Send to bot via Bot API (server-to-server) + const botResponse = await fetch('/api/telegram-bot-notify', { + method: 'POST', + body: JSON.stringify({ + telegramUser: authData, + signedChallenge, + sessionId: generateSessionId() + }) + }) + + // 5. Bot will process and return unsigned transaction + const { unsignedTransaction } = await botResponse.json() + + // 6. Show transaction to user for signing + showTransactionModal(unsignedTransaction) +} +``` + +#### **2. Bot Communication Endpoint** +```typescript +// pages/api/telegram-bot-notify.ts +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { telegramUser, signedChallenge, sessionId } = req.body + + // Send to bot via Telegram Bot API + const botResponse = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: telegramUser.id, + text: `Verification request received. Processing...` + }) + }) + + // Create attestation via bot + const attestation = await createBotAttestation(telegramUser, signedChallenge) + + // Get unsigned transaction from node + const nodeResponse = await fetch('https://node/api/tg-verify', { + method: 'POST', + body: JSON.stringify(attestation) + }) + + const { unsignedTransaction } = await nodeResponse.json() + + res.json({ unsignedTransaction }) +} +``` + +### **Bot Team Tasks**: + +#### **1. Bot Setup** +```python +# bot.py +import telegram +from telegram.ext import Application, CommandHandler, MessageHandler +import requests +import json + +# Bot token from @BotFather +BOT_TOKEN = "your_bot_token" +NODE_URL = "https://node.demos.network" +GENESIS_PRIVATE_KEY = "your_genesis_private_key" +GENESIS_ADDRESS = "your_genesis_address" + +bot = telegram.Bot(token=BOT_TOKEN) +``` + +#### **2. Verification Handler** +```python +async def handle_verification_request(telegram_user_id, telegram_username, signed_challenge): + """ + Handle verification request from dApp + """ + # Create attestation payload + attestation = { + 'telegram_id': str(telegram_user_id), + 'username': telegram_username or '', + 'signed_challenge': signed_challenge, + 'timestamp': int(time.time()) + } + + # Sign attestation with bot's genesis private key + attestation_json = json.dumps(attestation, sort_keys=True) + bot_signature = sign_message(attestation_json, GENESIS_PRIVATE_KEY) + + # Submit to node + payload = { + **attestation, + 'bot_address': GENESIS_ADDRESS, + 'bot_signature': bot_signature + } + + response = requests.post(f'{NODE_URL}/api/tg-verify', json=payload) + + if response.status_code == 200: + data = response.json() + return data.get('unsignedTransaction') + else: + raise Exception(f"Node verification failed: {response.text}") +``` + +#### **3. Webhook/API Integration** +```python +# For dApp to communicate with bot +@app.route('/webhook/verification', methods=['POST']) +def handle_dapp_verification(): + data = request.json + telegram_user = data['telegramUser'] + signed_challenge = data['signedChallenge'] + + try: + unsigned_tx = await handle_verification_request( + telegram_user['id'], + telegram_user.get('username'), + signed_challenge + ) + + return jsonify({ + 'success': True, + 'unsignedTransaction': unsigned_tx + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 400 +``` + +--- + +## πŸ” **Security Considerations** + +### **1. Telegram Auth Verification** +```typescript +// Verify Telegram authentication hash +function verifyTelegramAuth(authData: TelegramAuthData, botToken: string): boolean { + const { hash, ...data } = authData + + // Create data string + const dataCheckString = Object.keys(data) + .sort() + .map(key => `${key}=${data[key]}`) + .join('\n') + + // Calculate expected hash + const secretKey = crypto.createHash('sha256').update(botToken).digest() + const expectedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex') + + return expectedHash === hash +} +``` + +### **2. Session Management** +- **Unique session IDs** for each verification attempt +- **Expiration**: 15 minutes max per session +- **Rate limiting**: Max 5 attempts per user per hour +- **CSRF protection**: Validate origin and referrer + +### **3. Bot Security** +- **Genesis address validation**: Bot must own genesis private key +- **Signature verification**: All attestations cryptographically signed +- **IP allowlisting**: Only accept requests from known dApp servers +- **Webhook authentication**: Verify requests from dApp + +--- + +## 🎨 **UX Flow Design** + +### **Ideal User Experience**: + +``` +1. User on dApp β†’ Clicks "Connect Telegram" button +2. Telegram Login Widget β†’ Opens in popup/redirect +3. User authorizes β†’ Returns to dApp automatically +4. dApp shows β†’ "Signing challenge with wallet..." +5. Wallet prompts β†’ User signs challenge +6. dApp shows β†’ "Verifying with Telegram bot..." +7. Process completes β†’ "Sign transaction to finish linking" +8. User signs TX β†’ "βœ… Telegram linked successfully!" +``` + +### **Error Handling**: +- **Telegram auth fails** β†’ Clear error message + retry button +- **Challenge signing fails** β†’ Wallet-specific guidance +- **Bot verification fails** β†’ Contact support info +- **Transaction fails** β†’ Retry with gas adjustment + +--- + +## πŸ“± **Mobile Considerations** + +### **Deep Links**: +```typescript +// For mobile apps +const telegramDeepLink = `https://t.me/DemosBot?start=verify_${sessionId}` + +// Fallback for web +const telegramWebLink = `https://web.telegram.org/k/#@DemosBot?start=verify_${sessionId}` +``` + +### **Responsive Design**: +- **Mobile-first** Telegram login widget +- **Touch-friendly** buttons and interfaces +- **Proper viewport** meta tags +- **App-like transitions** between steps + +--- + +## πŸ§ͺ **Testing Strategy** + +### **Integration Testing**: +1. **dApp generates challenge** β†’ Verify API response +2. **Telegram auth simulation** β†’ Mock auth data +3. **Bot attestation creation** β†’ Test signature validity +4. **Node transaction creation** β†’ Verify transaction structure +5. **End-to-end flow** β†’ Complete user journey + +### **Security Testing**: +- **Invalid signatures** β†’ Should be rejected +- **Expired challenges** β†’ Should fail gracefully +- **Replay attacks** β†’ Should be prevented +- **Bot impersonation** β†’ Should be blocked + +--- + +## πŸš€ **Deployment Coordination** + +### **Sequence**: +1. **Node APIs** deployed and tested βœ… (Already done) +2. **Bot** deployed with webhook endpoints +3. **dApp** updated with Telegram integration +4. **Integration testing** across all components +5. **Production rollout** with monitoring + +### **Configuration**: +```env +# dApp .env +TELEGRAM_BOT_USERNAME=DemosBot +TELEGRAM_BOT_WEBHOOK_URL=https://bot.demos.network/webhook +NODE_API_URL=https://node.demos.network + +# Bot .env +BOT_TOKEN=your_telegram_bot_token +GENESIS_PRIVATE_KEY=your_genesis_private_key +GENESIS_ADDRESS=your_genesis_address +WEBHOOK_SECRET=random_secret_for_dapp_communication +DAPP_ALLOWED_ORIGINS=https://app.demos.network,https://demos.network +``` + +--- + +## πŸ“Š **Success Metrics** + +### **Technical KPIs**: +- **Completion rate** >85% (users who start finish successfully) +- **Error rate** <5% (failed verifications) +- **Response time** <3 seconds average +- **Mobile compatibility** >95% success rate + +### **User Experience KPIs**: +- **Time to complete** <60 seconds average +- **User satisfaction** >4.5/5 rating +- **Support tickets** <2% of attempts +- **Retry rate** <10% of users + +--- + +## πŸ†˜ **Troubleshooting Guide** + +### **Common Issues**: + +| Issue | Cause | Solution | +|-------|-------|----------| +| "Invalid Telegram auth" | Hash verification failed | Check bot token, verify hash calculation | +| "Challenge expired" | >15 minutes elapsed | Generate fresh challenge | +| "Unauthorized bot" | Bot not using genesis key | Verify genesis private key | +| "Transaction failed" | Invalid transaction format | Check transaction structure | +| "Wallet won't sign" | Wrong network/format | Verify wallet connection | + +### **Debug Tools**: +```typescript +// Add to dApp for debugging +const DEBUG_MODE = process.env.NODE_ENV === 'development' + +if (DEBUG_MODE) { + console.log('Challenge:', challenge) + console.log('Signed challenge:', signedChallenge) + console.log('Unsigned transaction:', unsignedTransaction) +} +``` + +--- + +## πŸ“ž **Support Contacts** + +- **Node API Issues**: Backend team +- **Bot Integration**: Bot development team +- **dApp Integration**: Frontend team +- **User Experience**: Product team + +--- + +## 🎯 **Next Steps** + +### **Immediate (Week 1)**: +1. **Bot team**: Set up Telegram bot with webhook endpoints +2. **dApp team**: Implement Telegram Login Widget +3. **Both teams**: Create communication protocol + +### **Integration (Week 2)**: +1. **Test** individual components +2. **Connect** dApp ↔ Bot communication +3. **End-to-end** testing + +### **Launch (Week 3)**: +1. **Production** deployment +2. **User** acceptance testing +3. **Monitor** and optimize + +**Total Estimated Time**: 2-3 weeks for both teams working in parallel. + +--- + +**Questions? Contact the backend team for node API details or clarification on the integration flow.** \ No newline at end of file From 2bae7712d3ef850579da815821c4e5c306672de3 Mon Sep 17 00:00:00 2001 From: TheCookingSenpai <153772003+tcsenpai@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:01:56 +0200 Subject: [PATCH 10/16] Create TG_FLOW.md --- src/libs/identity/tools/TG_FLOW.md | 186 +++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/libs/identity/tools/TG_FLOW.md diff --git a/src/libs/identity/tools/TG_FLOW.md b/src/libs/identity/tools/TG_FLOW.md new file mode 100644 index 000000000..72f03b4f5 --- /dev/null +++ b/src/libs/identity/tools/TG_FLOW.md @@ -0,0 +1,186 @@ +# Telegram Identity Verification System + +## ASCII Flow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Client β”‚ β”‚ Telegram Bot β”‚ β”‚ Demos Backend β”‚ +β”‚ β”‚ β”‚ (Authorized) β”‚ β”‚ (This Code) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ 1. Request Challenge β”‚ β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ + β”‚ β”‚ β”‚ + β”‚ 2. Challenge Responseβ”‚ β”‚ + │◄────────────────────────────────────────────── + β”‚ β”‚ β”‚ + β”‚ 3. Sign Challenge β”‚ β”‚ + β”‚ (with Demos key) β”‚ β”‚ + β”‚ β”‚ β”‚ + β”‚ 4. Send to Bot β”‚ β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ 5. Bot Verification β”‚ + β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ 6. Unsigned TX β”‚ + β”‚ │◄─────────────────────── + β”‚ β”‚ β”‚ + β”‚ 7. Return TX β”‚ β”‚ + │◄────────────────────── β”‚ + β”‚ β”‚ β”‚ + β”‚ 8. Sign & Submit TX β”‚ β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ + β”‚ β”‚ β”‚ +``` + +## Data Structures Schema + +``` +TelegramChallenge { + challenge: string // "DEMOS_TG_BIND_{address}_{timestamp}_{nonce}" + demos_address: string // User's Demos blockchain address + timestamp: number // Unix timestamp when created + used: boolean // Whether challenge has been consumed +} + +TelegramVerificationRequest { + bot_address: string // Ed25519 public key of authorized bot + telegram_id: string // Telegram user ID + username: string // Telegram username + signed_challenge: string // User-signed challenge (challenge:signature) + timestamp: number // Request timestamp + bot_signature: string // Bot's signature over attestation data +} + +TelegramVerificationResponse { + success: boolean + message: string + demosAddress?: string // User's Demos address (if successful) + telegramData?: { + userId: string + username: string + timestamp: number + } + unsignedTransaction?: Transaction // Ready for user to sign +} +``` + +## Security Model + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GENESIS BLOCK β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Authorized Bot Addresses (Ed25519 Public Keys) β”‚ β”‚ +β”‚ β”‚ - bot1_address: "abc123..." β”‚ β”‚ +β”‚ β”‚ - bot2_address: "def456..." β”‚ β”‚ +β”‚ β”‚ - bot3_address: "ghi789..." β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ VERIFICATION CHAIN β”‚ +β”‚ β”‚ +β”‚ 1. Bot Authorization Check βœ“ β”‚ +β”‚ └── Genesis block lookup β”‚ +β”‚ β”‚ +β”‚ 2. Challenge Validation βœ“ β”‚ +β”‚ └── Format + Expiry + Usage check β”‚ +β”‚ β”‚ +β”‚ 3. Bot Signature Verification βœ“ β”‚ +β”‚ └── Ed25519 verify(attestation_data, bot_signature) β”‚ +β”‚ β”‚ +β”‚ 4. User Signature Verification βœ“ β”‚ +β”‚ └── Ed25519 verify(challenge, user_signature) β”‚ +β”‚ β”‚ +β”‚ 5. Identity Transaction Creation β”‚ +β”‚ └── Unsigned transaction for blockchain submission β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Challenge Format + +``` +Challenge Structure: +"DEMOS_TG_BIND_{demos_address}_{timestamp}_{nonce}" + +Example: +"DEMOS_TG_BIND_abc123def456_1692345678_9f8e7d6c5b4a3210" + +Components: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Prefix β”‚ Demos Address β”‚ Timestamp β”‚ Random Nonce β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ DEMOS_ β”‚ User's Ed25519 β”‚ Unix time β”‚ 16-byte hex β”‚ +β”‚ TG_BIND β”‚ public key β”‚ creation β”‚ for uniqueness β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## State Management + +``` +Memory Storage (Map): +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Challenge Key β†’ Challenge Object β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ "DEMOS_TG_BIND_..." β†’ { β”‚ +β”‚ challenge: string, β”‚ +β”‚ demos_address: string, β”‚ +β”‚ timestamp: number, β”‚ +β”‚ used: boolean β”‚ +β”‚ } β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Auto-cleanup: 15 minutes TTL per challenge +Cache: Authorized bots (1 hour TTL) +``` + +--- + +## System Overview + +This code implements a **Telegram identity verification system** for the Demos blockchain network. It allows users to cryptographically bind their Telegram accounts to their Demos blockchain addresses through a secure challenge-response protocol. + +### Key Components + +**1. Challenge Generation** +- Creates unique challenges with format: `DEMOS_TG_BIND_{address}_{timestamp}_{nonce}` +- 15-minute expiration window +- One-time use only + +**2. Bot Authorization** +- Only pre-authorized bots (stored in genesis block) can perform verifications +- Bot addresses are Ed25519 public keys +- Cached for performance (1-hour TTL) + +**3. Dual Signature Verification** +- **User Signature**: User signs challenge with their Demos private key +- **Bot Signature**: Bot signs attestation data proving Telegram identity + +**4. Identity Transaction Creation** +- Generates unsigned blockchain transaction for identity binding +- Follows Web2 identity pattern used by other platforms (Twitter, etc.) +- User must sign and submit to complete the binding + +### Security Features + +- **Genesis-based Authorization**: Only pre-approved bots can verify identities +- **Challenge Uniqueness**: Cryptographic nonces prevent replay attacks +- **Dual Verification**: Both user and bot must provide valid signatures +- **Time Limits**: Challenges expire after 15 minutes +- **Single Use**: Each challenge can only be used once + +### Integration Flow + +1. User requests challenge from backend +2. User signs challenge with their Demos private key +3. User submits signed challenge to authorized Telegram bot +4. Bot verifies user's Telegram identity and signs attestation +5. Bot sends verification request to backend +6. Backend validates both signatures and bot authorization +7. Backend returns unsigned identity transaction +8. User signs and submits transaction to blockchain + +This creates a trustless bridge between Telegram identities and Demos blockchain addresses, enabling Web2-to-Web3 identity verification. From ecfb0a9d231485a7eede97d1cdfb542f31218633 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Tue, 19 Aug 2025 18:10:49 +0400 Subject: [PATCH 11/16] Refactor Telegram attestation verification to support 'validate' mode and improve challenge handling --- src/libs/abstraction/web2/telegram.ts | 4 +- src/libs/identity/tools/telegram.ts | 90 ++++++++++++++++++++------- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/libs/abstraction/web2/telegram.ts b/src/libs/abstraction/web2/telegram.ts index 6e713c620..32a59635b 100644 --- a/src/libs/abstraction/web2/telegram.ts +++ b/src/libs/abstraction/web2/telegram.ts @@ -55,7 +55,9 @@ export class TelegramProofParser extends Web2ProofParser { } // Verify the bot attestation first (this validates both signatures) - const verificationResult = await this.telegram.verifyAttestation(attestationData) + // IMPORTANT: use mode 'validate' here because interactive phase already marked challenge as used. + // We only need cryptographic verification now; reused/missing challenge should not fail. + const verificationResult = await this.telegram.verifyAttestation(attestationData, 'validate') if (!verificationResult.success) { throw new Error(`Telegram verification failed: ${verificationResult.message}`) diff --git a/src/libs/identity/tools/telegram.ts b/src/libs/identity/tools/telegram.ts index fc021fb15..5a7d9299a 100644 --- a/src/libs/identity/tools/telegram.ts +++ b/src/libs/identity/tools/telegram.ts @@ -7,7 +7,15 @@ import { TelegramChallengeResponse, Transaction, } from "@kynesyslabs/demosdk/types" -import { InferFromTelegramPayload } from "@kynesyslabs/demosdk/abstraction" +// NOTE: The SDK currently has no exported InferFromTelegramPayload type (compile error showed only Twitter variant). +// Define a minimal local interface mirroring expected web2 identity payload shape for Telegram. +interface TelegramWeb2Payload { + context: 'telegram' + proof: string // bot attestation JSON string + username: string + userId: string +} + import { ucrypto, hexToUint8Array } from "@kynesyslabs/demosdk/encryption" /** @@ -60,8 +68,27 @@ export default class Telegram { log.error("Genesis block not found or has no content") return [] } - - const genesisData = JSON.parse(genesisBlock.content) + // genesisBlock.content is a JSON object (typeorm column json), NOT a string. + // The original genesis data we need (balances) is embedded as string in content.extra.genesisData. + let genesisData: any + if (typeof genesisBlock.content === "string") { + // (unlikely) stored as string JSON + genesisData = JSON.parse(genesisBlock.content) + } else if ( + typeof genesisBlock.content === "object" && + genesisBlock.content?.extra?.genesisData + ) { + // extra.genesisData is a JSON string created in generateGenesisBlock + try { + genesisData = JSON.parse(genesisBlock.content.extra.genesisData) + } catch (e) { + log.error("Failed to parse embedded genesisData string:" + e) + return [] + } + } else { + // Fallback: maybe balances directly present + genesisData = genesisBlock.content + } // Extract addresses from balances array this.authorizedBots = genesisData.balances?.map((balance: [string, string]) => @@ -122,19 +149,21 @@ export default class Telegram { nonce: string } | null { const parts = challenge.split("_") - if (parts.length !== 5 || parts[0] !== "DEMOS" || parts[1] !== "TG" || parts[2] !== "BIND") { + // Expected format: DEMOS_TG_BIND___ + if (parts.length !== 6 || parts[0] !== "DEMOS" || parts[1] !== "TG" || parts[2] !== "BIND") { return null } - + const demosAddress = parts[3] const timestamp = parseInt(parts[4]) + const nonce = parts[5] if (isNaN(timestamp)) { return null } return { - demosAddress: parts[3], + demosAddress, timestamp, - nonce: parts[5], + nonce, } } @@ -142,7 +171,16 @@ export default class Telegram { * Verify a Telegram verification request from a bot * This includes both bot signature verification and user signature verification */ - async verifyAttestation(request: TelegramVerificationRequest): Promise { + /** + * Verify a Telegram attestation. + * mode = 'attest' : interactive phase (bot hits /api/tg-verify). Challenge must exist & be unused; we then mark it used. + * mode = 'validate': on-chain validation phase (parser replays verification). We ALLOW reused / missing (expired GC) challenge + * while still verifying both signatures cryptographically. This prevents false negatives when broadcasting. + */ + async verifyAttestation( + request: TelegramVerificationRequest, + mode: 'attest' | 'validate' = 'attest', + ): Promise { try { // 1. Check if bot address is authorized (from genesis) if (!(await this.isAuthorizedBot(request.bot_address))) { @@ -162,19 +200,20 @@ export default class Telegram { } } - // 3. Check if challenge exists and is not expired/used - const storedChallenge = this.challenges.get(`DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}`) - if (!storedChallenge) { - return { - success: false, - message: "Challenge not found or expired", + // 3. Fetch stored challenge (in-memory). For 'validate' we tolerate absence or reuse. + const storedChallenge = this.challenges.get( + `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}`, + ) + if (!storedChallenge && mode === 'attest') { + return { + success: false, + message: 'Challenge not found or expired', } } - - if (storedChallenge.used) { - return { - success: false, - message: "Challenge already used", + if (storedChallenge?.used && mode === 'attest') { + return { + success: false, + message: 'Challenge already used', } } @@ -227,8 +266,10 @@ export default class Telegram { } } - // 6. Mark challenge as used - storedChallenge.used = true + // 6. Mark challenge as used only during interactive attestation + if (storedChallenge && mode === 'attest') { + storedChallenge.used = true + } // 7. Create unsigned identity transaction following Twitter pattern const unsignedTransaction = this.createIdentityTransaction( @@ -293,7 +334,7 @@ export default class Telegram { // REVIEW: Create transaction following the exact Twitter pattern // See DemosTransactions.empty() and Identities.inferWeb2Identity() - const telegramPayload: InferFromTelegramPayload = { + const telegramPayload: TelegramWeb2Payload = { context: "telegram", proof: proofData, // Bot attestation containing all verification data username: username, @@ -306,8 +347,10 @@ export default class Telegram { content: { type: "identity", from_ed25519_address: demosAddress, + from: demosAddress, // required by TransactionContent to: demosAddress, // Identity transactions are self-directed amount: 0, // No tokens transferred for identity binding + transaction_fee: { network_fee: 0, rpc_fee: 0, additional_fee: 0 }, data: [ "identity", // Transaction data type identifier { @@ -321,6 +364,9 @@ export default class Telegram { gcr_edits: [], // Will be generated during transaction validation }, signature: null, // User must sign this + ed25519_signature: null as any, + status: "pending" as any, + blockNumber: 0, } log.info(`Created unsigned Telegram identity transaction for ${demosAddress} ↔ ${telegramId}`) From 7986fc02d3a7449dddf2390888f579c45de8801f Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Wed, 20 Aug 2025 15:47:56 +0400 Subject: [PATCH 12/16] Enhance Telegram identity verification with transaction challenge hash for replay protection --- src/libs/abstraction/index.ts | 3 + src/libs/abstraction/web2/telegram.ts | 20 +++- src/libs/identity/tools/telegram.ts | 96 ++++++++++++++----- .../transactions/handleIdentityRequest.ts | 1 + 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index 57e6db78c..6e4e47838 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -5,6 +5,7 @@ import { type Web2ProofParser } from "./web2/parsers" import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction" import { hexToUint8Array, ucrypto } from "@kynesyslabs/demosdk/encryption" import { Twitter } from "../identity/tools/twitter" +import { Transaction } from "@kynesyslabs/demosdk/types" /** * Fetches the proof data using the appropriate parser and verifies the signature @@ -27,6 +28,7 @@ import { Twitter } from "../identity/tools/twitter" export async function verifyWeb2Proof( payload: Web2CoreTargetIdentityPayload, sender: string, + transaction?: Transaction, ) { let parser: typeof TwitterProofParser | typeof GithubProofParser | typeof TelegramProofParser @@ -75,6 +77,7 @@ export async function verifyWeb2Proof( try { const { message, type, signature } = await instance.readData( payload.proof, + payload.context === "telegram" ? transaction : undefined, ) try { const verified = await ucrypto.verify({ diff --git a/src/libs/abstraction/web2/telegram.ts b/src/libs/abstraction/web2/telegram.ts index 32a59635b..76a696330 100644 --- a/src/libs/abstraction/web2/telegram.ts +++ b/src/libs/abstraction/web2/telegram.ts @@ -1,6 +1,6 @@ import { Web2ProofParser } from "./parsers" import Telegram from "@/libs/identity/tools/telegram" -import { SigningAlgorithm } from "@kynesyslabs/demosdk/types" +import { SigningAlgorithm, Transaction } from "@kynesyslabs/demosdk/types" /** * TelegramProofParser - Parses and validates Telegram identity proofs @@ -34,9 +34,10 @@ export class TelegramProofParser extends Web2ProofParser { * - Bot's signature of the attestation * * @param proofData - JSON string containing bot attestation data + * @param transaction - The transaction containing this proof (for challenge hash extraction) * @returns Parsed signature data for verification */ - async readData(proofData: string): Promise<{ + async readData(proofData: string, transaction?: Transaction): Promise<{ message: string signature: string type: SigningAlgorithm @@ -54,10 +55,19 @@ export class TelegramProofParser extends Web2ProofParser { } } + // Extract challenge hash from transaction for replay protection validation + const transactionChallengeHash = transaction + ? Telegram.extractChallengeHashFromTransaction(transaction) + : null + // Verify the bot attestation first (this validates both signatures) - // IMPORTANT: use mode 'validate' here because interactive phase already marked challenge as used. - // We only need cryptographic verification now; reused/missing challenge should not fail. - const verificationResult = await this.telegram.verifyAttestation(attestationData, 'validate') + // IMPORTANT: Use 'validate' mode for on-chain validation + // This mode performs cryptographic verification and replay protection when transaction hash is available + const verificationResult = await this.telegram.verifyAttestation( + attestationData, + 'validate', + transactionChallengeHash + ) if (!verificationResult.success) { throw new Error(`Telegram verification failed: ${verificationResult.message}`) diff --git a/src/libs/identity/tools/telegram.ts b/src/libs/identity/tools/telegram.ts index 5a7d9299a..d798ac28d 100644 --- a/src/libs/identity/tools/telegram.ts +++ b/src/libs/identity/tools/telegram.ts @@ -14,6 +14,7 @@ interface TelegramWeb2Payload { proof: string // bot attestation JSON string username: string userId: string + attestation_id?: string // Challenge hash for replay attack prevention } import { ucrypto, hexToUint8Array } from "@kynesyslabs/demosdk/encryption" @@ -174,12 +175,13 @@ export default class Telegram { /** * Verify a Telegram attestation. * mode = 'attest' : interactive phase (bot hits /api/tg-verify). Challenge must exist & be unused; we then mark it used. - * mode = 'validate': on-chain validation phase (parser replays verification). We ALLOW reused / missing (expired GC) challenge - * while still verifying both signatures cryptographically. This prevents false negatives when broadcasting. + * mode = 'validate': on-chain validation phase (parser replays verification). We verify challenge reuse protection + * by checking the challenge hash embedded in the transaction payload. */ async verifyAttestation( request: TelegramVerificationRequest, mode: 'attest' | 'validate' = 'attest', + transactionChallengeHash?: string, // For validate mode: hash from transaction payload ): Promise { try { // 1. Check if bot address is authorized (from genesis) @@ -192,7 +194,9 @@ export default class Telegram { } // 2. Parse the challenge from user's signed message - const challengeData = this.parseChallenge(request.signed_challenge.split(":")[0] || request.signed_challenge) + const challengeInput = request.signed_challenge.split(":")[0] || request.signed_challenge + + const challengeData = this.parseChallenge(challengeInput) if (!challengeData) { return { success: false, @@ -200,24 +204,46 @@ export default class Telegram { } } - // 3. Fetch stored challenge (in-memory). For 'validate' we tolerate absence or reuse. - const storedChallenge = this.challenges.get( - `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}`, - ) - if (!storedChallenge && mode === 'attest') { - return { - success: false, - message: 'Challenge not found or expired', + // 3. Calculate challenge hash for replay protection + const originalChallenge = `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}` + const challengeHash = crypto.createHash('sha256').update(originalChallenge).digest('hex') + + // 4. Validate challenge reuse protection based on mode + const storedChallenge = this.challenges.get(originalChallenge) + + if (mode === 'attest') { + // Interactive mode: strict challenge validation + if (!storedChallenge) { + return { + success: false, + message: 'Challenge not found or expired', + } } - } - if (storedChallenge?.used && mode === 'attest') { - return { - success: false, - message: 'Challenge already used', + if (storedChallenge.used) { + return { + success: false, + message: 'Challenge already used', + } + } + } else if (mode === 'validate') { + // If no transaction challenge hash is provided, reject the validation (no legacy txs expected) + if (!transactionChallengeHash) { + return { + success: false, + message: 'Transaction challenge hash missing - replay protection failed', + } + } else { + // Perform full replay protection validation + if (challengeHash !== transactionChallengeHash) { + return { + success: false, + message: 'Challenge hash mismatch - potential replay attack detected', + } + } } } - // 4. Verify bot signature + // 5. Verify bot signature const attestationData = { telegram_id: request.telegram_id, username: request.username, @@ -242,8 +268,7 @@ export default class Telegram { } } - // 5. Verify user signature against the original challenge - const originalChallenge = `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}` + // 6. Verify user signature against the original challenge const challengeMessage = new TextEncoder().encode(originalChallenge) // Extract signature from signed challenge (assuming format: "challenge:signature" or just signature) @@ -266,12 +291,12 @@ export default class Telegram { } } - // 6. Mark challenge as used only during interactive attestation + // 7. Mark challenge as used only during interactive attestation if (storedChallenge && mode === 'attest') { storedChallenge.used = true } - // 7. Create unsigned identity transaction following Twitter pattern + // 8. Create unsigned identity transaction with challenge hash for replay protection const unsignedTransaction = this.createIdentityTransaction( challengeData.demosAddress, request.telegram_id, @@ -284,6 +309,7 @@ export default class Telegram { bot_address: request.bot_address, bot_signature: request.bot_signature, }), + challengeHash, // Add challenge hash to prevent replay attacks ) // 8. Return success with unsigned transaction for user to sign @@ -318,11 +344,13 @@ export default class Telegram { * - Context: "web2" * - Method: "web2_identity_assign" * - Payload contains Telegram identity data and bot attestation proof + * - Challenge hash embedded for replay attack prevention * * @param demosAddress - User's Demos address * @param telegramId - Telegram user ID * @param username - Telegram username * @param proofData - JSON string containing bot attestation + * @param challengeHash - SHA256 hash of the original challenge for replay protection * @returns Unsigned transaction ready for user signature */ private createIdentityTransaction( @@ -330,15 +358,14 @@ export default class Telegram { telegramId: string, username: string, proofData: string, + challengeHash: string, ): Transaction { - // REVIEW: Create transaction following the exact Twitter pattern - // See DemosTransactions.empty() and Identities.inferWeb2Identity() - const telegramPayload: TelegramWeb2Payload = { context: "telegram", proof: proofData, // Bot attestation containing all verification data username: username, userId: telegramId, + attestation_id: challengeHash, // Challenge hash for replay attack prevention } // Create transaction skeleton (same structure as Twitter identity transactions) @@ -373,6 +400,27 @@ export default class Telegram { return transaction } + /** + * Extract challenge hash from a transaction payload + * Used during validation mode to verify replay protection + */ + static extractChallengeHashFromTransaction(transaction: Transaction): string | null { + try { + if ( + transaction && + transaction.content && + Array.isArray(transaction.content.data) && + transaction.content.data[0] === "identity" && + (transaction.content.data[1] as { payload?: TelegramWeb2Payload })?.payload?.attestation_id + ) { + return (transaction.content.data[1] as { payload: TelegramWeb2Payload }).payload.attestation_id || null; + } + return null; + } catch { + return null; + } + } + /** * Get statistics about current challenges (for debugging/monitoring) */ diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index c0c78a0fc..a523af2c2 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -81,6 +81,7 @@ export default async function handleIdentityRequest( return await verifyWeb2Proof( payload.payload as Web2CoreTargetIdentityPayload, sender, + tx, // Pass the transaction for Telegram challenge hash validation ) case "xm_identity_remove": case "pqc_identity_remove": From 4d34422bcc0fa1a77115bcf4dc3d613d1bfa1020 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Thu, 21 Aug 2025 11:15:22 +0400 Subject: [PATCH 13/16] Enhance Telegram identity verification with anti-replay protection features, including challenge hash embedding and validation mode security --- TG_INTEGRATION.md | 39 +++++++++++++++++++++++++++--- src/libs/identity/tools/TG_FLOW.md | 28 ++++++++++++++++----- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/TG_INTEGRATION.md b/TG_INTEGRATION.md index 51303e84f..a3030a898 100644 --- a/TG_INTEGRATION.md +++ b/TG_INTEGRATION.md @@ -3,6 +3,12 @@ This document outlines how the **dApp** and **Bot** teams should work together to implement seamless Telegram identity verification using the Demos node APIs. +## πŸ”’ **Security Update: Anti-Replay Protection** +**NEW**: The system now includes enhanced security features to prevent replay attacks: +- **Challenge Hash Embedding**: SHA256 hash of challenges embedded in transactions +- **Validation Mode Security**: On-chain validation requires matching challenge hash +- **Replay Attack Prevention**: Old attestations cannot be reused even with valid bot signatures + ## 🎯 **Goal: Seamless User Experience** Instead of users manually copying/pasting messages, we want: @@ -200,6 +206,7 @@ bot = telegram.Bot(token=BOT_TOKEN) async def handle_verification_request(telegram_user_id, telegram_username, signed_challenge): """ Handle verification request from dApp + Enhanced with anti-replay protection via challenge hash embedding """ # Create attestation payload attestation = { @@ -213,7 +220,7 @@ async def handle_verification_request(telegram_user_id, telegram_username, signe attestation_json = json.dumps(attestation, sort_keys=True) bot_signature = sign_message(attestation_json, GENESIS_PRIVATE_KEY) - # Submit to node + # Submit to node (node will generate challenge hash automatically) payload = { **attestation, 'bot_address': GENESIS_ADDRESS, @@ -224,6 +231,7 @@ async def handle_verification_request(telegram_user_id, telegram_username, signe if response.status_code == 200: data = response.json() + # Transaction now includes embedded challenge hash for replay protection return data.get('unsignedTransaction') else: raise Exception(f"Node verification failed: {response.text}") @@ -281,15 +289,34 @@ function verifyTelegramAuth(authData: TelegramAuthData, botToken: string): boole } ``` -### **2. Session Management** +### **2. Anti-Replay Protection** +```typescript +// Challenge hash generation for replay protection +function generateChallengeHash(challenge: string): string { + return crypto.createHash('sha256').update(challenge).digest('hex') +} + +// Transaction payload must include challenge hash +interface TelegramTransactionPayload { + context: 'telegram' + proof: string + username: string + userId: string + attestation_id: string // SHA256 hash of original challenge +} +``` + +### **3. Session Management** - **Unique session IDs** for each verification attempt - **Expiration**: 15 minutes max per session - **Rate limiting**: Max 5 attempts per user per hour - **CSRF protection**: Validate origin and referrer -### **3. Bot Security** +### **4. Bot Security** - **Genesis address validation**: Bot must own genesis private key - **Signature verification**: All attestations cryptographically signed +- **Challenge hash embedding**: All transactions include SHA256 challenge hash +- **Replay attack prevention**: On-chain validation requires matching challenge hash - **IP allowlisting**: Only accept requests from known dApp servers - **Webhook authentication**: Verify requests from dApp @@ -404,8 +431,9 @@ DAPP_ALLOWED_ORIGINS=https://app.demos.network,https://demos.network |-------|-------|----------| | "Invalid Telegram auth" | Hash verification failed | Check bot token, verify hash calculation | | "Challenge expired" | >15 minutes elapsed | Generate fresh challenge | +| "Challenge hash mismatch" | Replay attack detected | Ensure transaction includes correct challenge hash | | "Unauthorized bot" | Bot not using genesis key | Verify genesis private key | -| "Transaction failed" | Invalid transaction format | Check transaction structure | +| "Transaction failed" | Invalid transaction format | Check transaction structure and attestation_id | | "Wallet won't sign" | Wrong network/format | Verify wallet connection | ### **Debug Tools**: @@ -415,8 +443,11 @@ const DEBUG_MODE = process.env.NODE_ENV === 'development' if (DEBUG_MODE) { console.log('Challenge:', challenge) + console.log('Challenge Hash:', generateChallengeHash(challenge)) console.log('Signed challenge:', signedChallenge) console.log('Unsigned transaction:', unsignedTransaction) + console.log('Transaction attestation_id:', unsignedTransaction.content.data[1].attestation_id) +} } ``` diff --git a/src/libs/identity/tools/TG_FLOW.md b/src/libs/identity/tools/TG_FLOW.md index 72f03b4f5..f44bf9601 100644 --- a/src/libs/identity/tools/TG_FLOW.md +++ b/src/libs/identity/tools/TG_FLOW.md @@ -53,6 +53,14 @@ TelegramVerificationRequest { bot_signature: string // Bot's signature over attestation data } +TelegramWeb2Payload { + context: string // "telegram" + proof: string // Bot attestation JSON string + username: string // Telegram username + userId: string // Telegram user ID + attestation_id: string // SHA256 hash of challenge for replay protection +} + TelegramVerificationResponse { success: boolean message: string @@ -89,13 +97,16 @@ TelegramVerificationResponse { β”‚ 2. Challenge Validation βœ“ β”‚ β”‚ └── Format + Expiry + Usage check β”‚ β”‚ β”‚ -β”‚ 3. Bot Signature Verification βœ“ β”‚ +β”‚ 3. Challenge Hash Anti-Replay Protection βœ“ β”‚ +β”‚ └── SHA256(challenge) embedded in transaction β”‚ +β”‚ β”‚ +β”‚ 4. Bot Signature Verification βœ“ β”‚ β”‚ └── Ed25519 verify(attestation_data, bot_signature) β”‚ β”‚ β”‚ -β”‚ 4. User Signature Verification βœ“ β”‚ +β”‚ 5. User Signature Verification βœ“ β”‚ β”‚ └── Ed25519 verify(challenge, user_signature) β”‚ β”‚ β”‚ -β”‚ 5. Identity Transaction Creation β”‚ +β”‚ 6. Identity Transaction Creation β”‚ β”‚ └── Unsigned transaction for blockchain submission β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -168,9 +179,12 @@ This code implements a **Telegram identity verification system** for the Demos b - **Genesis-based Authorization**: Only pre-approved bots can verify identities - **Challenge Uniqueness**: Cryptographic nonces prevent replay attacks +- **Challenge Hash Binding**: SHA256 challenge hash embedded in transactions +- **Anti-Replay Protection**: Transaction validation requires matching challenge hash - **Dual Verification**: Both user and bot must provide valid signatures - **Time Limits**: Challenges expire after 15 minutes - **Single Use**: Each challenge can only be used once +- **Validate Mode Security**: On-chain validation prevents replay of old attestations ### Integration Flow @@ -180,7 +194,9 @@ This code implements a **Telegram identity verification system** for the Demos b 4. Bot verifies user's Telegram identity and signs attestation 5. Bot sends verification request to backend 6. Backend validates both signatures and bot authorization -7. Backend returns unsigned identity transaction -8. User signs and submits transaction to blockchain +7. Backend generates challenge hash (SHA256) for replay protection +8. Backend returns unsigned identity transaction with embedded challenge hash +9. User signs and submits transaction to blockchain +10. On-chain validation verifies challenge hash matches transaction payload -This creates a trustless bridge between Telegram identities and Demos blockchain addresses, enabling Web2-to-Web3 identity verification. +This creates a trustless bridge between Telegram identities and Demos blockchain addresses, with cryptographic protection against replay attacks. From 0f9d053177244d8e38478030225dbf889562ae87 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Tue, 2 Sep 2025 10:36:56 +0400 Subject: [PATCH 14/16] Add Telegram account linking and points management to PointSystem and IncentiveManager --- src/features/incentive/PointSystem.ts | 137 +++++++++++++++++- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 24 ++- .../gcr/gcr_routines/IncentiveManager.ts | 2 - 3 files changed, 155 insertions(+), 8 deletions(-) diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 7ea434d5a..9a7797659 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -12,6 +12,7 @@ import { Twitter } from "@/libs/identity/tools/twitter" const pointValues = { LINK_WEB3_WALLET: 0.5, LINK_TWITTER: 2, + LINK_TELEGRAM: 2, FOLLOW_DEMOS: 1, } @@ -32,13 +33,17 @@ export class PointSystem { */ private async getUserIdentitiesFromGCR(userId: string): Promise<{ linkedWallets: string[] - linkedSocials: { twitter?: string } + linkedSocials: { twitter?: string; telegram?: string } }> { const xmIdentities = await IdentityManager.getIdentities(userId) const twitterIdentities = await IdentityManager.getWeb2Identities( userId, "twitter", ) + const telegramIdentities = await IdentityManager.getWeb2Identities( + userId, + "telegram", + ) const linkedWallets: string[] = [] @@ -62,12 +67,16 @@ export class PointSystem { } } - const linkedSocials: { twitter?: string } = {} + const linkedSocials: { twitter?: string; telegram?: string } = {} if (Array.isArray(twitterIdentities) && twitterIdentities.length > 0) { linkedSocials.twitter = twitterIdentities[0].username } + if (Array.isArray(telegramIdentities) && telegramIdentities.length > 0) { + linkedSocials.telegram = telegramIdentities[0].username + } + return { linkedWallets, linkedSocials } } @@ -193,7 +202,8 @@ export class PointSystem { type === "socialAccounts" && (platform === "twitter" || platform === "github" || - platform === "discord") + platform === "discord" || + platform === "telegram") ) { const oldPlatformPoints = account.points.breakdown?.socialAccounts?.[platform] || 0 @@ -524,4 +534,125 @@ export class PointSystem { } } } + + /** + * Award points for linking a Telegram account + * @param userId The user's Demos address + * @param referralCode Optional referral code + * @returns RPCResponse + */ + async awardTelegramPoints( + userId: string, + referralCode?: string, + ): Promise { + try { + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + // Check if user already has Telegram points specifically + if ((userPointsWithIdentities.breakdown.socialAccounts as any).telegram > 0) { + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPointsWithIdentities.totalPoints, + message: "Telegram points already awarded", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + pointValues.LINK_TELEGRAM, + "socialAccounts", + "telegram", + referralCode, + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_TELEGRAM, + totalPoints: updatedPoints.totalPoints, + message: "Points awarded for linking Telegram", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + + /** + * Deduct points for unlinking a Telegram account + * @param userId The user's Demos address + * @returns RPCResponse + */ + async deductTelegramPoints(userId: string): Promise { + try { + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + // Check if user has Telegram points to deduct + if ( + ((userPointsWithIdentities.breakdown.socialAccounts as any).telegram || 0) <= 0 + ) { + return { + result: 200, + response: { + pointsDeducted: 0, + totalPoints: userPointsWithIdentities.totalPoints, + message: "No Telegram points to deduct", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + -pointValues.LINK_TELEGRAM, + "socialAccounts", + "telegram", + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsDeducted: pointValues.LINK_TELEGRAM, + totalPoints: updatedPoints.totalPoints, + message: "Points deducted for unlinking Telegram", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error deducting points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } } diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index adf535e79..618d91198 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -244,7 +244,6 @@ export default class GCRIdentityRoutines { if (isFirst) { await IncentiveManager.telegramLinked( editOperation.account, - data.userId, editOperation.referralCode, ) } @@ -523,9 +522,9 @@ export default class GCRIdentityRoutines { } private static async isFirstConnection( - type: "twitter" | "web3", + type: "twitter" | "web3" | "telegram", data: { - userId?: string // for twitter + userId?: string // for twitter and telegram chain?: string // for web3 subchain?: string // for web3 address?: string // for web3 @@ -548,6 +547,25 @@ export default class GCRIdentityRoutines { .andWhere("gcr.pubkey != :currentAccount", { currentAccount }) .getOne() + /** + * Return true if no account has this userId + */ + return !result + } else if (type === "telegram") { + /** + * Check if this Telegram userId exists anywhere + */ + const result = await gcrMainRepository + .createQueryBuilder("gcr") + .where( + "EXISTS (SELECT 1 FROM jsonb_array_elements(gcr.identities->'web2'->'telegram') as telegram_id WHERE telegram_id->>'userId' = :userId)", + { + userId: data.userId, + }, + ) + .andWhere("gcr.pubkey != :currentAccount", { currentAccount }) + .getOne() + /** * Return true if no account has this userId */ diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts index ab5a8a872..b2e2a3785 100644 --- a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts @@ -67,12 +67,10 @@ export class IncentiveManager { */ static async telegramLinked( userId: string, - telegramUserId: string, referralCode?: string, ): Promise { return await this.pointSystem.awardTelegramPoints( userId, - telegramUserId, referralCode, ) } From 33c059d454bcb902ee73484a4c79eba45ce1cba1 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Wed, 3 Sep 2025 10:17:37 +0400 Subject: [PATCH 15/16] fix: update LINK_TELEGRAM point value from 2 to 1 --- src/features/incentive/PointSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 9a7797659..5010f95df 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -12,7 +12,7 @@ import { Twitter } from "@/libs/identity/tools/twitter" const pointValues = { LINK_WEB3_WALLET: 0.5, LINK_TWITTER: 2, - LINK_TELEGRAM: 2, + LINK_TELEGRAM: 1, FOLLOW_DEMOS: 1, } From 4614c3e37e5903b33d9bb9fe4bae52298df3c7dd Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Wed, 3 Sep 2025 10:22:01 +0400 Subject: [PATCH 16/16] fix: update LINK_TWITTER point value from 2 to 1 --- src/features/incentive/PointSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 5010f95df..a0c861945 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -11,7 +11,7 @@ import { Twitter } from "@/libs/identity/tools/twitter" const pointValues = { LINK_WEB3_WALLET: 0.5, - LINK_TWITTER: 2, + LINK_TWITTER: 1, LINK_TELEGRAM: 1, FOLLOW_DEMOS: 1, }