-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathTonConnectServerV1.ts
129 lines (102 loc) · 4.04 KB
/
TonConnectServerV1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import nacl from 'tweetnacl';
import TonWeb from 'tonweb';
import { CreateRequestOptions, DecodedResponse, SessionData, TonOwnershipPayload } from './TonConnectServer.types';
import { Base64, bytesToString, getTimeSec, concatBytes, extractBytes, stringToBytes } from './utils';
import { TonConnectError } from './TonConnectError';
type TonConnectServerOptions = {
staticSecret: string;
}
export class TonConnectServerV1 {
public protocol = 'ton-auth';
public version = 'v1';
private sessionExpirationSec = 5 * 60;
private staticSk: Uint8Array;
constructor(options: TonConnectServerOptions) {
this.staticSk = Base64.decodeBytes(options.staticSecret);
}
public createRequest(options: CreateRequestOptions, sessionData: SessionData = {}) {
const session = nacl.box.keyPair();
const sessionPayload = this.packSessionPayload(session.secretKey, sessionData);
return {
protocol: this.protocol,
[this.version]: {
session: Base64.encodeBytes(session.publicKey),
session_payload: sessionPayload,
...options
}
};
}
public async verifyTonOwnership(payload: TonOwnershipPayload, client_id: string) {
const TonWallet = TonWeb.Wallets.all[payload.wallet_version];
if (TonWallet) {
// Construct wallet contract
const publicKey = Base64.decodeBytes(payload.pubkey)
const wc = new TonWeb.Address(payload.address).wc;
const wallet = new TonWallet({}, { publicKey, wc });
const contractAddress = await wallet.getAddress();
const friendlyAddress = contractAddress.toString(true, true, true);
const isAddressMatched = friendlyAddress === payload.address;
// Verify the signature
const message = `tonlogin/ownership/${payload.wallet_version}/${payload.address}/${client_id}`;
const signature = Base64.decodeBytes(payload.signature);
const isSignatureVerified = nacl.sign.detached.verify(
stringToBytes(message),
signature,
publicKey
);
if (isAddressMatched && isSignatureVerified) {
return true;
}
}
return false;
}
public decodeResponse(base64: string): DecodedResponse {
const response = Base64.decodeUrlSafeObj(base64);
const authenticator = Base64.decodeBytes(response.authenticator);
const clientId = Base64.decodeBytes(response.client_id);
const nonce = Base64.decodeBytes(response.nonce);
const session = this.unpackSessionPayload(response.session_payload);
const payloadBytes = nacl.box.open(authenticator, nonce, clientId, session.sk);
if (!payloadBytes) {
throw new TonConnectError('Payload decoding failed');
}
const payload = JSON.parse(bytesToString(payloadBytes));
return {
client_id: response.client_id,
sessionData: session.data,
payload,
};
}
private packSessionPayload(sessionSk: Uint8Array, data: SessionData) {
const exp = Math.floor(getTimeSec() + this.sessionExpirationSec);
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const sk = Base64.encodeBytes(sessionSk);
const payload = JSON.stringify({
tonconnect: { exp, sk },
data
});
const box = nacl.secretbox(stringToBytes(payload), nonce, this.staticSk);
return Base64.encodeBytes(concatBytes([nonce, box]));
}
private unpackSessionPayload(base64: string) {
const bytes = Base64.decodeBytes(base64);
const nonceLength = nacl.secretbox.nonceLength;
const nonce = extractBytes(bytes, 0, nonceLength);
const box = extractBytes(bytes, nonceLength);
const payloadBytes = nacl.secretbox.open(box, nonce, this.staticSk);
if (!payloadBytes) {
throw new TonConnectError('Failed unpack session payload');
}
const payload = JSON.parse(bytesToString(payloadBytes));
if (!payload.tonconnect) {
throw new TonConnectError('Invalid session payload');
}
if (payload.tonconnect.exp < getTimeSec()) {
throw new TonConnectError('Session expired');
}
return {
sk: Base64.decodeBytes(payload.tonconnect.sk),
data: payload.data
};
}
}