seald_sdk_flutter 0.0.1-beta.0 copy "seald_sdk_flutter: ^0.0.1-beta.0" to clipboard
seald_sdk_flutter: ^0.0.1-beta.0 copied to clipboard

Seald SDK for Flutter.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:seald_sdk_flutter/seald_sdk.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path;
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

void main() {
  runApp(const MyApp());
}

class BlinkingWidget extends StatefulWidget {
  const BlinkingWidget({super.key});

  @override
  _BlinkingWidgetState createState() => _BlinkingWidgetState();
}

class _BlinkingWidgetState extends State<BlinkingWidget> {
  bool _isVisible = true;

  @override
  void initState() {
    super.initState();
    // start the blinking animation
    Timer.periodic(const Duration(milliseconds: 500), (timer) {
      setState(() {
        _isVisible = !_isVisible;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: _isVisible ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 50),
      child: Container(
        width: 20.0,
        height: 20.0,
        color: Colors.red,
      ),
    );
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

const Map<String, String> testCredentials = {
  "api_url": "https://api-dev.soyouz.seald.io/",
  "app_id": "00000000-0000-1000-a000-7ea300000018",
  "domain_validation_key_id": "00000000-0000-1000-a000-d11c00000020",
  "domain_validation_key": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
  "jwt_shared_secret_id": "00000000-0000-1000-a000-7ea300000019",
  "jwt_shared_secret": "o75u89og9rxc9me54qxaxvdutr2t4t25ozj4m64utwemm0osld0zdb02j7gv8t7x",
  "debug_api_secret": "f0r_d3bu6",
  "ssks_url": "https://ssks.soyouz.seald.io/",
  "ssks_backend_app_id": "00000000-0000-0000-0000-000000000001",
  "ssks_backend_app_key": "00000000-0000-0000-0000-000000000002",
  "ssks_tmr_challenge": "aaaaaaaa"
};

const String databaseEncryptionKeyB64 =
    "V4olGDOE5bAWNa9HDCvOACvZ59hUSUdKmpuZNyl1eJQnWKs5/l+PGnKUv4mKjivL3BtU014uRAIF2sOl83o6vQ";

String getRegistrationJwt() {
  // Create a json web token
  final JWT jwt = JWT({
    "iss": testCredentials["jwt_shared_secret_id"]!,
    "jti": base64.encode(List<int>.generate(16, (i) => Random.secure().nextInt(256))),
    "iat": DateTime.now().millisecondsSinceEpoch ~/ 1000,
    "scopes": [3],
    "join_team": true
  });

  return jwt.sign(SecretKey(testCredentials["jwt_shared_secret"]!), algorithm: JWTAlgorithm.HS256);
}

String getConnectorJwt(customUserId) {
  // Create a json web token
  final JWT jwt = JWT({
    "iss": testCredentials["jwt_shared_secret_id"]!,
    "jti": base64.encode(List<int>.generate(16, (i) => Random.secure().nextInt(256))),
    "iat": DateTime.now().millisecondsSinceEpoch ~/ 1000,
    "scopes": [4],
    "connector_add": {"type": "AP", "value": "$customUserId@${testCredentials["app_id"]}"}
  });

  return jwt.sign(SecretKey(testCredentials["jwt_shared_secret"]!), algorithm: JWTAlgorithm.HS256);
}

void assertEqual(actual, expected) {
  if (actual != expected) {
    print('Assert fail: expected $expected, got $actual');
    throw AssertionError('Assertion failed');
  }
}

void assertNotEqual(actual, expected) {
  if (actual == expected) {
    print('Assert fail: expected to be not equal to $expected, got $actual');
    throw AssertionError('Assertion failed');
  }
}

void assertThrows(Function func) {
  try {
    func();
  } catch (err) {
    return; // Got expected error
  }
  print('Assert fail: expected function to throw, but succeeded');
  throw AssertionError('Assertion failed');
}

Future<void> assertThrowsAsync(Future<void> Function() func) async {
  try {
    await func();
  } catch (err) {
    return; // Got expected error
  }
  print('Assert fail: expected async function to throw, but succeeded');
  throw AssertionError('Assertion failed');
}

Future<bool> testSealdSdk() async {
  print('Starting tests...');
  try {
    final Directory tmpDir = await path_provider.getTemporaryDirectory();
    final Directory dbDir = Directory(path.join(tmpDir.path, 'seald-test-db'));
    // Delete local database from previous run
    if (dbDir.existsSync()) {
      dbDir.deleteSync(recursive: true);
    }

    final SealdSdk sdk1 = SealdSdk(
      apiURL: testCredentials["api_url"]!,
      appId: testCredentials["app_id"]!,
      dbPath: Directory(path.join(dbDir.path, 'sdk1')).path,
      logLevel: -1,
      instanceName: "Dart1",
      databaseEncryptionKeyB64: databaseEncryptionKeyB64,
    );
    final SealdSdk sdk2 = SealdSdk(
      apiURL: testCredentials["api_url"]!,
      appId: testCredentials["app_id"]!,
      dbPath: Directory(path.join(dbDir.path, 'sdk2')).path,
      logLevel: -1,
      instanceName: "Dart2",
      databaseEncryptionKeyB64: databaseEncryptionKeyB64,
    );
    final SealdSdk sdk3 = SealdSdk(
      apiURL: testCredentials["api_url"]!,
      appId: testCredentials["app_id"]!,
      dbPath: Directory(path.join(dbDir.path, 'sdk3')).path,
      logLevel: -1,
      instanceName: "Dart3",
      databaseEncryptionKeyB64: databaseEncryptionKeyB64,
    );

    // retrieve info about current user before creating a user should return null
    final SealdAccountInfo? retrieveNoAccount = sdk1.getCurrentAccountInfo();
    assertEqual(retrieveNoAccount, null);

    // Create the 3 accounts. Again, the signupJWT should be generated by your backend
    final SealdAccountInfo user1AccountInfo =
        await sdk1.createAccountAsync(getRegistrationJwt(), displayName: "User1", deviceName: "deviceUser1");
    final SealdAccountInfo user2AccountInfo =
        await sdk2.createAccountAsync(getRegistrationJwt(), displayName: "User2", deviceName: "deviceUser2");
    final SealdAccountInfo user3AccountInfo =
        await sdk3.createAccountAsync(getRegistrationJwt(), displayName: "User3", deviceName: "deviceUser3");

    // retrieve info about current user:
    final SealdAccountInfo? retrieveAccountInfo = sdk1.getCurrentAccountInfo();
    assertNotEqual(retrieveAccountInfo, null);
    assertEqual(retrieveAccountInfo?.userId, user1AccountInfo.userId);
    assertEqual(retrieveAccountInfo?.deviceId, user1AccountInfo.deviceId);

    // Create group: https://docs.seald.io/sdk/guides/5-groups.html
    final String groupId = await sdk1
        .createGroupAsync(groupName: "group-1", members: [user1AccountInfo.userId], admins: [user1AccountInfo.userId]);

    // Manage group members and admins
    await sdk1.addGroupMembersAsync(groupId, membersToAdd: [user2AccountInfo.userId]); // Add user2 as group member
    await sdk1.addGroupMembersAsync(groupId,
        membersToAdd: [user3AccountInfo.userId],
        adminsToSet: [user3AccountInfo.userId]); // user1 add user3 as group member and group admin
    await sdk3.removeGroupMembersAsync(groupId, membersToRemove: [user2AccountInfo.userId]); // user3 can remove user2
    await sdk3.setGroupAdminsAsync(groupId,
        addToAdmins: [], removeFromAdmins: [user1AccountInfo.userId]); // user3 can remove user1 from admins

    // Create encryption session: https://docs.seald.io/sdk/guides/6-encryption-sessions.html
    final EncryptionSession es1SDK1 = await sdk1.createEncryptionSessionAsync(
        [user1AccountInfo.userId, user2AccountInfo.userId, groupId]); // user1, user2, and group as recipients

    // The EncryptionSession object can encrypt and decrypt for user1
    const String initialString = "a message that needs to be encrypted!";
    final String encryptedMessage = await es1SDK1.encryptMessageAsync(initialString);
    final String decryptedMessage = await es1SDK1.decryptMessageAsync(encryptedMessage);
    assertEqual(decryptedMessage, initialString);

    // user1 can retrieve the EncryptionSession from the encrypted message
    final EncryptionSession es1SDK1RetrieveFromMess =
        await sdk1.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: true);
    final String decryptedMessageFromMess = await es1SDK1RetrieveFromMess.decryptMessageAsync(encryptedMessage);
    assertEqual(decryptedMessageFromMess, initialString);

    // user2 and user3 can retrieve the encryptionSession (from the encrypted message or the session ID).
    final EncryptionSession es1SDK2 = await sdk2.retrieveEncryptionSessionAsync(sessionId: es1SDK1.id, useCache: true);
    final String decryptedMessageSDK2 = await es1SDK2.decryptMessageAsync(encryptedMessage);
    assertEqual(decryptedMessageSDK2, initialString);

    final EncryptionSession es1SDK3FromGroup =
        await sdk3.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: true);
    final String decryptedMessageSDK3 = await es1SDK3FromGroup.decryptMessageAsync(encryptedMessage);
    assertEqual(decryptedMessageSDK3, initialString);

    // user3 removes all members of "group-1". A group without member is deleted.
    await sdk3.removeGroupMembersAsync(groupId, membersToRemove: [user1AccountInfo.userId, user3AccountInfo.userId]);

    // user3 could retrieve the previous encryption session only because "group-1" was set as recipient.
    // As the group was deleted, it can no longer access it.
    // user3 still has the encryption session in its cache, but we can disable it.
    await assertThrowsAsync(
        () async => sdk3.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: false));

    // user2 adds user3 as recipient of the encryption session.
    await es1SDK2.addRecipientsAsync([user3AccountInfo.userId]);

    // user3 can now retrieve it.
    final EncryptionSession es1SDK3 = await sdk3.retrieveEncryptionSessionAsync(sessionId: es1SDK1.id, useCache: false);
    final String decryptedMessageAfterAdd = await es1SDK3.decryptMessageAsync(encryptedMessage);
    assertEqual(decryptedMessageAfterAdd, initialString);

    // user1 revokes user3 from the encryption session.
    await es1SDK1.revokeRecipientsAsync([user3AccountInfo.userId]); // TODO: used to be user2, but new ACLs break it

    // user3 cannot retrieve the session anymore
    await assertThrowsAsync(
        () async => sdk3.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: false));

    // user1 revokes all other recipients from the session
    await es1SDK1.revokeOthersAsync();

    // user2 cannot retrieve the session anymore
    await assertThrowsAsync(
        () async => sdk2.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: false));

