diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md index d8aae70..8790f3d 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md @@ -9,6 +9,7 @@ - `audio` — динамик/аудио-кодек (пример `07_ES8311`) - `hello` — базовый тест экрана (пример `01_HelloWorld`) - `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU) +- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости) Запуск: diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/argon2_sd_test/argon2_sd_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/argon2_sd_test/argon2_sd_test.ino new file mode 100644 index 0000000..43f5ce5 --- /dev/null +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/argon2_sd_test/argon2_sd_test.ino @@ -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=|suffix=master.secret")[0:16] + * passBytes = UTF8("\n") + * 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 +#include +#include +#include +#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 +#include + +// ═══════════════════════════════════════════════════════════ +// 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=inlenbuf+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]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>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=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;kfillScreen(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;isetTextSize(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=ky&&tyfillRect(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&&gDonefillRect(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&&sysetTextSize(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&&sydrawFastHLine(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;offCPL)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 +#include + +// BLAKE2b state +struct B2State { + uint64_t h[8], t[2], f[2]; + uint8_t buf[128]; + size_t buflen, outlen; +}; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh index e967baa..3b9cb55 100755 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh @@ -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 diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index 9e624cb..c25cbb6 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -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 { diff --git a/VERSION.properties b/VERSION.properties index e4f760b..803df9d 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.126 -server.version=1.2.118 +client.version=1.2.127 +server.version=1.2.119 diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js index dead6a3..c20d8d8 100644 --- a/shine-UI/js/services/shine-user-pda-service.js +++ b/shine-UI/js/services/shine-user-pda-service.js @@ -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, }); diff --git a/shine-solana/shine/programs/shine_users/src/users.rs b/shine-solana/shine/programs/shine_users/src/users.rs index 543d4be..6604d23 100644 --- a/shine-solana/shine/programs/shine_users/src/users.rs +++ b/shine-solana/shine/programs/shine_users/src/users.rs @@ -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, pub access_servers: Vec, + pub sessions_mode: u8, + pub sessions: Vec, pub trusted_count: u8, } @@ -83,6 +100,8 @@ pub struct UserRecord { pub server_address: String, pub sync_servers: Vec, pub access_servers: Vec, + pub sessions_mode: u8, + pub sessions: Vec, 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, 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, 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, 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, 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, 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, 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, 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, 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 ); - validate_blockchain_limits( - &new_record.blockchain, - old_used_bytes, - old_last_block_number, - false, - )?; - drop(old_record); + { + let candidate_blockchain = + build_candidate_blockchain_for_update(&old_record, *new_balance, &args.fields)?; + validate_blockchain_limits( + &candidate_blockchain, + old_record.blockchain.used_bytes, + old_record.blockchain.last_block_number, + false, + )?; + } 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, args: UpdateUserPdaArgs) -> Ok(()) } +#[inline(never)] +fn build_candidate_blockchain_for_update( + old_record: &UserRecord, + new_balance: u64, + fields: &UserMutableFields, +) -> Result { + 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 { + 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> { + 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> { 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> { 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> { 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, record: &UserRecord) -> Result< Ok(()) } +fn write_sessions_block(out: &mut Vec, 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, 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, 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 Result { + 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 { 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 { 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 { 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 { } } + 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 { 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(()) } diff --git a/shine-solana/shine/tests/shine.ts b/shine-solana/shine/tests/shine.ts index 4aee55d..d948083 100644 --- a/shine-solana/shine/tests/shine.ts +++ b/shine-solana/shine/tests/shine.ts @@ -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), [] );