Зафиксировать все текущие изменения проекта

This commit is contained in:
AidarKC 2026-06-04 22:27:09 +04:00
parent 624557ebfd
commit 60049442f1
9 changed files with 1526 additions and 182 deletions

View File

@ -9,6 +9,7 @@
- `audio` — динамик/аудио-кодек (пример `07_ES8311`)
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
Запуск:

View File

@ -0,0 +1,943 @@
/*
* argon2_sd_test.ino v2
* Генерация masterSecret (Argon2id) + 3 Ed25519 ключевых пары.
* Результат сохраняется в NVS (внутренняя flash ESP32).
*
* Алгоритм совпадает с JS crypto-utils.js:
* salt = SHA256("shine-auth-v2|login=<login>|suffix=master.secret")[0:16]
* passBytes = UTF8("<login>\n<password>")
* secret = Argon2id(passBytes, salt, t=2, m=65536 KB, p=1, dkLen=32)
* keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
* suffixes = ["root.key", "bch.key", "dev.key"]
*
* Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
* SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3
*/
#include <Arduino.h>
#include <Wire.h>
#include <string.h>
#include <stdint.h>
#include "argon2_types.h"
#include "driver/gpio.h"
#include "Arduino_GFX_Library.h"
#include "TouchDrvCSTXXX.hpp"
#include "SD_MMC.h"
#include "mbedtls/sha256.h"
#include "mbedtls/base64.h"
#include <Ed25519.h>
#include <Preferences.h>
// ═══════════════════════════════════════════════════════════
// PINS
// ═══════════════════════════════════════════════════════════
#define PIN_LCD_CS 12
#define PIN_LCD_SCLK 38
#define PIN_LCD_D0 4
#define PIN_LCD_D1 5
#define PIN_LCD_D2 6
#define PIN_LCD_D3 7
#define PIN_LCD_RST 2
#define PIN_I2C_SDA 15
#define PIN_I2C_SCL 14
#define PIN_TP_INT 11
#define PIN_SD_CLK 2
#define PIN_SD_CMD 1
#define PIN_SD_D0 3
// ═══════════════════════════════════════════════════════════
// DISPLAY
// ═══════════════════════════════════════════════════════════
#define DISP_W 480
#define DISP_H 480
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
PIN_LCD_CS, PIN_LCD_SCLK,
PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
// ═══════════════════════════════════════════════════════════
// TOUCH (полинг + прерывание)
// ═══════════════════════════════════════════════════════════
TouchDrvCST92xx gTouch;
volatile bool gTouchIrq = false;
void IRAM_ATTR onTouchIrq() { gTouchIrq = true; }
// Состояние свайпа
static bool gTouching = false;
static int16_t gTouchStartY = 0;
static int16_t gTouchPrevY = 0;
static int16_t gTouchCurY = 0;
static int16_t gTouchCurX = 0;
// ═══════════════════════════════════════════════════════════
// COLORS
// ═══════════════════════════════════════════════════════════
#define C_BG 0x0000u
#define C_KEY 0x2104u
#define C_KEY_HL 0x4208u
#define C_TEXT 0xFFFFu
#define C_HINT 0x7BEFu
#define C_GREEN 0x07E0u
#define C_RED 0xF800u
#define C_FIELD 0x18C3u
#define C_BAR_BG 0x18C3u
#define C_BAR_FG 0xFFFFu
#define C_SEP 0x2945u
#define C_ACCENT 0x05FFu
// ═══════════════════════════════════════════════════════════
// APP STATE
// ═══════════════════════════════════════════════════════════
enum AppState {
ST_LOGIN, // ввод логина
ST_PASS, // ввод пароля
ST_RUNNING, // вычисление
ST_DONE, // результат (только что посчитан)
ST_SAVED, // результат (загружен из NVS)
ST_CONFIRM, // диалог "сгенерировать новый?"
ST_ERROR
};
static AppState gState = ST_LOGIN;
static char gLogin[64] = {};
static char gPass[64] = {};
static uint8_t gSecret[32];
static char gSecretHex[65];
static bool gKbNums = false;
// ═══════════════════════════════════════════════════════════
// КЛЮЧЕВЫЕ ПАРЫ
// ═══════════════════════════════════════════════════════════
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
static KeyPair gKeys[3];
static const char * KEY_SUFFIXES[3] = {"root.key", "bch.key", "dev.key"};
static const char * KEY_LABELS[3] = {"root.key", "bch.key", "dev.key"};
static uint32_t gElapsedSec = 0;
// Base58 представления (43-44 символа для 32 байт + \0)
static char gSecretB58[50];
static char gPubB58[3][50];
static char gPrivB58[3][50];
// ═══════════════════════════════════════════════════════════
// ARGON2 ПАРАМЕТРЫ
// ═══════════════════════════════════════════════════════════
#define A2_T 2u
#define A2_M 65536u
#define A2_P 1u
#define A2_DKLEN 32u
#define A2_SYNC 4u
#define A2_SEG (A2_M / A2_SYNC)
#define A2_VERSION 0x13u
#define A2_TYPE 2u
#define A2_BLKSZ 1024u
#define TOTAL_FILLS (A2_T * A2_M - 2u)
static uint32_t gDone = 0;
static uint32_t gStartMs = 0;
// ─── прогресс-бар: антимерцание ───────────────────────────
static int gBarFilledPx = 0; // сколько пикселей уже зелёных
static uint32_t gLastElSec = 0xFFFFFFFF; // последнее нарисованное время
// ═══════════════════════════════════════════════════════════
// ВЫЧИСЛИТЕЛЬНОЕ СОСТОЯНИЕ
// ═══════════════════════════════════════════════════════════
static uint8_t gH0[64];
static uint32_t gCurPass = 0;
static uint32_t gCurBlock = 2;
static bool gInitDone = false;
static uint8_t *gBufPrev = nullptr;
static uint8_t *gBufRef = nullptr;
static uint8_t *gBufOut = nullptr;
static uint8_t *gBufZero = nullptr;
static uint8_t *gBufAddr = nullptr;
static uint32_t *gJ1Seg = nullptr;
#define SD_MEM_FILE "/argon2.bin"
static File gSdFile;
// ═══════════════════════════════════════════════════════════
// РЕЗУЛЬТАТ — прокрутка
// ═══════════════════════════════════════════════════════════
#define RES_BTN_H 52 // высота кнопки "Новый" внизу
#define RES_VIEW_H (DISP_H - RES_BTN_H) // 428 px
static int32_t gScrollY = 0;
static int32_t gScrollMax = 0; // рассчитывается при отрисовке
// NVS
static Preferences gPrefs;
// ═══════════════════════════════════════════════════════════
// BLAKE2b
// ═══════════════════════════════════════════════════════════
#define ROTR64(x,n) (((x)>>(n))|((x)<<(64-(n))))
static const uint64_t B2IV[8] = {
0x6A09E667F3BCC908ULL,0xBB67AE8584CAA73BULL,
0x3C6EF372FE94F82BULL,0xA54FF53A5F1D36F1ULL,
0x510E527FADE682D1ULL,0x9B05688C2B3E6C1FULL,
0x1F83D9ABFB41BD6BULL,0x5BE0CD19137E2179ULL
};
static const uint8_t B2SIGMA[12][16] = {
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},
{11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4},
{7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},
{9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},
{2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9},
{12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},
{13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},
{6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5},
{10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0},
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}
};
static B2State gB2S;
static void b2_compress(B2State *S, const uint8_t *blk) {
uint64_t m[16],v[16];
for (int i=0;i<16;i++) m[i]=((const uint64_t*)blk)[i];
for (int i=0;i<8;i++) v[i]=S->h[i];
v[8]=B2IV[0];v[9]=B2IV[1];v[10]=B2IV[2];v[11]=B2IV[3];
v[12]=B2IV[4]^S->t[0];v[13]=B2IV[5]^S->t[1];
v[14]=B2IV[6]^S->f[0];v[15]=B2IV[7]^S->f[1];
#define BG(r,i,a,b,c,d) \
v[a]+=v[b]+m[B2SIGMA[r][2*(i)]]; v[d]=ROTR64(v[d]^v[a],32); \
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],24); \
v[a]+=v[b]+m[B2SIGMA[r][2*(i)+1]];v[d]=ROTR64(v[d]^v[a],16); \
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],63)
for (int r=0;r<12;r++){
BG(r,0,0,4,8,12);BG(r,1,1,5,9,13);BG(r,2,2,6,10,14);BG(r,3,3,7,11,15);
BG(r,4,0,5,10,15);BG(r,5,1,6,11,12);BG(r,6,2,7,8,13);BG(r,7,3,4,9,14);
}
#undef BG
for (int i=0;i<8;i++) S->h[i]^=v[i]^v[i+8];
}
static void b2_init(B2State *S,size_t outlen){
memset(S,0,sizeof(*S));memcpy(S->h,B2IV,64);
S->h[0]^=0x01010000ULL|outlen;S->outlen=outlen;
}
static void b2_update(B2State *S,const uint8_t *in,size_t inlen){
while(inlen>0){
size_t fill=128-S->buflen,use=inlen<fill?inlen:fill;
memcpy(S->buf+S->buflen,in,use);S->buflen+=use;in+=use;inlen-=use;
if(S->buflen==128&&inlen>0){
S->t[0]+=128;if(S->t[0]<128)S->t[1]++;
b2_compress(S,S->buf);S->buflen=0;
}
}
}
static void b2_final(B2State *S,uint8_t *digest){
S->f[0]=~0ULL;S->t[0]+=S->buflen;if(S->t[0]<S->buflen)S->t[1]++;
memset(S->buf+S->buflen,0,128-S->buflen);b2_compress(S,S->buf);
uint8_t tmp[64];
for(int i=0;i<8;i++){uint64_t w=S->h[i];for(int j=0;j<8;j++)tmp[i*8+j]=(uint8_t)(w>>(j*8));}
memcpy(digest,tmp,S->outlen);
}
static void b2hash(const uint8_t *in,size_t inlen,uint8_t *digest,size_t outlen){
b2_init(&gB2S,outlen);b2_update(&gB2S,in,inlen);b2_final(&gB2S,digest);
}
static void b2long(const uint8_t *in,size_t inlen,uint8_t *digest,uint32_t outlen){
uint8_t lenle[4]={(uint8_t)outlen,(uint8_t)(outlen>>8),(uint8_t)(outlen>>16),(uint8_t)(outlen>>24)};
if(outlen<=64){b2_init(&gB2S,outlen);b2_update(&gB2S,lenle,4);b2_update(&gB2S,in,inlen);b2_final(&gB2S,digest);return;}
uint8_t tmp[64];
b2_init(&gB2S,64);b2_update(&gB2S,lenle,4);b2_update(&gB2S,in,inlen);b2_final(&gB2S,tmp);
memcpy(digest,tmp,32);digest+=32;uint32_t rem=outlen-32;
while(rem>64){b2hash(tmp,64,tmp,64);memcpy(digest,tmp,32);digest+=32;rem-=32;}
b2hash(tmp,64,tmp,rem);memcpy(digest,tmp,rem);
}
// ═══════════════════════════════════════════════════════════
// ARGON2ID — BLOCK FILL
// ═══════════════════════════════════════════════════════════
#define A2G(a,b,c,d) do{ \
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
(d)=ROTR64((d)^(a),32); \
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
(b)=ROTR64((b)^(c),24); \
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
(d)=ROTR64((d)^(a),16); \
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
(b)=ROTR64((b)^(c),63); \
}while(0)
#define A2P(v) do{ \
A2G((v)[0],(v)[4],(v)[8],(v)[12]);A2G((v)[1],(v)[5],(v)[9],(v)[13]); \
A2G((v)[2],(v)[6],(v)[10],(v)[14]);A2G((v)[3],(v)[7],(v)[11],(v)[15]); \
A2G((v)[0],(v)[5],(v)[10],(v)[15]);A2G((v)[1],(v)[6],(v)[11],(v)[12]); \
A2G((v)[2],(v)[7],(v)[8],(v)[13]);A2G((v)[3],(v)[4],(v)[9],(v)[14]); \
}while(0)
static void fillBlock(uint32_t prevIdx,uint32_t refIdx,uint32_t outIdx,bool xorMode){
gSdFile.seek((uint64_t)prevIdx*A2_BLKSZ);gSdFile.read(gBufPrev,A2_BLKSZ);
gSdFile.seek((uint64_t)refIdx*A2_BLKSZ);gSdFile.read(gBufRef,A2_BLKSZ);
uint64_t *R=(uint64_t*)gBufOut,*P=(uint64_t*)gBufPrev,*F=(uint64_t*)gBufRef;
for(int i=0;i<128;i++)R[i]=P[i]^F[i];
uint64_t *Q=(uint64_t*)gBufRef;memcpy(Q,R,A2_BLKSZ);
for(int j=0;j<8;j++)A2P(&Q[16*j]);
uint64_t row[16];
for(int i=0;i<8;i++){
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
A2P(row);
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
}
for(int i=0;i<128;i++)R[i]=Q[i]^R[i];
if(xorMode){
gSdFile.seek((uint64_t)outIdx*A2_BLKSZ);gSdFile.read(gBufPrev,A2_BLKSZ);
uint64_t *O=(uint64_t*)gBufPrev;for(int i=0;i<128;i++)R[i]^=O[i];
}
gSdFile.seek((uint64_t)outIdx*A2_BLKSZ);gSdFile.write(gBufOut,A2_BLKSZ);
}
static void generateAddresses(uint32_t pass,uint32_t slice){
memset(gBufZero,0,A2_BLKSZ);
uint8_t inputBlk[A2_BLKSZ];memset(inputBlk,0,A2_BLKSZ);
uint64_t *iv=(uint64_t*)inputBlk;
iv[0]=pass;iv[1]=0;iv[2]=slice;iv[3]=A2_M;iv[4]=A2_T;iv[5]=A2_TYPE;
uint32_t count=0;
for(uint32_t b=0;b<A2_SEG;b+=128){
iv[6]=++count;
{
uint64_t *R=(uint64_t*)gBufAddr,*Z=(uint64_t*)gBufZero,*I=iv;
for(int i=0;i<128;i++)R[i]=Z[i]^I[i];
uint64_t *Q=R,row[16];
for(int j=0;j<8;j++)A2P(&Q[16*j]);
for(int i=0;i<8;i++){
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
A2P(row);
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
}
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^I[i]);
}
{
uint64_t *R=(uint64_t*)gBufPrev,*Z=(uint64_t*)gBufZero,*A=(uint64_t*)gBufAddr;
for(int i=0;i<128;i++)R[i]=Z[i]^A[i];
uint64_t *Q=R,row[16];
for(int j=0;j<8;j++)A2P(&Q[16*j]);
for(int i=0;i<8;i++){
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
A2P(row);
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
}
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^A[i]);
memcpy(gBufAddr,gBufPrev,A2_BLKSZ);
}
uint64_t *addr=(uint64_t*)gBufAddr;
uint32_t cnt=(b+128<=A2_SEG)?128:(A2_SEG-b);
for(uint32_t j=0;j<cnt;j++)gJ1Seg[b+j]=(uint32_t)(addr[j]&0xFFFFFFFFULL);
}
}
static uint32_t indexAlpha(uint32_t pass,uint32_t slice,uint32_t posInSlice,uint32_t J1){
uint32_t refAreaSize;
if(pass==0){
if(slice==0){uint32_t pos=posInSlice;refAreaSize=(pos<2)?0:pos-1;}
else refAreaSize=slice*A2_SEG+posInSlice-1;
}else refAreaSize=A2_M-A2_SEG+posInSlice-1;
if(refAreaSize==0)refAreaSize=1;
uint64_t relPos=(uint64_t)J1*J1>>32;
relPos=refAreaSize-1-((uint64_t)refAreaSize*relPos>>32);
uint32_t startPos=0;
if(pass!=0&&slice!=A2_SYNC-1)startPos=(slice+1)*A2_SEG;
return (startPos+relPos)%A2_M;
}
// ═══════════════════════════════════════════════════════════
// SHA-256
// ═══════════════════════════════════════════════════════════
static void sha256calc(const uint8_t *in,size_t len,uint8_t *out32){
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);mbedtls_sha256_starts(&ctx,0);
mbedtls_sha256_update(&ctx,in,len);mbedtls_sha256_finish(&ctx,out32);
mbedtls_sha256_free(&ctx);
}
// ═══════════════════════════════════════════════════════════
// BASE58
// ═══════════════════════════════════════════════════════════
static const char B58_ALPHA[] =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static void bytesToBase58(const uint8_t *data,size_t len,char *out,size_t outSz){
int zeros=0;
while(zeros<(int)len&&data[zeros]==0)zeros++;
uint8_t tmp[64]={};int tmpLen=0;
for(int i=zeros;i<(int)len;i++){
int carry=data[i];
for(int j=0;j<tmpLen;j++){carry+=256*tmp[j];tmp[j]=carry%58;carry/=58;}
while(carry){tmp[tmpLen++]=carry%58;carry/=58;}
}
int n=0;
for(int i=0;i<zeros&&n<(int)outSz-1;i++)out[n++]='1';
for(int i=tmpLen-1;i>=0&&n<(int)outSz-1;i--)out[n++]=B58_ALPHA[(int)tmp[i]];
out[n]='\0';
}
// ═══════════════════════════════════════════════════════════
// KEY DERIVATION (Ed25519 из masterSecret)
// ═══════════════════════════════════════════════════════════
static void deriveKeyPairs(){
// base64(masterSecret) — standard base64 с '+','/'
uint8_t b64buf[48];size_t b64len=0;
mbedtls_base64_encode(b64buf,sizeof(b64buf),&b64len,gSecret,32);
b64buf[b64len]='\0';
for(int i=0;i<3;i++){
char material[96];
snprintf(material,sizeof(material),"%s|%s",(char*)b64buf,KEY_SUFFIXES[i]);
uint8_t seed[32];
sha256calc((uint8_t*)material,strlen(material),seed);
memcpy(gKeys[i].priv,seed,32);
Ed25519::derivePublicKey(gKeys[i].pub,seed);
}
// base58 для отображения
bytesToBase58(gSecret,32,gSecretB58,sizeof(gSecretB58));
for(int i=0;i<3;i++){
bytesToBase58(gKeys[i].pub,32,gPubB58[i],sizeof(gPubB58[i]));
bytesToBase58(gKeys[i].priv,32,gPrivB58[i],sizeof(gPrivB58[i]));
}
}
// ═══════════════════════════════════════════════════════════
// NVS
// ═══════════════════════════════════════════════════════════
static void nvsSave(){
gPrefs.begin("shine",false);
gPrefs.putString("login",gLogin);
gPrefs.putBytes("secret",gSecret,32);
for(int i=0;i<3;i++){
char k[6];
snprintf(k,sizeof(k),"pub%d",i); gPrefs.putBytes(k,gKeys[i].pub,32);
snprintf(k,sizeof(k),"prv%d",i); gPrefs.putBytes(k,gKeys[i].priv,32);
}
gPrefs.putUInt("tsec",gElapsedSec);
gPrefs.putBool("valid",true);
gPrefs.end();
}
static bool nvsLoad(){
gPrefs.begin("shine",true);
bool valid=gPrefs.getBool("valid",false);
if(valid){
gPrefs.getString("login",gLogin,sizeof(gLogin));
gPrefs.getBytes("secret",gSecret,32);
for(int i=0;i<3;i++){
char k[6];
snprintf(k,sizeof(k),"pub%d",i); gPrefs.getBytes(k,gKeys[i].pub,32);
snprintf(k,sizeof(k),"prv%d",i); gPrefs.getBytes(k,gKeys[i].priv,32);
}
gElapsedSec=gPrefs.getUInt("tsec",0);
for(int i=0;i<32;i++)snprintf(gSecretHex+i*2,3,"%02x",gSecret[i]);
// base58
bytesToBase58(gSecret,32,gSecretB58,sizeof(gSecretB58));
for(int i=0;i<3;i++){
bytesToBase58(gKeys[i].pub,32,gPubB58[i],sizeof(gPubB58[i]));
bytesToBase58(gKeys[i].priv,32,gPrivB58[i],sizeof(gPrivB58[i]));
}
}
gPrefs.end();
return valid;
}
static void nvsClear(){
gPrefs.begin("shine",false);
gPrefs.clear();
gPrefs.end();
}
// ═══════════════════════════════════════════════════════════
// ARGON2ID INIT
// ═══════════════════════════════════════════════════════════
static void argon2Init(const char *login,const char *password){
char saltSrc[256];
snprintf(saltSrc,sizeof(saltSrc),"shine-auth-v2|login=%s|suffix=master.secret",login);
uint8_t saltHash[32];sha256calc((uint8_t*)saltSrc,strlen(saltSrc),saltHash);
uint8_t salt[16];memcpy(salt,saltHash,16);
char passBytes[128];
snprintf(passBytes,sizeof(passBytes),"%s\n%s",login,password);
uint32_t passLen=strlen(passBytes),saltLen=16;
b2_init(&gB2S,64);
uint8_t le4[4];
#define B2LE32(val) do{uint32_t _v=(val);le4[0]=_v;le4[1]=_v>>8;le4[2]=_v>>16;le4[3]=_v>>24;b2_update(&gB2S,le4,4);}while(0)
B2LE32(A2_P);B2LE32(A2_DKLEN);B2LE32(A2_M);B2LE32(A2_T);
B2LE32(A2_VERSION);B2LE32(A2_TYPE);
B2LE32(passLen);b2_update(&gB2S,(const uint8_t*)passBytes,passLen);
B2LE32(saltLen);b2_update(&gB2S,salt,saltLen);
B2LE32(0);B2LE32(0);
#undef B2LE32
b2_final(&gB2S,gH0);
uint8_t input[72];memcpy(input,gH0,64);
for(uint32_t i=0;i<2;i++){
input[64]=i;input[65]=0;input[66]=0;input[67]=0;
input[68]=0;input[69]=0;input[70]=0;input[71]=0;
b2long(input,72,gBufOut,A2_BLKSZ);
gSdFile.seek((uint64_t)i*A2_BLKSZ);gSdFile.write(gBufOut,A2_BLKSZ);
}
gCurPass=0;gCurBlock=2;gDone=0;gStartMs=millis();
gBarFilledPx=0;gLastElSec=0xFFFFFFFF;
gInitDone=true;
generateAddresses(0,0);
}
// ═══════════════════════════════════════════════════════════
// ARGON2ID STEP
// ═══════════════════════════════════════════════════════════
static bool argon2Step(){
if(gCurPass>=A2_T)return true;
uint32_t blk=gCurBlock;
uint32_t slice=blk/A2_SEG;
uint32_t posInSlice=blk-slice*A2_SEG;
bool xorMode=(gCurPass>0);
uint32_t J1;
bool useArgon2i=(gCurPass==0&&slice<2);
if(useArgon2i){
J1=gJ1Seg[posInSlice];
}else{
uint32_t prevIdx=(blk==0)?A2_M-1:blk-1;
gSdFile.seek((uint64_t)prevIdx*A2_BLKSZ);gSdFile.read(gBufRef,8);
J1=((uint32_t*)gBufRef)[0];
}
uint32_t prevIdx=(blk==0)?A2_M-1:blk-1;
uint32_t refIdx=indexAlpha(gCurPass,slice,posInSlice,J1);
fillBlock(prevIdx,refIdx,blk,xorMode);
gDone++;
gCurBlock++;
if(gCurBlock>=A2_M){
gCurBlock=0;gCurPass++;
}else{
uint32_t newSlice=gCurBlock/A2_SEG;
if(newSlice!=slice&&gCurPass==0&&newSlice<2)
generateAddresses(gCurPass,newSlice);
}
return(gCurPass>=A2_T);
}
// ═══════════════════════════════════════════════════════════
// ARGON2ID FINALIZE
// ═══════════════════════════════════════════════════════════
static void argon2Finalize(){
gSdFile.seek((uint64_t)(A2_M-1)*A2_BLKSZ);
gSdFile.read(gBufOut,A2_BLKSZ);
b2long(gBufOut,A2_BLKSZ,gSecret,A2_DKLEN);
for(int i=0;i<32;i++)snprintf(gSecretHex+i*2,3,"%02x",gSecret[i]);
gSecretHex[64]='\0';
gElapsedSec=(millis()-gStartMs)/1000;
deriveKeyPairs();
nvsSave();
Serial.println("=== RESULT ===");
Serial.printf("Login : %s\n",gLogin);
Serial.printf("Secret: %s\n",gSecretHex);
for(int i=0;i<3;i++){
Serial.printf("%s pub : %s\n",KEY_LABELS[i],gPubB58[i]);
Serial.printf("%s priv: %s\n",KEY_LABELS[i],gPrivB58[i]);
}
Serial.println("=== END ===");
}
// ═══════════════════════════════════════════════════════════
// UI — HELPERS
// ═══════════════════════════════════════════════════════════
static void uiText(int x,int y,const char *s,uint16_t color,uint8_t sz=2){
gfx->setTextColor(color);gfx->setTextSize(sz);gfx->setCursor(x,y);gfx->print(s);
}
static void uiRect(int x,int y,int w,int h,uint16_t fill,uint16_t border=0,int r=4){
gfx->fillRoundRect(x,y,w,h,r,fill);
if(border)gfx->drawRoundRect(x,y,w,h,r,border);
}
// ═══════════════════════════════════════════════════════════
// UI — KEYBOARD
// ═══════════════════════════════════════════════════════════
static const char *KB_ROWS[3]={"qwertyuiop","asdfghjkl","zxcvbnm"};
static const char *KB_NUMS[3]={"1234567890","@#$%^&*()-",".,:!?/;\"'="};
#define KB_Y0 200
#define KB_KH 44
#define KB_KW 44
#define KB_GAP 2
static void drawKeyboard(){
gfx->fillRect(0,KB_Y0-4,DISP_W,DISP_H-KB_Y0+4,C_BG);
for(int row=0;row<3;row++){
const char *keys=gKbNums?KB_NUMS[row]:KB_ROWS[row];
int n=strlen(keys);
int totalW=n*(KB_KW+KB_GAP)-KB_GAP;
int x0=(DISP_W-totalW)/2,y=KB_Y0+row*(KB_KH+KB_GAP);
for(int k=0;k<n;k++){
int x=x0+k*(KB_KW+KB_GAP);
uiRect(x,y,KB_KW,KB_KH,C_KEY,C_KEY_HL);
char tmp[3]={0};tmp[0]=keys[k];
uiText(x+KB_KW/2-6,y+KB_KH/2-8,tmp,C_TEXT,2);
}
}
int y4=KB_Y0+3*(KB_KH+KB_GAP);
uiRect(20,y4,80,KB_KH,C_KEY_HL);uiText(28,y4+12,"123",C_TEXT,2);
uiRect(108,y4,164,KB_KH,C_KEY,C_KEY_HL);uiText(162,y4+12,"_",C_TEXT,2);
uiRect(280,y4,80,KB_KH,C_KEY,C_KEY_HL);uiText(296,y4+12,"<<",C_TEXT,2);
uiRect(368,y4,92,KB_KH,C_GREEN);uiText(384,y4+12,"OK",C_TEXT,2);
}
static void drawInputScreen(const char *title,const char *buf,bool isMasked){
gfx->fillScreen(C_BG);
gfx->setTextSize(2);uiText(20,20,title,C_HINT);
uiRect(20,60,DISP_W-40,52,C_FIELD,C_KEY_HL);
char show[68];
if(isMasked){int n=strlen(buf);if(n>64)n=64;for(int i=0;i<n;i++)show[i]='*';show[n]='\0';}
else{strncpy(show,buf,67);show[67]='\0';}
gfx->setTextSize(2);gfx->setTextColor(C_TEXT);gfx->setCursor(30,78);gfx->print(show);
int cx=30+strlen(show)*12;gfx->drawFastVLine(cx,78,20,C_GREEN);
drawKeyboard();
}
static bool kbHit(int tx,int ty,int kx,int ky,int kw,int kh){
return tx>=kx&&tx<kx+kw&&ty>=ky&&ty<ky+kh;
}
static char kbTouch(int tx,int ty){
for(int row=0;row<3;row++){
const char *keys=gKbNums?KB_NUMS[row]:KB_ROWS[row];
int n=strlen(keys);
int totalW=n*(KB_KW+KB_GAP)-KB_GAP,x0=(DISP_W-totalW)/2;
int y=KB_Y0+row*(KB_KH+KB_GAP);
for(int k=0;k<n;k++){
int x=x0+k*(KB_KW+KB_GAP);
if(kbHit(tx,ty,x,y,KB_KW,KB_KH))return keys[k];
}
}
int y4=KB_Y0+3*(KB_KH+KB_GAP);
if(kbHit(tx,ty,20,y4,80,KB_KH))return'\x03';
if(kbHit(tx,ty,108,y4,164,KB_KH))return' ';
if(kbHit(tx,ty,280,y4,80,KB_KH))return'\x08';
if(kbHit(tx,ty,368,y4,92,KB_KH))return'\x0D';
return 0;
}
// ═══════════════════════════════════════════════════════════
// UI — ПРОГРЕСС (антимерцание)
// ═══════════════════════════════════════════════════════════
static void drawProgressHeader(){
gfx->fillRect(0,0,DISP_W,80,C_BG);
gfx->setTextSize(2);uiText(20,18,"Argon2id (SD)",C_GREEN);
char sub[80];snprintf(sub,sizeof(sub),"t=%u m=%u KB p=%u",A2_T,A2_M,A2_P);
uiText(20,46,sub,C_HINT);
// нарисовать пустую полосу один раз
int bx=20,by=90,bw=DISP_W-40,bh=36;
uiRect(bx,by,bw,bh,C_BAR_BG,C_SEP,4);
}
static void drawProgress(){
uint32_t elMs=millis()-gStartMs;
uint32_t elSec=elMs/1000;
// Прогресс-бар: только дорисовываем новую зелёную часть
int bx=20,by=90,bw=DISP_W-40,bh=36;
int filled=(int)((uint64_t)bw*gDone/TOTAL_FILLS);
if(gDone>0&&filled==0)filled=1; // at least 1px once started
if(filled>gBarFilledPx){
gfx->fillRect(bx+gBarFilledPx,by+1,filled-gBarFilledPx,bh-2,C_BAR_FG);
gBarFilledPx=filled;
}
// Текст обновляем только раз в секунду
if(elSec==gLastElSec)return;
gLastElSec=elSec;
float pct=(gDone>0)?((float)gDone/TOTAL_FILLS*100.0f):0.0f;
uint32_t etaSec=0;
if(gDone>10&&gDone<TOTAL_FILLS)
etaSec=(uint32_t)((float)elMs/gDone*(TOTAL_FILLS-gDone)/1000.0f);
gfx->fillRect(0,136,DISP_W,120,C_BG);
char line[64];
snprintf(line,sizeof(line),"%.1f%% (%lu / %lu)",pct,(unsigned long)gDone,(unsigned long)TOTAL_FILLS);
uiText(20,136,line,C_TEXT,2);
snprintf(line,sizeof(line),"Elapsed: %lus (%lum%lus)",
(unsigned long)elSec,(unsigned long)elSec/60,(unsigned long)elSec%60);
uiText(20,162,line,C_GREEN,2);
if(gDone>10){
if(etaSec>=3600)
snprintf(line,sizeof(line),"ETA: ~%lum%lus",(unsigned long)etaSec/60,(unsigned long)etaSec%60);
else
snprintf(line,sizeof(line),"ETA: ~%lus",(unsigned long)etaSec);
uiText(20,188,line,C_HINT,2);
}
}
// ═══════════════════════════════════════════════════════════
// UI — РЕЗУЛЬТАТ (с прокруткой свайпом)
// ═══════════════════════════════════════════════════════════
// Контент рисуется в виртуальных координатах y_virt.
// На экран попадает только [gScrollY .. gScrollY+RES_VIEW_H).
// Кнопка "Новый" всегда внизу (фиксирована).
#define RLH 20 // высота строки (textSize 2)
#define RLH1 12 // высота строки (textSize 1)
#define RSP 6 // маленький отступ
#define RPX 20 // отступ слева
// Вспомогательный класс — рисует текст только если строка видима
struct RDraw {
int32_t scroll;
int32_t y; // текущая виртуальная Y
void line(const char *s, uint16_t col, uint8_t sz=2){
int sy=(int)y-(int)scroll;
int lh=(sz==1)?RLH1:RLH;
if(sy>=-lh&&sy<RES_VIEW_H){
gfx->setTextSize(sz);gfx->setTextColor(col);gfx->setCursor(RPX,sy);gfx->print(s);
}
y+=lh+2;
}
void sep(){
int sy=(int)y-(int)scroll;
if(sy>=0&&sy<RES_VIEW_H)gfx->drawFastHLine(RPX,sy,DISP_W-2*RPX,C_SEP);
y+=10;
}
void gap(int px=4){y+=px;}
// Для длинных строк (base58): разбить на куски по 38 символов при textSize=2
void longval(const char *s, uint16_t col){
const int CPL=38;
int len=(int)strlen(s);
if(len==0){line("(empty)",C_HINT,2);return;}
for(int off=0;off<len;off+=CPL){
char tmp[42];int take=len-off;if(take>CPL)take=CPL;
memcpy(tmp,s+off,take);tmp[take]='\0';
line(tmp,col,2);
}
}
};
static void drawResultContent(){
// очистить область прокрутки
gfx->fillRect(0,0,DISP_W,RES_VIEW_H,C_BG);
RDraw d;d.scroll=gScrollY;d.y=8;
char buf[96];
// — заголовок —
d.line("RESULT",C_GREEN,2);
snprintf(buf,sizeof(buf),"Login: %s",gLogin);
d.line(buf,C_TEXT,2);
d.gap(4);
d.line("Master secret (base58):",C_HINT,2);
d.longval(gSecretB58,C_GREEN);
snprintf(buf,sizeof(buf),"Time: %lum %lus",
(unsigned long)gElapsedSec/60,(unsigned long)gElapsedSec%60);
d.line(buf,C_HINT,2);
d.sep();
// — 3 ключевые пары —
for(int i=0;i<3;i++){
d.line(KEY_LABELS[i],C_ACCENT,2);
d.line("Pub:",C_HINT,2);
d.longval(gPubB58[i],C_TEXT);
d.line("Priv:",C_HINT,2);
d.longval(gPrivB58[i],C_TEXT);
d.sep();
}
// запомним общую высоту контента для ограничения прокрутки
gScrollMax=(int32_t)d.y - RES_VIEW_H;
if(gScrollMax<0)gScrollMax=0;
}
static void drawResultBtnBar(){
// кнопка "Новый" — фиксирована внизу
gfx->fillRect(0,RES_VIEW_H,DISP_W,RES_BTN_H,C_BG);
uiRect(20,RES_VIEW_H+6,DISP_W-40,RES_BTN_H-12,C_RED,0,6);
gfx->setTextSize(2);gfx->setTextColor(C_TEXT);
gfx->setCursor(DISP_W/2-72,RES_VIEW_H+18);
gfx->print("Generate new");
}
static void drawResultFull(){
gScrollY=0;
drawResultContent();
drawResultBtnBar();
}
// ═══════════════════════════════════════════════════════════
// UI — ДИАЛОГ ПОДТВЕРЖДЕНИЯ
// ═══════════════════════════════════════════════════════════
static void drawConfirmDialog(){
gfx->fillRect(40,120,DISP_W-80,260,C_KEY);
gfx->drawRoundRect(40,120,DISP_W-80,260,8,C_KEY_HL);
gfx->setTextSize(2);gfx->setTextColor(C_TEXT);
gfx->setCursor(60,140);gfx->print("Generate new?");
gfx->setTextSize(1);gfx->setTextColor(C_HINT);
gfx->setCursor(60,172);gfx->print("This will ERASE the saved");
gfx->setCursor(60,184);gfx->print("secret and keys from memory.");
// кнопки
uiRect(60,240,140,56,C_RED,0,6);
gfx->setTextSize(2);gfx->setTextColor(C_TEXT);gfx->setCursor(106,260);gfx->print("Yes");
uiRect(280,240,140,56,C_KEY_HL,C_SEP,6);
gfx->setCursor(316,260);gfx->print("No");
}
// ═══════════════════════════════════════════════════════════
// TOUCH HANDLER
// ═══════════════════════════════════════════════════════════
static void handleTap(int tx,int ty){
if(gState==ST_LOGIN||gState==ST_PASS){
bool isPass=(gState==ST_PASS);
char *buf=isPass?gPass:gLogin;
char c=kbTouch(tx,ty);
if(!c)return;
if(c=='\x03'){gKbNums=!gKbNums;drawInputScreen(isPass?"Password:":"Login:",buf,isPass);return;}
if(c=='\x08'){int n=strlen(buf);if(n>0)buf[n-1]='\0';}
else if(c=='\x0D'){
if(!strlen(buf))return;
if(gState==ST_LOGIN){
for(int i=0;buf[i];i++)if(buf[i]>='A'&&buf[i]<='Z')buf[i]+=32;
gState=ST_PASS;drawInputScreen("Password:",gPass,true);return;
} else {
gState=ST_RUNNING;
gfx->fillScreen(C_BG);drawProgressHeader();
SD_MMC.remove(SD_MEM_FILE);
gSdFile=SD_MMC.open(SD_MEM_FILE,FILE_WRITE);
if(!gSdFile){
gfx->setTextSize(2);gfx->setTextColor(C_RED);
gfx->setCursor(20,200);gfx->println("SD open failed");
gState=ST_ERROR;return;
}
argon2Init(gLogin,gPass);
drawProgress();return;
}
} else {int n=strlen(buf);if(n<63){buf[n]=c;buf[n+1]='\0';}}
drawInputScreen(isPass?"Password:":"Login:",buf,isPass);
return;
}
if(gState==ST_DONE||gState==ST_SAVED){
// Кнопка "Новый" внизу
if(ty>=RES_VIEW_H){
gState=ST_CONFIRM;
drawConfirmDialog();
}
return;
}
if(gState==ST_CONFIRM){
if(tx>=60&&tx<200&&ty>=240&&ty<296){
// Да
nvsClear();
memset(gLogin,0,sizeof(gLogin));memset(gPass,0,sizeof(gPass));
gKbNums=false;gScrollY=0;
gState=ST_LOGIN;
drawInputScreen("Login:",gLogin,false);
} else if(tx>=280&&tx<420&&ty>=240&&ty<296){
// Нет
gState=ST_DONE;
drawResultFull();
}
return;
}
}
// ═══════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════
void setup(){
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA,PIN_I2C_SCL);
// Display
gfx->begin();gBus->writeC8D8(0x36,0xA0);
gfx->setBrightness(200);gfx->fillScreen(C_BG);
// Touch
gTouch.setPins(PIN_TP_INT,-1);
gTouch.begin(Wire,CST92XX_SLAVE_ADDRESS,PIN_I2C_SDA,PIN_I2C_SCL);
gTouch.setMaxCoordinates(DISP_W,DISP_H);
gTouch.setSwapXY(true);gTouch.setMirrorXY(true,false);
attachInterrupt(PIN_TP_INT,onTouchIrq,FALLING);
// SD
gpio_reset_pin(GPIO_NUM_2);
pinMode(PIN_SD_CMD,INPUT_PULLUP);pinMode(PIN_SD_D0,INPUT_PULLUP);
delay(20);
SD_MMC.setPins(PIN_SD_CLK,PIN_SD_CMD,PIN_SD_D0);
bool sdOk=SD_MMC.begin("/sdcard",true);
Serial.printf("SD: %s\n",sdOk?"OK":"FAIL");
if(sdOk)Serial.printf("SD size: %llu MB\n",SD_MMC.cardSize()/(1024ULL*1024));
if(!sdOk){
gfx->setTextSize(2);gfx->setTextColor(C_RED);
gfx->setCursor(20,160);gfx->println("SD CARD ERROR");
gfx->setTextSize(1);gfx->setTextColor(C_HINT);
gfx->setCursor(20,200);gfx->println("FAT32 card required.");
while(true)delay(1000);
}
// PSRAM буферы
gBufPrev=(uint8_t*)ps_malloc(A2_BLKSZ);gBufRef=(uint8_t*)ps_malloc(A2_BLKSZ);
gBufOut=(uint8_t*)ps_malloc(A2_BLKSZ);gBufZero=(uint8_t*)ps_calloc(1,A2_BLKSZ);
gBufAddr=(uint8_t*)ps_malloc(A2_BLKSZ);
gJ1Seg=(uint32_t*)ps_malloc(A2_SEG*sizeof(uint32_t));
if(!gBufPrev||!gBufRef||!gBufOut||!gBufZero||!gBufAddr||!gJ1Seg){
gfx->setTextSize(2);gfx->setTextColor(C_RED);
gfx->setCursor(20,200);gfx->println("PSRAM alloc failed");
while(true)delay(1000);
}
// Попытка загрузить сохранённый результат из NVS
if(nvsLoad()){
gState=ST_SAVED;
drawResultFull();
}else{
drawInputScreen("Login:",gLogin,false);
}
}
// ═══════════════════════════════════════════════════════════
// LOOP
// ═══════════════════════════════════════════════════════════
#define BLOCKS_PER_TICK 4
void loop(){
// ── Touch polling (для свайпа + обычных нажатий) ──────
int16_t tx=0,ty=0;
bool nowTouching=(gTouch.getPoint(&tx,&ty,1)>0);
if(nowTouching&&!gTouching){
// начало касания
gTouchStartY=ty;gTouchPrevY=ty;gTouchCurY=ty;gTouchCurX=tx;
gTouching=true;
} else if(nowTouching&&gTouching){
int16_t delta=gTouchPrevY-ty;
gTouchCurY=ty;gTouchPrevY=ty;gTouchCurX=tx;
// живая прокрутка результата
if((gState==ST_DONE||gState==ST_SAVED)&&abs(delta)>0){
gScrollY+=delta;
if(gScrollY<0)gScrollY=0;
if(gScrollY>gScrollMax)gScrollY=gScrollMax;
drawResultContent(); // перерисовать только контент (без кнопки)
}
} else if(!nowTouching&&gTouching){
// конец касания: если почти не скроллили — считаем нажатием
int16_t swipeDelta=abs(gTouchStartY-gTouchCurY);
if(swipeDelta<15)handleTap(gTouchCurX,gTouchCurY);
gTouching=false;
}
// ── Argon2id вычисление ────────────────────────────────
if(gState==ST_RUNNING&&gInitDone){
bool done=false;
for(int i=0;i<BLOCKS_PER_TICK&&!done;i++)
done=argon2Step();
drawProgress();
if(done){
argon2Finalize();
gSdFile.close();
gState=ST_DONE;
gScrollY=0;
drawResultFull();
}
}
}