    // user1 revokes all. It can no longer retrieve it.
    await es1SDK1.revokeAllAsync();
    await assertThrowsAsync(
        () async => sdk1.retrieveEncryptionSessionAsync(message: encryptedMessage, useCache: false));

    // Create additional data for user1
    final EncryptionSession es2SDK1 =
        await sdk1.createEncryptionSessionAsync([user1AccountInfo.userId], useCache: true);
    const String anotherMessage = "nobody should read that!";
    final String secondEncryptedMessage = await es2SDK1.encryptMessageAsync(anotherMessage);

    // user1 can renew its key, and still decrypt old messages
    await sdk1.renewKeysAsync(expireAfter: const Duration(days: 365 * 5));
    final EncryptionSession es2SDK1AfterRenew =
        await sdk1.retrieveEncryptionSessionAsync(sessionId: es2SDK1.id, useCache: false);
    final String decryptedMessageAfterRenew = await es2SDK1AfterRenew.decryptMessageAsync(secondEncryptedMessage);
    assertEqual(decryptedMessageAfterRenew, anotherMessage);

    // CONNECTORS https://docs.seald.io/en/sdk/guides/jwt.html#adding-a-userid

    // we can add a custom userId using a JWT
    const String customConnectorJWTValue = "user1-custom-id";
    await sdk1.pushJWTAsync(getConnectorJwt(customConnectorJWTValue));