View File

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// BLAKE2b state
struct B2State {
uint64_t h[8], t[2], f[2];
uint8_t buf[128];
size_t buflen, outlen;
};

View File

@ -15,9 +15,10 @@ case "${MODE}" in
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
*)
echo "Unknown mode: ${MODE}" >&2
echo "Use one of: hello, widgets, audio, simple" >&2
echo "Use one of: hello, widgets, audio, simple, argon2" >&2
exit 2
;;
esac

View File

@ -190,6 +190,18 @@ public final class SolanaUserPdaImportService {
int n = u8(raw, c++);
c += n;
}
} else if (blockType == 55) {
int sessionsMode = u8(raw, c++);
if (sessionsMode != 1 && sessionsMode != 10) return null;
int sessionsCount = u8(raw, c++);
if (sessionsCount > 64) return null;
for (int j = 0; j < sessionsCount; j++) {
c += 1; // session_type
c += 1; // session_version
int n = u8(raw, c++);
c += n;
c += 32; // session_pub_key
}
} else if (blockType == 50) {
c += 1;
} else {

View File

@ -1,2 +1,2 @@
client.version=1.2.126
server.version=1.2.118
client.version=1.2.127
server.version=1.2.119

View File

@ -22,7 +22,11 @@ const BLOCK_TYPE_DEVICE_KEY = 2;
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
const BLOCK_TYPE_SERVER_PROFILE = 30;
const BLOCK_TYPE_ACCESS_SERVERS = 40;
const BLOCK_TYPE_SESSIONS = 55;
const BLOCK_TYPE_TRUSTED_STATE = 50;
const SESSIONS_MODE_MIXED = 1;
const SESSION_TYPE_USER = 1;
const SESSION_TYPE_SUBSERVER = 100;
let solanaLibPromise = null;
function loadSolanaLib() {
@ -72,6 +76,18 @@ function pushVecStrU32(buf, values) {
for (const value of arr) pushStrU32(buf, value);
}
function pushSessionRecordsU32(buf, sessions) {
const arr = Array.isArray(sessions) ? sessions : [];
pushU32LE(buf, arr.length);
for (const session of arr) {
buf.push(Number(session?.sessionType || 0) & 0xff);
buf.push(Number(session?.sessionVersion || 0) & 0xff);
pushStrU32(buf, String(session?.sessionName || ''));
const key = session?.sessionPubKey32 || new Uint8Array(32);
for (const x of key) buf.push(x);
}
}
function makeReader(bytes) {
let offset = 0;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
@ -178,6 +194,8 @@ function serializeCreateUserPdaArgs(args) {
pushStrU32(buf, args.serverAddress);
pushVecStrU32(buf, args.syncServers);
pushVecStrU32(buf, args.accessServers);
buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
pushSessionRecordsU32(buf, args.sessions);
buf.push(Number(args.trustedCount || 0) & 0xff);
pushVecU8(buf, args.rootSignature64);
return new Uint8Array(buf);
@ -207,6 +225,8 @@ function serializeUpdateUserPdaArgs(args) {
pushStrU32(buf, args.serverAddress);
pushVecStrU32(buf, args.syncServers);
pushVecStrU32(buf, args.accessServers);
buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
pushSessionRecordsU32(buf, args.sessions);
buf.push(Number(args.trustedCount || 0) & 0xff);
pushVecU8(buf, args.rootSignature64);
return new Uint8Array(buf);
@ -250,6 +270,8 @@ function createPdaState({
serverAddress,
syncServers,
accessServers,
sessionsMode,
sessions,
trustedCount,
}) {
const serverProfile = isServer ? {
@ -275,6 +297,13 @@ function createPdaState({
serverAddress: serverProfile?.serverAddress ?? '',
syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [],
accessServers: Array.isArray(accessServers) ? [...accessServers] : [],
sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff,
sessions: Array.isArray(sessions) ? sessions.map((x) => ({
sessionType: Number(x?.sessionType || 0) & 0xff,
sessionVersion: Number(x?.sessionVersion || 0) & 0xff,
sessionName: String(x?.sessionName || ''),
sessionPubKey32: x?.sessionPubKey32 instanceof Uint8Array ? x.sessionPubKey32 : new Uint8Array(x?.sessionPubKey32 || 32),
})) : [],
trustedCount: Number(trustedCount || 0) & 0xff,
};
}
@ -326,6 +355,8 @@ export function parseShineUserPda(dataBytes) {
let serverAddress = '';
let syncServers = [];
let accessServers = [];
let sessionsMode = SESSIONS_MODE_MIXED;
let sessions = [];
let trustedCount = 0;
for (let i = 0; i < blocksCount; i += 1) {
@ -387,6 +418,20 @@ export function parseShineUserPda(dataBytes) {
for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8());
continue;
}
if (blockType === BLOCK_TYPE_SESSIONS) {
sessionsMode = reader.readU8();
const sessionsCount = reader.readU8();
sessions = [];
for (let j = 0; j < sessionsCount; j += 1) {
sessions.push({
sessionType: reader.readU8(),
sessionVersion: reader.readU8(),
sessionName: reader.readStrU8(),
sessionPubKey32: reader.readBytes(32),
});
}
continue;
}
if (blockType === BLOCK_TYPE_TRUSTED_STATE) {
trustedCount = reader.readU8();
continue;
@ -415,6 +460,8 @@ export function parseShineUserPda(dataBytes) {
serverAddress,
syncServers,
accessServers,
sessionsMode,
sessions,
trustedCount,
});
@ -442,6 +489,8 @@ export function serializeUnsignedRecordFromState(stateLike) {
serverAddress: stateLike.serverAddress ?? stateLike.serverProfile?.serverAddress,
syncServers: stateLike.syncServers ?? stateLike.serverProfile?.syncServers,
accessServers: stateLike.accessServers,
sessionsMode: stateLike.sessionsMode,
sessions: stateLike.sessions,
trustedCount: stateLike.trustedCount,
});
@ -481,6 +530,13 @@ export function serializeUnsignedRecordFromState(stateLike) {
buf.push(BLOCK_TYPE_ACCESS_SERVERS, 0, state.accessServers.length & 0xff);
for (const loginValue of state.accessServers) pushStrU8(buf, loginValue);
buf.push(BLOCK_TYPE_SESSIONS, 0, state.sessionsMode & 0xff, state.sessions.length & 0xff);
for (const session of state.sessions) {
buf.push(session.sessionType & 0xff, session.sessionVersion & 0xff);
pushStrU8(buf, session.sessionName);
for (const x of session.sessionPubKey32) buf.push(x);
}
buf.push(BLOCK_TYPE_TRUSTED_STATE, 0, state.trustedCount & 0xff);
const recordLen = buf.length + 64;
@ -655,6 +711,8 @@ async function createShineUserPdaOnSolana({
serverAddress,
syncServers,
accessServers,
sessionsMode: SESSIONS_MODE_MIXED,
sessions: [],
trustedCount: 0,
});
@ -680,6 +738,8 @@ async function createShineUserPdaOnSolana({
serverAddress: isServer ? serverAddress : '',
syncServers: isServer ? syncServers : [],
accessServers,
sessionsMode: SESSIONS_MODE_MIXED,
sessions: [],
trustedCount: 0,
rootSignature64: rootSig64,
});
@ -858,6 +918,8 @@ export async function updateShineUserPdaOnSolana({
serverAddress: nextServerProfile?.serverAddress ?? '',
syncServers: nextServerProfile?.syncServers ?? [],
accessServers: accessServers == null ? current.accessServers : accessServers,
sessionsMode: current.sessionsMode,
sessions: current.sessions,
trustedCount: trustedCount == null ? current.trustedCount : trustedCount,
});
@ -887,6 +949,8 @@ export async function updateShineUserPdaOnSolana({
serverAddress: nextState.serverAddress,
syncServers: nextState.syncServers,
accessServers: nextState.accessServers,
sessionsMode: nextState.sessionsMode,
sessions: nextState.sessions,
trustedCount: nextState.trustedCount,
rootSignature64: rootSig64,
});

View File

@ -14,6 +14,8 @@ const MAGIC: &[u8; 5] = b"SHiNE";
const FORMAT_MAJOR: u8 = 1;
const FORMAT_MINOR: u8 = 0;
const MAX_SYNC_SERVERS: usize = 32;
const MAX_SESSIONS: usize = 64;
const MAX_SESSION_NAME_LEN: usize = 64;
const MAX_AUTO_REALLOC_INCREASE: usize = 10_000;
const ZERO_HASH: [u8; 32] = [0; 32];
const BLOCK_TYPE_ROOT_KEY: u8 = 1;
@ -22,10 +24,23 @@ const BLOCK_TYPE_BLOCKCHAIN_REGISTRY: u8 = 3;
const BLOCK_TYPE_SERVER_PROFILE: u8 = 30;
const BLOCK_TYPE_ACCESS_SERVERS: u8 = 40;
const BLOCK_TYPE_TRUSTED_STATE: u8 = 50;
const BLOCK_TYPE_SESSIONS: u8 = 55;
const BLOCK_VERSION_0: u8 = 0;
const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1;
const SESSIONS_MODE_MIXED: u8 = 1;
const SESSIONS_MODE_PDA_ONLY: u8 = 10;
const SESSION_TYPE_USER: u8 = 1;
const SESSION_TYPE_SUBSERVER: u8 = 100;
const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK";
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct SessionRecord {
pub session_type: u8,
pub session_version: u8,
pub session_name: String,
pub session_pub_key: Pubkey,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UserMutableFields {
pub device_key: Pubkey,
@ -42,6 +57,8 @@ pub struct UserMutableFields {
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub sessions_mode: u8,
pub sessions: Vec<SessionRecord>,
pub trusted_count: u8,
}
@ -83,6 +100,8 @@ pub struct UserRecord {
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub sessions_mode: u8,
pub sessions: Vec<SessionRecord>,
pub trusted_count: u8,
pub signature: [u8; 64],
}
@ -242,7 +261,35 @@ pub fn update_users_economy_config(
}
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
validate_login(&args.login)?;
let CreateUserPdaArgs {
login,
root_key,
created_at_ms,
additional_limit,
fields,
signature,
} = args;
let UserMutableFields {
device_key,
blockchain_public_key,
blockchain_name,
used_bytes,
last_block_number,
last_block_hash,
last_block_signature,
arweave_tx_id,
is_server,
address_format_type,
address_format_version,
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
} = fields;
validate_login(&login)?;
require_keys_eq!(
ctx.accounts.login_guard_program.key(),
Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID)
@ -252,17 +299,35 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
classify_login_or_fail(
&ctx.accounts.login_guard_program.to_account_info(),
&ctx.accounts.signer,
&args.login,
&login,
)?;
validate_fields(&args.fields)?;
validate_fields(&UserMutableFields {
device_key,
blockchain_public_key,
blockchain_name: blockchain_name.clone(),
used_bytes,
last_block_number,
last_block_hash: last_block_hash.clone(),
last_block_signature: last_block_signature.clone(),
arweave_tx_id: arweave_tx_id.clone(),
is_server,
address_format_type,
address_format_version,
server_address: server_address.clone(),
sync_servers: sync_servers.clone(),
access_servers: access_servers.clone(),
sessions_mode,
sessions: sessions.clone(),
trusted_count,
})?;
validate_inflow_vault(&ctx.accounts.inflow_vault)?;
require!(
args.additional_limit % settings::LIMIT_STEP == 0,
additional_limit % settings::LIMIT_STEP == 0,
ErrCode::InvalidLimitIncrement
);
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let login_seed = login_seed_normalized(&args.login);
let login_seed = login_seed_normalized(&login);
let (expected_pda, bump) = find_user_pda(ctx.program_id, &login_seed);
require_keys_eq!(
expected_pda,
@ -276,46 +341,50 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
let start_balance = economy
.start_bonus_limit
.checked_add(args.additional_limit)
.checked_add(additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?;
let mut record = UserRecord {
created_at_ms: args.created_at_ms,
updated_at_ms: args.created_at_ms,
created_at_ms,
updated_at_ms: created_at_ms,
record_number: 0,
prev_record_hash: ZERO_HASH,
login: args.login.clone(),
root_key: args.root_key,
device_key: args.fields.device_key,
login,
root_key,
device_key,
blockchain: BlockchainRecord {
blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER,
blockchain_name: args.fields.blockchain_name.clone(),
blockchain_public_key: args.fields.blockchain_public_key,
blockchain_name,
blockchain_public_key,
paid_limit_bytes: start_balance,
used_bytes: args.fields.used_bytes,
last_block_number: args.fields.last_block_number,
last_block_hash: vec_to_hash32(&args.fields.last_block_hash)?,
last_block_signature: vec_to_signature(&args.fields.last_block_signature)?,
arweave_tx_id: args.fields.arweave_tx_id.clone(),
used_bytes,
last_block_number,
last_block_hash: vec_to_hash32(&last_block_hash)?,
last_block_signature: vec_to_signature(&last_block_signature)?,
arweave_tx_id,
},
is_server: args.fields.is_server,
address_format_type: args.fields.address_format_type,
address_format_version: args.fields.address_format_version,
server_address: args.fields.server_address.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
trusted_count: args.fields.trusted_count,
is_server,
address_format_type,
address_format_version,
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
signature: [0; 64],
};
validate_blockchain_limits(&record.blockchain, 0, 0, true)?;
verify_last_block_state_signature(&ctx.accounts.instructions, &record)?;
let unsigned = serialize_unsigned_record(&record)?;
record.signature = verify_record_signature(
let unsigned_hash = hashv(&[&unsigned]);
drop(unsigned);
record.signature = verify_record_signature_hash(
&ctx.accounts.instructions,
&record.root_key,
&args.signature,
&unsigned,
&signature,
unsigned_hash.as_ref(),
)?;
let serialized = serialize_full_record(&record)?;
@ -342,10 +411,7 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
let total_fee = economy
.registration_fee_lamports
.checked_add(limit_fee_lamports(
args.additional_limit,
economy.lamports_per_limit_step,
)?)
.checked_add(limit_fee_lamports(additional_limit, economy.lamports_per_limit_step)?)
.ok_or(error!(ErrCode::MathOverflow))?;
transfer_lamports(
&ctx.accounts.signer,
@ -396,10 +462,11 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
);
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let login_seed = login_seed_normalized(&args.login);
let (expected_pda, _) = find_user_pda(ctx.program_id, &login_seed);
require_keys_eq!(
expected_pda,
{
let normalized_login = login_seed_normalized(&args.login);
find_user_pda(ctx.program_id, &normalized_login).0
},
ctx.accounts.user_pda.key(),
ErrCode::InvalidPdaAddress
);
@ -410,7 +477,7 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
let raw = safe_read_pda(&ctx.accounts.user_pda);
require!(!raw.is_empty(), ErrCode::EmptyPdaData);
let old_record = deserialize_record_from_pda(raw.as_slice())?;
let old_record = Box::new(deserialize_record_from_pda(raw.as_slice())?);
drop(raw);
require!(
@ -431,24 +498,21 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
ErrCode::InvalidVersion
);
let expected_prev_hash = hash_unsigned_record(&old_record)?;
let provided_prev_hash = vec_to_hash32(&args.prev_hash)?;
let provided_prev_hash = Box::new(vec_to_hash32(&args.prev_hash)?);
require!(
expected_prev_hash == provided_prev_hash,
hash_unsigned_record(&old_record)? == *provided_prev_hash,
ErrCode::InvalidPrevHash
);
let new_balance = old_record
let new_balance = Box::new(old_record
.blockchain
.paid_limit_bytes
.checked_add(args.additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?;
.ok_or(error!(ErrCode::MathOverflow))?);
require!(
new_balance >= old_record.blockchain.paid_limit_bytes,
*new_balance >= old_record.blockchain.paid_limit_bytes,
ErrCode::BalanceDecrease
);
let old_used_bytes = old_record.blockchain.used_bytes;
let old_last_block_number = old_record.blockchain.last_block_number;
let blockchain_state_unchanged = old_record.blockchain.used_bytes == args.fields.used_bytes
&& old_record.blockchain.last_block_number == args.fields.last_block_number
&& old_record.blockchain.last_block_hash.as_slice() == args.fields.last_block_hash.as_slice()
@ -456,60 +520,60 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
== args.fields.last_block_signature.as_slice()
&& old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id;
let mut new_record = UserRecord {
created_at_ms: old_record.created_at_ms,
updated_at_ms: args.updated_at_ms,
record_number: args.version,
prev_record_hash: provided_prev_hash,
login: old_record.login.clone(),
root_key: old_record.root_key,
device_key: args.fields.device_key,
blockchain: BlockchainRecord {
blockchain_type: old_record.blockchain.blockchain_type,
blockchain_name: args.fields.blockchain_name.clone(),
blockchain_public_key: args.fields.blockchain_public_key,
paid_limit_bytes: new_balance,
used_bytes: args.fields.used_bytes,
last_block_number: args.fields.last_block_number,
last_block_hash: vec_to_hash32(&args.fields.last_block_hash)?,
last_block_signature: vec_to_signature(&args.fields.last_block_signature)?,
arweave_tx_id: args.fields.arweave_tx_id.clone(),
},
is_server: args.fields.is_server,
address_format_type: args.fields.address_format_type,
address_format_version: args.fields.address_format_version,
server_address: args.fields.server_address.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
trusted_count: args.fields.trusted_count,
signature: [0; 64],
};
require!(
new_record.blockchain.blockchain_type == old_record.blockchain.blockchain_type
&& new_record.blockchain.blockchain_name == old_record.blockchain.blockchain_name
&& new_record.blockchain.blockchain_public_key
args.fields.blockchain_name == old_record.blockchain.blockchain_name
&& args.fields.blockchain_public_key
== old_record.blockchain.blockchain_public_key,
ErrCode::ImmutableFieldChanged
);
{
let candidate_blockchain =
build_candidate_blockchain_for_update(&old_record, *new_balance, &args.fields)?;
validate_blockchain_limits(
&new_record.blockchain,
old_used_bytes,
old_last_block_number,
&candidate_blockchain,
old_record.blockchain.used_bytes,
old_record.blockchain.last_block_number,
false,
)?;
drop(old_record);
}
if !blockchain_state_unchanged {
verify_last_block_state_signature(&ctx.accounts.instructions, &new_record)?;
verify_last_block_state_signature_candidate(
&ctx.accounts.instructions,
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
}
let unsigned = serialize_unsigned_record(&new_record)?;
new_record.signature = verify_record_signature(
&ctx.accounts.instructions,
&new_record.root_key,
&args.signature,
&unsigned,
let unsigned = serialize_unsigned_update_candidate(
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
let unsigned_hash = hashv(&[&unsigned]);
drop(unsigned);
let verified_signature = Box::new(verify_record_signature_hash(
&ctx.accounts.instructions,
&old_record.root_key,
&args.signature,
unsigned_hash.as_ref(),
)?);
let mut new_record = build_update_record(
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
new_record.signature = *verified_signature;
drop(old_record);
let serialized = serialize_full_record(&new_record)?;
ensure_pda_size_and_rent(
@ -533,6 +597,97 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
Ok(())
}
#[inline(never)]
fn build_candidate_blockchain_for_update(
old_record: &UserRecord,
new_balance: u64,
fields: &UserMutableFields,
) -> Result<BlockchainRecord> {
Ok(BlockchainRecord {
blockchain_type: old_record.blockchain.blockchain_type,
blockchain_name: fields.blockchain_name.clone(),
blockchain_public_key: fields.blockchain_public_key,
paid_limit_bytes: new_balance,
used_bytes: fields.used_bytes,
last_block_number: fields.last_block_number,
last_block_hash: vec_to_hash32(&fields.last_block_hash)?,
last_block_signature: vec_to_signature(&fields.last_block_signature)?,
arweave_tx_id: fields.arweave_tx_id.clone(),
})
}
#[inline(never)]
fn build_update_record(
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<UserRecord> {
Ok(UserRecord {
created_at_ms: old_record.created_at_ms,
updated_at_ms,
record_number: version,
prev_record_hash,
login: old_record.login.clone(),
root_key: old_record.root_key,
device_key: fields.device_key,
blockchain: build_candidate_blockchain_for_update(old_record, new_balance, fields)?,
is_server: fields.is_server,
address_format_type: fields.address_format_type,
address_format_version: fields.address_format_version,
server_address: fields.server_address.clone(),
sync_servers: fields.sync_servers.clone(),
access_servers: fields.access_servers.clone(),
sessions_mode: fields.sessions_mode,
sessions: fields.sessions.clone(),
trusted_count: fields.trusted_count,
signature: [0; 64],
})
}
#[inline(never)]
fn verify_last_block_state_signature_candidate(
instructions_sysvar: &AccountInfo,
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<()> {
let candidate = build_update_record(
old_record,
updated_at_ms,
version,
prev_record_hash,
new_balance,
fields,
)?;
verify_last_block_state_signature(instructions_sysvar, &candidate)
}
#[inline(never)]
fn serialize_unsigned_update_candidate(
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<Vec<u8>> {
let candidate = build_update_record(
old_record,
updated_at_ms,
version,
prev_record_hash,
new_balance,
fields,
)?;
serialize_unsigned_record(&candidate)
}
fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
let login_bytes = record.login.as_bytes();
require!(login_bytes.len() <= u8::MAX as usize, ErrCode::InvalidLogin);
@ -551,7 +706,7 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
out.push(login_bytes.len() as u8);
out.extend_from_slice(login_bytes);
let blocks_count = if record.is_server { 6 } else { 5 };
let blocks_count = if record.is_server { 7 } else { 6 };
out.push(blocks_count);
write_root_key_block(&mut out, record);
write_device_key_block(&mut out, record);
@ -560,6 +715,7 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
write_server_profile_block(&mut out, record)?;
}
write_access_servers_block(&mut out, record)?;
write_sessions_block(&mut out, record)?;
write_trusted_state_block(&mut out, record);
let record_len = out
@ -650,6 +806,29 @@ fn write_access_servers_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<
Ok(())
}
fn write_sessions_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<()> {
out.push(BLOCK_TYPE_SESSIONS);
out.push(BLOCK_VERSION_0);
out.push(record.sessions_mode);
require!(
record.sessions.len() <= MAX_SESSIONS,
ErrCode::InvalidRecordData
);
out.push(record.sessions.len() as u8);
for session in &record.sessions {
write_session_record(out, session)?;
}
Ok(())
}
fn write_session_record(out: &mut Vec<u8>, session: &SessionRecord) -> Result<()> {
out.push(session.session_type);
out.push(session.session_version);
write_len_prefixed_string(out, &session.session_name)?;
out.extend_from_slice(session.session_pub_key.as_ref());
Ok(())
}
fn write_trusted_state_block(out: &mut Vec<u8>, record: &UserRecord) {
out.push(BLOCK_TYPE_TRUSTED_STATE);
out.push(BLOCK_VERSION_0);
@ -696,6 +875,19 @@ fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result<BlockchainR
})
}
fn read_session_record(data: &[u8], cursor: &mut usize) -> Result<SessionRecord> {
let session_type = read_u8(data, cursor)?;
let session_version = read_u8(data, cursor)?;
let session_name = read_len_prefixed_string(data, cursor)?;
let session_pub_key = Pubkey::new_from_array(read_fixed_32(data, cursor)?);
Ok(SessionRecord {
session_type,
session_version,
session_name,
session_pub_key,
})
}
fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
require!(raw.len() >= 9, ErrCode::InvalidRecordData);
require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic);
@ -727,6 +919,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
let mut server_address = String::new();
let mut sync_servers = Vec::new();
let mut access_servers = Vec::new();
let mut sessions_mode = SESSIONS_MODE_MIXED;
let mut sessions = Vec::new();
let mut trusted_count = 0u8;
for _ in 0..blocks_count {
@ -771,6 +965,15 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
access_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
}
}
BLOCK_TYPE_SESSIONS => {
require!(sessions.is_empty(), ErrCode::InvalidRecordData);
sessions_mode = read_u8(useful, &mut cursor)?;
let sessions_count = read_u8(useful, &mut cursor)? as usize;
require!(sessions_count <= MAX_SESSIONS, ErrCode::InvalidRecordData);
for _ in 0..sessions_count {
sessions.push(read_session_record(useful, &mut cursor)?);
}
}
BLOCK_TYPE_TRUSTED_STATE => {
trusted_count = read_u8(useful, &mut cursor)?;
}
@ -778,6 +981,7 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
}
}
validate_sessions_fields(sessions_mode, &sessions)?;
let signature = read_fixed_64(useful, &mut cursor)?;
require!(cursor == useful.len(), ErrCode::InvalidRecordLength);
@ -796,6 +1000,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
signature,
})
@ -809,20 +1015,19 @@ fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> {
Ok(out)
}
fn verify_record_signature(
fn verify_record_signature_hash(
instructions_sysvar: &AccountInfo,
root_key: &Pubkey,
signature: &[u8],
unsigned: &[u8],
message_hash: &[u8],
) -> Result<[u8; 64]> {
let provided_sig = vec_to_signature(signature)?;
let msg_hash = hashv(&[unsigned]);
verify_ed25519_signature_instruction(
instructions_sysvar,
-2,
root_key,
&provided_sig,
msg_hash.as_ref(),
message_hash,
)?;
Ok(provided_sig)
}
@ -1007,6 +1212,46 @@ fn validate_fields(fields: &UserMutableFields) -> Result<()> {
ErrCode::InvalidRecordData
);
}
validate_sessions_fields(fields.sessions_mode, &fields.sessions)?;
Ok(())
}
fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> Result<()> {
require!(
mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY,
ErrCode::InvalidRecordData
);
require!(sessions.len() <= MAX_SESSIONS, ErrCode::InvalidRecordData);
for i in 0..sessions.len() {
validate_session_record(&sessions[i])?;
for j in (i + 1)..sessions.len() {
require!(
sessions[i].session_name != sessions[j].session_name,
ErrCode::InvalidRecordData
);
require!(
sessions[i].session_pub_key != sessions[j].session_pub_key,
ErrCode::InvalidRecordData
);
}
}
Ok(())
}
fn validate_session_record(session: &SessionRecord) -> Result<()> {
require!(
session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER,
ErrCode::InvalidRecordData
);
require!(session.session_version == 1, ErrCode::InvalidRecordData);
let bytes = session.session_name.as_bytes();
require!(!bytes.is_empty(), ErrCode::InvalidRecordData);
require!(bytes.len() <= MAX_SESSION_NAME_LEN, ErrCode::InvalidRecordData);
for &b in bytes {
let ok = b.is_ascii_alphanumeric() || b == b'_';
require!(ok, ErrCode::InvalidRecordData);
}
Ok(())
}

View File

@ -1,6 +1,7 @@
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
ComputeBudgetProgram,
Ed25519Program,
PublicKey,
SYSVAR_INSTRUCTIONS_PUBKEY,
@ -21,8 +22,10 @@ const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
const BLOCK_TYPE_SERVER_PROFILE = 30;
const BLOCK_TYPE_ACCESS_SERVERS = 40;
const BLOCK_TYPE_TRUSTED_STATE = 50;
const BLOCK_TYPE_SESSIONS = 55;
const BLOCK_VERSION_0 = 0;
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
const SESSIONS_MODE_MIXED = 1;
const LIMIT_STEP = 10_000n;
const START_BONUS_LIMIT = 100_000n;
@ -32,6 +35,13 @@ const SHINE_PAYMENTS_PROGRAM_ID = new PublicKey(
);
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = "shine_payments_inflow_vault";
type SessionRecord = {
sessionType: number;
sessionVersion: number;
sessionName: string;
sessionPubKey: PublicKey;
};
type MutableFields = {
deviceKey: PublicKey;
blockchainPublicKey: PublicKey;
@ -42,10 +52,13 @@ type MutableFields = {
lastBlockSignature: Buffer;
arweaveTxId: string;
isServer: boolean;
serverKey: PublicKey;
addressFormatType: number;
addressFormatVersion: number;
serverAddress: string;
syncServers: string[];
accessServers: string[];
sessionsMode: number;
sessions: SessionRecord[];
trustedCount: number;
};
@ -69,10 +82,13 @@ type UnsignedRecord = {
arweaveTxId: string;
};
isServer: boolean;
serverKey: PublicKey;
addressFormatType: number;
addressFormatVersion: number;
serverAddress: string;
syncServers: string[];
accessServers: string[];
sessionsMode: number;
sessions: SessionRecord[];
trustedCount: number;
};
@ -98,7 +114,7 @@ function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
out.push(MAGIC);
out.push(Buffer.from([FORMAT_MAJOR]));
out.push(Buffer.from([FORMAT_MINOR]));
out.push(Buffer.alloc(2, 0)); // record_len placeholder
out.push(Buffer.alloc(2, 0));
out.push(u64le(r.createdAtMs));
out.push(u64le(r.updatedAtMs));
@ -106,7 +122,7 @@ function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
out.push(r.prevRecordHash);
out.push(strBytes(r.login));
out.push(Buffer.from([r.isServer ? 6 : 5]));
out.push(Buffer.from([r.isServer ? 7 : 6]));
out.push(Buffer.from([BLOCK_TYPE_ROOT_KEY, BLOCK_VERSION_0]));
out.push(r.rootKey.toBuffer());
out.push(Buffer.from([BLOCK_TYPE_DEVICE_KEY, BLOCK_VERSION_0]));
@ -129,21 +145,23 @@ function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
if (r.isServer) {
out.push(Buffer.from([BLOCK_TYPE_SERVER_PROFILE, BLOCK_VERSION_0, 1]));
out.push(r.serverKey.toBuffer());
out.push(Buffer.from([r.addressFormatType, r.addressFormatVersion]));
out.push(strBytes(r.serverAddress));
out.push(Buffer.from([r.syncServers.length]));
for (const s of r.syncServers) {
out.push(strBytes(s));
}
for (const s of r.syncServers) out.push(strBytes(s));
}
out.push(Buffer.from([BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0]));
out.push(Buffer.from([r.accessServers.length]));
for (const s of r.accessServers) {
out.push(strBytes(s));
out.push(Buffer.from([BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0, r.accessServers.length]));
for (const s of r.accessServers) out.push(strBytes(s));
out.push(Buffer.from([BLOCK_TYPE_SESSIONS, BLOCK_VERSION_0, r.sessionsMode, r.sessions.length]));
for (const s of r.sessions) {
out.push(Buffer.from([s.sessionType, s.sessionVersion]));
out.push(strBytes(s.sessionName));
out.push(s.sessionPubKey.toBuffer());
}
out.push(Buffer.from([BLOCK_TYPE_TRUSTED_STATE, BLOCK_VERSION_0]));
out.push(Buffer.from([r.trustedCount]));
out.push(Buffer.from([BLOCK_TYPE_TRUSTED_STATE, BLOCK_VERSION_0, r.trustedCount]));
const unsigned = Buffer.concat(out);
const recordLen = unsigned.length + 64;
@ -191,9 +209,7 @@ describe("shine_users e2e", () => {
SHINE_PAYMENTS_PROGRAM_ID
);
const economyAi = await provider.connection.getAccountInfo(
usersEconomyConfigPda
);
const economyAi = await provider.connection.getAccountInfo(usersEconomyConfigPda);
if (!economyAi) {
await program.methods
.initUsersEconomyConfig()
@ -207,10 +223,28 @@ describe("shine_users e2e", () => {
const root = anchor.web3.Keypair.generate();
const blockchain = anchor.web3.Keypair.generate();
const deviceKey = anchor.web3.Keypair.generate().publicKey;
const serverKey1 = anchor.web3.Keypair.generate().publicKey;
const serverKey2 = anchor.web3.Keypair.generate().publicKey;
const blockchainName = `${login}-001`;
const createFields: MutableFields = {
deviceKey,
blockchainPublicKey: blockchain.publicKey,
blockchainName,
usedBytes: 0n,
lastBlockNumber: 0,
lastBlockHash: ZERO_HASH,
lastBlockSignature: Buffer.alloc(64, 0),
arweaveTxId: "",
isServer: true,
addressFormatType: 1,
addressFormatVersion: 0,
serverAddress: "https://srv-1.local",
syncServers: ["sync_srv_1", "sync_srv_2"],
accessServers: ["access_srv_1"],
sessionsMode: SESSIONS_MODE_MIXED,
sessions: [],
trustedCount: 0,
};
const createdAtMs = BigInt(Date.now());
const additionalLimitCreate = 20_000n;
expect(additionalLimitCreate % LIMIT_STEP).eq(0n);
@ -228,27 +262,29 @@ describe("shine_users e2e", () => {
blockchainName,
blockchainPublicKey: blockchain.publicKey,
paidLimitBytes: START_BONUS_LIMIT + additionalLimitCreate,
usedBytes: 0n,
lastBlockNumber: 0,
lastBlockHash: ZERO_HASH,
usedBytes: createFields.usedBytes,
lastBlockNumber: createFields.lastBlockNumber,
lastBlockHash: createFields.lastBlockHash,
lastBlockSignature: Buffer.alloc(64, 0),
arweaveTxId: "",
arweaveTxId: createFields.arweaveTxId,
},
isServer: true,
serverKey: serverKey1,
serverAddress: "https://srv-1.local",
syncServers: ["sync_srv_1", "sync_srv_2"],
accessServers: ["access_srv_1"],
trustedCount: 0,
isServer: createFields.isServer,
addressFormatType: createFields.addressFormatType,
addressFormatVersion: createFields.addressFormatVersion,
serverAddress: createFields.serverAddress,
syncServers: createFields.syncServers,
accessServers: createFields.accessServers,
sessionsMode: createFields.sessionsMode,
sessions: createFields.sessions,
trustedCount: createFields.trustedCount,
};
const createLastBlockHash = sha256(serializeLastBlockState(createRecord));
const createLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
privateKey: blockchain.secretKey,
message: createLastBlockHash,
});
createRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
Buffer.from(createLastBlockEdIx.data)
);
createRecord.blockchain.lastBlockSignature = extractSigFromEdIx(Buffer.from(createLastBlockEdIx.data));
createFields.lastBlockSignature = createRecord.blockchain.lastBlockSignature;
const createUnsigned = serializeUnsignedRecord(createRecord);
const createHash = sha256(createUnsigned);
@ -265,22 +301,28 @@ describe("shine_users e2e", () => {
createdAtMs: new anchor.BN(createdAtMs.toString()),
additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
fields: {
deviceKey,
blockchainPublicKey: blockchain.publicKey,
blockchainName,
usedBytes: new anchor.BN(
createRecord.blockchain.usedBytes.toString()
),
lastBlockNumber: createRecord.blockchain.lastBlockNumber,
lastBlockHash: createRecord.blockchain.lastBlockHash,
lastBlockSignature: createRecord.blockchain.lastBlockSignature,
arweaveTxId: "",
isServer: true,
serverKey: serverKey1,
serverAddress: "https://srv-1.local",
syncServers: ["sync_srv_1", "sync_srv_2"],
accessServers: ["access_srv_1"],
trustedCount: 0,
deviceKey: createFields.deviceKey,
blockchainPublicKey: createFields.blockchainPublicKey,
blockchainName: createFields.blockchainName,
usedBytes: new anchor.BN(createFields.usedBytes.toString()),
lastBlockNumber: createFields.lastBlockNumber,
lastBlockHash: createFields.lastBlockHash,
lastBlockSignature: createFields.lastBlockSignature,
arweaveTxId: createFields.arweaveTxId,
isServer: createFields.isServer,
addressFormatType: createFields.addressFormatType,
addressFormatVersion: createFields.addressFormatVersion,
serverAddress: createFields.serverAddress,
syncServers: createFields.syncServers,
accessServers: createFields.accessServers,
sessionsMode: createFields.sessionsMode,
sessions: createFields.sessions.map((s) => ({
sessionType: s.sessionType,
sessionVersion: s.sessionVersion,
sessionName: s.sessionName,
sessionPubKey: s.sessionPubKey,
})),
trustedCount: createFields.trustedCount,
},
signature: createSig,
})
@ -293,10 +335,7 @@ describe("shine_users e2e", () => {
})
.instruction();
await provider.sendAndConfirm(
new Transaction().add(createEdIx, createLastBlockEdIx, createIx),
[]
);
await provider.sendAndConfirm(new Transaction().add(createEdIx, createLastBlockEdIx, createIx), []);
const createAcc = await provider.connection.getAccountInfo(userPda);
expect(createAcc).not.eq(null);
@ -304,6 +343,26 @@ describe("shine_users e2e", () => {
const additionalLimitUpdate = 30_000n;
expect(additionalLimitUpdate % LIMIT_STEP).eq(0n);
const updatedDeviceKey = anchor.web3.Keypair.generate().publicKey;
const updateFields: MutableFields = {
deviceKey: updatedDeviceKey,
blockchainPublicKey: blockchain.publicKey,
blockchainName,
usedBytes: 512n,
lastBlockNumber: 1,
lastBlockHash: sha256(Buffer.from("first-shine-block")),
lastBlockSignature: Buffer.alloc(64, 0),
arweaveTxId: "",
isServer: true,
addressFormatType: 1,
addressFormatVersion: 0,
serverAddress: "https://srv-2.local",
syncServers: ["sync_srv_3"],
accessServers: ["access_srv_2", "access_srv_3"],
sessionsMode: SESSIONS_MODE_MIXED,
sessions: [],
trustedCount: 0,
};
const updateRecord: UnsignedRecord = {
createdAtMs,
@ -312,34 +371,35 @@ describe("shine_users e2e", () => {
prevRecordHash: sha256(createUnsigned),
login,
rootKey: root.publicKey,
deviceKey: anchor.web3.Keypair.generate().publicKey,
deviceKey: updatedDeviceKey,
blockchain: {
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
blockchainName,
blockchainPublicKey: blockchain.publicKey,
paidLimitBytes:
START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
usedBytes: 512n,
lastBlockNumber: 1,
lastBlockHash: sha256(Buffer.from("first-shine-block")),
paidLimitBytes: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
usedBytes: updateFields.usedBytes,
lastBlockNumber: updateFields.lastBlockNumber,
lastBlockHash: updateFields.lastBlockHash,
lastBlockSignature: Buffer.alloc(64, 0),
arweaveTxId: "",
arweaveTxId: updateFields.arweaveTxId,
},
isServer: true,
serverKey: serverKey2,
serverAddress: "https://srv-2.local",
syncServers: ["sync_srv_3"],
accessServers: ["access_srv_2", "access_srv_3"],
trustedCount: 0,
isServer: updateFields.isServer,
addressFormatType: updateFields.addressFormatType,
addressFormatVersion: updateFields.addressFormatVersion,
serverAddress: updateFields.serverAddress,
syncServers: updateFields.syncServers,
accessServers: updateFields.accessServers,
sessionsMode: updateFields.sessionsMode,
sessions: updateFields.sessions,
trustedCount: updateFields.trustedCount,
};
const updateLastBlockHash = sha256(serializeLastBlockState(updateRecord));
const updateLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
privateKey: blockchain.secretKey,
message: updateLastBlockHash,
});
updateRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
Buffer.from(updateLastBlockEdIx.data)
);
updateRecord.blockchain.lastBlockSignature = extractSigFromEdIx(Buffer.from(updateLastBlockEdIx.data));
updateFields.lastBlockSignature = updateRecord.blockchain.lastBlockSignature;
const updateUnsigned = serializeUnsignedRecord(updateRecord);
const updateHash = sha256(updateUnsigned);
@ -359,22 +419,28 @@ describe("shine_users e2e", () => {
prevHash: sha256(createUnsigned),
additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
fields: {
deviceKey: updateRecord.deviceKey,
blockchainPublicKey: updateRecord.blockchain.blockchainPublicKey,
blockchainName,
usedBytes: new anchor.BN(
updateRecord.blockchain.usedBytes.toString()
),
lastBlockNumber: updateRecord.blockchain.lastBlockNumber,
lastBlockHash: updateRecord.blockchain.lastBlockHash,
lastBlockSignature: updateRecord.blockchain.lastBlockSignature,
arweaveTxId: "",
isServer: true,
serverKey: serverKey2,
serverAddress: "https://srv-2.local",
syncServers: ["sync_srv_3"],
accessServers: ["access_srv_2", "access_srv_3"],
trustedCount: 0,
deviceKey: updateFields.deviceKey,
blockchainPublicKey: updateFields.blockchainPublicKey,
blockchainName: updateFields.blockchainName,
usedBytes: new anchor.BN(updateFields.usedBytes.toString()),
lastBlockNumber: updateFields.lastBlockNumber,
lastBlockHash: updateFields.lastBlockHash,
lastBlockSignature: updateFields.lastBlockSignature,
arweaveTxId: updateFields.arweaveTxId,
isServer: updateFields.isServer,
addressFormatType: updateFields.addressFormatType,
addressFormatVersion: updateFields.addressFormatVersion,
serverAddress: updateFields.serverAddress,
syncServers: updateFields.syncServers,
accessServers: updateFields.accessServers,
sessionsMode: updateFields.sessionsMode,
sessions: updateFields.sessions.map((s) => ({
sessionType: s.sessionType,
sessionVersion: s.sessionVersion,
sessionName: s.sessionName,
sessionPubKey: s.sessionPubKey,
})),
trustedCount: updateFields.trustedCount,
},
signature: updateSig,
})
@ -387,8 +453,10 @@ describe("shine_users e2e", () => {
})
.instruction();
const updateComputeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
const updateHeapIx = ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });
await provider.sendAndConfirm(
new Transaction().add(updateEdIx, updateLastBlockEdIx, updateIx),
new Transaction().add(updateComputeIx, updateHeapIx, updateEdIx, updateLastBlockEdIx, updateIx),
[]
);