    final List<SealdConnector> connectors = await sdk1.listConnectorsAsync();
    assertEqual(connectors.length, 1);
    assertEqual(connectors[0].state, "VO");
    assertEqual(connectors[0].type, "AP");
    assertEqual(connectors[0].sealdId, user1AccountInfo.userId);
    assertEqual(connectors[0].value, "$customConnectorJWTValue@${testCredentials['app_id']}");

    // Retrieve connector by its id
    final SealdConnector retrieveConnector = await sdk1.retrieveConnectorAsync(connectors[0].id);
    assertEqual(retrieveConnector.sealdId, user1AccountInfo.userId);
    assertEqual(retrieveConnector.state, "VO");
    assertEqual(retrieveConnector.type, "AP");
    assertEqual(retrieveConnector.value, "$customConnectorJWTValue@${testCredentials['app_id']}");

    // Retrieve connectors from a user id.
    final List<SealdConnector> connectorsFromSealdId =
        await sdk1.getConnectorsFromSealdIdAsync(user1AccountInfo.userId);
    assertEqual(connectorsFromSealdId.length, 1);
    assertEqual(connectorsFromSealdId[0].state, "VO");
    assertEqual(connectorsFromSealdId[0].type, "AP");
    assertEqual(connectorsFromSealdId[0].sealdId, user1AccountInfo.userId);
    assertEqual(connectorsFromSealdId[0].value, "$customConnectorJWTValue@${testCredentials['app_id']}");

    // Get sealdId of a user from a connector
    final List<String> sealdIds = await sdk2.getSealdIdsFromConnectorsAsync(
        [SealdConnectorTypeValue(type: "AP", value: "$customConnectorJWTValue@${testCredentials['app_id']}")]);
    assertEqual(sealdIds.length, 1);
    assertEqual(sealdIds[0], user1AccountInfo.userId);

    // user1 can remove a connector
    await sdk1.removeConnectorAsync(connectors[0].id);

    // verify that only one connector left
    final List<SealdConnector> connectorListAfterRevoke = await sdk1.listConnectorsAsync();
    assertEqual(connectorListAfterRevoke.length, 0);

    // user1 can export its identity
    final Uint8List exportIdentity = sdk1.exportIdentity();

    // We can instantiate a new SealdSDK, import the exported identity
    final SealdSdk sdk1Exported = SealdSdk(
      apiURL: testCredentials["api_url"]!,
      appId: testCredentials["app_id"]!,
      dbPath: Directory(path.join(dbDir.path, 'sdk1exported')).path,
      logLevel: -1,
      instanceName: "Dart1Exported",
      databaseEncryptionKeyB64: databaseEncryptionKeyB64,
    );
    await sdk1Exported.importIdentityAsync(exportIdentity);

    // SDK with imported identity can decrypt
    final EncryptionSession es2SDK1Exported =
        await sdk1Exported.retrieveEncryptionSessionAsync(message: secondEncryptedMessage);
    final String clearMessageExportedIdentity = await es2SDK1Exported.decryptMessageAsync(secondEncryptedMessage);
    assertEqual(clearMessageExportedIdentity, anotherMessage);
    sdk1Exported.close();

    // user1 can create sub identity
    final SealdCreateSubIdentityResponse subIdentity = await sdk1.createSubIdentityAsync(deviceName: "SUB-deviceName");
    assertNotEqual(subIdentity.deviceId, "");

    // first device needs to reencrypt for the new device
    await sdk1.massReencryptAsync(subIdentity.deviceId);
    // We can instantiate a new SealdSDK, import the sub-device identity
    final SealdSdk sdk1SubDevice = SealdSdk(
      apiURL: testCredentials["api_url"]!,
      appId: testCredentials["app_id"]!,
      dbPath: Directory(path.join(dbDir.path, 'sdk1SubDevice')).path,
      logLevel: -1,
      instanceName: "Dart1SubDevice",
      databaseEncryptionKeyB64: databaseEncryptionKeyB64,
    );
    await sdk1SubDevice.importIdentityAsync(subIdentity.backupKey);

    // sub device can decrypt
    final EncryptionSession es2SDK1SubDevice =
        await sdk1SubDevice.retrieveEncryptionSessionAsync(message: secondEncryptedMessage, useCache: false);
    final String clearMessageSubdIdentity = await es2SDK1SubDevice.decryptMessageAsync(secondEncryptedMessage);
    assertEqual(clearMessageSubdIdentity, anotherMessage);
    sdk1SubDevice.close();

    await sdk1.heartbeatAsync();

    sdk1.close();
    sdk2.close();
    sdk3.close();

    print('Test success!');
    return true;
  } catch (err) {
    print('Test failed');
    print(err);
    return false;
  }
}

class _MyAppState extends State<MyApp> {
  late Future<bool> testResult;

  @override
  void initState() {
    super.initState();
    testResult = testSealdSdk();
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 25);
    const spacerSmall = SizedBox(height: 10);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Native Packages'),
        ),
        body: SingleChildScrollView(
          child: Container(
            padding: const EdgeInsets.all(10),
            child: Column(
              children: [
                BlinkingWidget(),
                spacerSmall,
                FutureBuilder<bool>(
                  future: testResult,
                  builder: (BuildContext context, AsyncSnapshot<bool> value) {
                    final String displayValue = (value.hasData) ? (value.data! ? 'success' : 'fail') : 'running';
                    return Text(
                      'Tests: $displayValue',
                      style: textStyle,
                      textAlign: TextAlign.center,
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}