diff --git a/.gitignore b/.gitignore index 6c0a6fb75..ba762d031 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,15 @@ testem.log Thumbs.db .nx -.vscode \ No newline at end of file +.vscode + +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +# env files +.env* +!.env.example diff --git a/README.md b/README.md index 61ca29466..20dd41c2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Affinidi Trust Development Kit (Affinidi TDK) + + [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) + The Affinidi Trust Development Kit (Affinidi TDK) is a modern interface that allows you to easily manage and integrate [Affinidi Elements](https://www.affinidi.com/product/affinidi-elements) and [Frameworks](https://www.affinidi.com/developer#lota-framework) into your application. It minimises dependencies and enables developers seamless entry into the [Affinidi Trust Network (ATN)](https://www.affinidi.com/get-started). @@ -17,25 +20,38 @@ Each module has its own README that you can check to better understand how to in ## Available modules -The Affinidi TDK offers the following modules and support to programming languages: - - - -| | TypeScript | Python | Dart | PHP | -|----------- |-----------------------|-----------------------|----------------------|---------------------| -|**Packages** | | | | -|[auth-provider](packages/auth-provider/) | 🟢 | 🟢 | 🔴 | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/AuthProvider) | -|[common](packages/common/) | 🟢 | 🟢 | 🔴 | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Common) | -|**Packages** | | | | -|credential-issuance-client | [🟢 Link](clients/typescript/credential-issuance-client/) | [🟢 Link](clients/python/credential_issuance_client/) | [🟡 Link](clients/dart/credential_issuance_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/CredentialIssuanceClient) | -|credential-verification-client | [🟢 Link](clients/typescript/credential-verification-client/) | [🟢 Link](clients/python/credential_verification_client/) | [🟡 Link](clients/dart/credential_verification_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/CredentialVerificationClient) | -|iam-client | [🟢 Link](clients/typescript/iam-client/) | [🟢 Link](clients/python/iam_client/) | [🟡 Link](clients/dart/iam_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/IamClient) | -|iota-client | [🟢 Link](clients/typescript/iota-client/) | [🟢 Link](clients/python/iota_client/) | [🟡 Link](clients/dart/iota_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/IotaClient) | -|login-configuration-client | [🟢 Link](clients/typescript/login-configuration-client/) | [🟢 Link](clients/python/login_configuration_client/) | [🟡 Link](clients/dart/login_configuration_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/LoginConfigurationClient) | -|wallets-client | [🟢 Link](clients/typescript/wallets-client/) | [🟢 Link](clients/python/wallets_client/) | [🟡 Link](clients/dart/wallets_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/WalletsClient) | -|**Libraries** | | | | -|[iota-browser](libs/iota-browser/) | 🟢 | 🔴 | 🔴 | 🔴 | -|[iota-core](libs/iota-core/) | 🟢 | 🟢 | 🔴 | 🔴 | +The Affinidi TDK offers several modules depending on the type of application you are using and the programming language. + +### For vault applications + +If you are building a vault application that manages user's data, you will be interested in the following packages: + +| | TypeScript | Dart | +| ------------------------- | -------------------------------------------------------- | -------------------------------------------------- | +| **Packages** | | | +| consumer-auth-provider | 🔴 | [🟡 Link](packages/dart/consumer_auth_provider/) | +| **Clients** | | | +| vault-data-manager-client | [🟡 Link](clients/typescript/vault-data-manager-client/) | [🟡 Link](clients/dart/vault_data_manager_client/) | + +### For issuer/verifier applications + +If you are building a site that issues or requests data from the user vaults you will be interested in the following packages: + +| | TypeScript | Python | Dart | PHP | +| ------------------------------ | ------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| **Packages** | | | | +| auth-provider | [🟢 Link](packages/jsii/auth-provider/) | 🟢 | [🟡 Link](packages/dart/auth_provider/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/AuthProvider) | +| common | [🟢 Link](packages/jsii/common/) | 🟢 | 🔴 | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Common) | +| **Clients** | | | | +| credential-issuance-client | [🟢 Link](clients/typescript/credential-issuance-client/) | [🟢 Link](clients/python/credential_issuance_client/) | [🟡 Link](clients/dart/credential_issuance_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/CredentialIssuanceClient) | +| credential-verification-client | [🟢 Link](clients/typescript/credential-verification-client/) | [🟢 Link](clients/python/credential_verification_client/) | [🟡 Link](clients/dart/credential_verification_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/CredentialVerificationClient) | +| iam-client | [🟢 Link](clients/typescript/iam-client/) | [🟢 Link](clients/python/iam_client/) | [🟡 Link](clients/dart/iam_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/IamClient) | +| iota-client | [🟢 Link](clients/typescript/iota-client/) | [🟢 Link](clients/python/iota_client/) | [🟡 Link](clients/dart/iota_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/IotaClient) | +| login-configuration-client | [🟢 Link](clients/typescript/login-configuration-client/) | [🟢 Link](clients/python/login_configuration_client/) | [🟡 Link](clients/dart/login_configuration_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/LoginConfigurationClient) | +| wallets-client | [🟢 Link](clients/typescript/wallets-client/) | [🟢 Link](clients/python/wallets_client/) | [🟡 Link](clients/dart/wallets_client/) | [🟢 Link](https://github.com/affinidi/affinidi-tdk-php/tree/main/src/Clients/WalletsClient) | +| **Libraries** | | | | +| iota-browser | [🟢 Link](libs/iota-browser/) | 🔴 | 🔴 | 🔴 | +| iota-core | [🟢 Link](libs/iota-core/) | 🟢 | 🔴 | 🔴 |
🟢 Supported
diff --git a/packages/dart/.gitignore b/packages/dart/.gitignore new file mode 100644 index 000000000..4a33ddae8 --- /dev/null +++ b/packages/dart/.gitignore @@ -0,0 +1 @@ +.fvmrc \ No newline at end of file diff --git a/packages/dart/auth_provider/.env.example b/packages/dart/auth_provider/.env.example new file mode 100644 index 000000000..fdbc65d24 --- /dev/null +++ b/packages/dart/auth_provider/.env.example @@ -0,0 +1,10 @@ +# Personal access token +PROJECT_ID="" +TOKEN_ID="" +PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" +PASSPHRASE="" # Optional. Required if private key is encrypted +KEY_ID="" # Optional. Required if token's key id is different from token id + +# Iota (Websocket) +IOTA_CONFIG_ID="" +DID="" # Usually obtained at runtime from the registered user \ No newline at end of file diff --git a/packages/dart/auth_provider/.gitignore b/packages/dart/auth_provider/.gitignore new file mode 100644 index 000000000..5fe01895a --- /dev/null +++ b/packages/dart/auth_provider/.gitignore @@ -0,0 +1,11 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +# env files +.env* +!.env.example diff --git a/packages/dart/auth_provider/CHANGELOG.md b/packages/dart/auth_provider/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/packages/dart/auth_provider/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/dart/auth_provider/README.md b/packages/dart/auth_provider/README.md new file mode 100644 index 000000000..8b55e735b --- /dev/null +++ b/packages/dart/auth_provider/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/dart/auth_provider/analysis_options.yaml b/packages/dart/auth_provider/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/packages/dart/auth_provider/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/dart/auth_provider/example/iota_token.dart b/packages/dart/auth_provider/example/iota_token.dart new file mode 100644 index 000000000..8d3263da2 --- /dev/null +++ b/packages/dart/auth_provider/example/iota_token.dart @@ -0,0 +1,33 @@ +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; +import 'package:dotenv/dotenv.dart'; + +void main() async { + var env = DotEnv()..load(); + if (!env.isEveryDefined( + ['PROJECT_ID', 'TOKEN_ID', 'PRIVATE_KEY', 'IOTA_CONFIG_ID', 'DID'])) { + print( + 'Missing environment variables. Please provide PROJECT_ID, TOKEN_ID, PRIVATE_KEY, DID'); + return; + } + // Workaround for dotenv multiline limitations + final privateKey = env['PRIVATE_KEY']!.replaceAll('\\n', '\n'); + + final provider = AuthProvider( + projectId: env['PROJECT_ID']!, + tokenId: env['TOKEN_ID']!, + privateKey: privateKey, + // Optional parameters + keyId: env['KEY_ID'], + passphrase: env['PASSPHRASE'], + ); + + try { + // Fetch iota token (websocket). Did is usually obtained at runtime from the registered user + final iotaToken = provider.createIotaToken( + iotaConfigId: env['IOTA_CONFIG_ID']!, did: env['DID']!); + print('Successfully obtained iota token:'); + print(iotaToken); + } catch (e) { + print('Error obtaining token: $e'); + } +} diff --git a/packages/dart/auth_provider/example/project_scoped_token.dart b/packages/dart/auth_provider/example/project_scoped_token.dart new file mode 100644 index 000000000..151799d1c --- /dev/null +++ b/packages/dart/auth_provider/example/project_scoped_token.dart @@ -0,0 +1,31 @@ +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; +import 'package:dotenv/dotenv.dart'; + +void main() async { + final env = DotEnv(includePlatformEnvironment: true)..load(); + if (!env.isEveryDefined(['PROJECT_ID', 'TOKEN_ID', 'PRIVATE_KEY'])) { + print( + 'Missing environment variables. Please provide PROJECT_ID, TOKEN_ID, PRIVATE_KEY'); + return; + } + // Workaround for dotenv multiline limitations + final privateKey = env['PRIVATE_KEY']!.replaceAll('\\n', '\n'); + + final provider = AuthProvider( + projectId: env['PROJECT_ID']!, + tokenId: env['TOKEN_ID']!, + privateKey: privateKey, + // Optional parameters + keyId: env['KEY_ID'], + passphrase: env['PASSPHRASE'], + ); + + try { + // Fetch project scoped token + final projectScopedToken = await provider.fetchProjectScopedToken(); + print('Successfully obtained project scoped token:'); + print(projectScopedToken); + } catch (e) { + print('Error obtaining token: $e'); + } +} diff --git a/packages/dart/auth_provider/lib/affinidi_tdk_auth_provider.dart b/packages/dart/auth_provider/lib/affinidi_tdk_auth_provider.dart new file mode 100644 index 000000000..d761bb676 --- /dev/null +++ b/packages/dart/auth_provider/lib/affinidi_tdk_auth_provider.dart @@ -0,0 +1,8 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/auth_provider.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/packages/dart/auth_provider/lib/src/auth_provider.dart b/packages/dart/auth_provider/lib/src/auth_provider.dart new file mode 100644 index 000000000..1f58209c8 --- /dev/null +++ b/packages/dart/auth_provider/lib/src/auth_provider.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:affinidi_tdk_common/affinidi_tdk_common.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; + +import 'iam_client.dart'; +import 'jwt_helper.dart'; + +class AuthProvider { + final String projectId; + final String tokenId; + final String privateKey; + final String? keyId; + final String? passphrase; + final String apiGatewayUrl; + final String tokenEndpoint; + + AuthProvider({ + required this.projectId, + required this.tokenId, + required this.privateKey, + this.keyId, + this.passphrase, + }) : apiGatewayUrl = Environment.fetchApiGwUrl(), + tokenEndpoint = Environment.fetchElementsAuthTokenUrl(); + + AuthProvider.withEnv({ + required this.projectId, + required this.tokenId, + required this.privateKey, + this.keyId, + this.passphrase, + required this.apiGatewayUrl, + required this.tokenEndpoint, + }); + + ECPublicKey? _publicKey; + String? _projectScopedToken; + + Future _shouldFetchToken() async { + if (_projectScopedToken == null) { + return true; + } + + final iamClient = IamClient(apiGatewayUrl: apiGatewayUrl); + _publicKey ??= await JWTHelper.fetchPublicKey(iamClient); + try { + JWT.verify(_projectScopedToken!, _publicKey!); + return false; + } on JWTException { + return true; + } + } + + Future fetchProjectScopedToken() async { + if (await _shouldFetchToken()) { + _projectScopedToken = + await _getProjectScopedToken(audience: tokenEndpoint); + } + return _projectScopedToken!; + } + + Future _getUserAccessToken({required String audience}) async { + final token = JWTHelper.signPayload( + audience: audience, + tokenId: tokenId, + privateKey: privateKey, + keyId: keyId, + passphrase: passphrase, + ); + + final response = await http.post( + Uri.parse(audience), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: { + 'grant_type': 'client_credentials', + 'scope': 'openid', + 'client_assertion_type': + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion': token, + 'client_id': tokenId, + }, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to get user access token'); + } + + final data = jsonDecode(response.body); + return data['access_token']; + } + + Future _getProjectScopedToken({ + required String audience, + }) async { + final userAccessToken = await _getUserAccessToken(audience: audience); + + final response = await http.post( + Uri.parse('$apiGatewayUrl/iam/v1/sts/create-project-scoped-token'), + headers: { + 'Authorization': 'Bearer $userAccessToken', + 'Content-Type': 'application/json', + }, + body: jsonEncode({'projectId': projectId}), + ); + + if (response.statusCode != 200) { + throw Exception('Failed to get project scoped token'); + } + + final data = jsonDecode(response.body); + return data['accessToken']; + } + + ({String iotaJwt, String iotaSessionId}) createIotaToken({ + required String iotaConfigId, + required String did, + String? iotaSessionId, + }) { + final sessionId = iotaSessionId ?? Uuid().v4(); + + return ( + iotaJwt: JWTHelper.signPayload( + audience: did, + tokenId: 'token/$tokenId', + privateKey: privateKey, + keyId: keyId, + passphrase: passphrase, + additionalPayload: { + 'project_id': projectId, + 'iota_configuration_id': iotaConfigId, + 'iota_session_id': sessionId, + 'scope': 'iota_channel', + }, + ), + iotaSessionId: sessionId, + ); + } +} diff --git a/packages/dart/auth_provider/lib/src/iam_client.dart b/packages/dart/auth_provider/lib/src/iam_client.dart new file mode 100644 index 000000000..8f452b731 --- /dev/null +++ b/packages/dart/auth_provider/lib/src/iam_client.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +class IamClient { + IamClient({ + required String apiGatewayUrl, + http.Client? httpClient, + }) : _httpClient = httpClient ?? http.Client(), + _apiGatewayUrl = apiGatewayUrl; + + final http.Client _httpClient; + final String _apiGatewayUrl; + + Future> getPublicKeyJWKS() async { + final response = await _httpClient.get( + Uri.parse('$_apiGatewayUrl/iam/.well-known/jwks.json'), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to fetch public key'); + } + + final data = jsonDecode(response.body); + + if (data['keys'] == null || data['keys'].isEmpty) { + throw Exception('No keys found in JWKS'); + } + + return data['keys'][0]; + } +} diff --git a/packages/dart/auth_provider/lib/src/jwt_helper.dart b/packages/dart/auth_provider/lib/src/jwt_helper.dart new file mode 100644 index 000000000..2ed2b474b --- /dev/null +++ b/packages/dart/auth_provider/lib/src/jwt_helper.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:uuid/uuid.dart'; +import 'package:pointycastle/export.dart' as pce; + +import 'iam_client.dart'; + +class JWTHelper { + static String signPayload({ + required String audience, + required String tokenId, + required String privateKey, + String? keyId, + String? passphrase, // TODO: Implement passphrase + dynamic additionalPayload, + }) { + final issueTimeInSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final algorithm = JWTAlgorithm.RS256; + + final jwt = JWT( + { + 'iss': tokenId, + 'sub': tokenId, + 'aud': audience, + 'jti': Uuid().v4(), + 'exp': issueTimeInSeconds + 5 * 60, + 'iat': issueTimeInSeconds, + if (additionalPayload != null) ...additionalPayload, + }, + header: { + 'alg': algorithm.name, + if (keyId != null) 'kid': keyId, + }, + ); + + final token = jwt.sign( + RSAPrivateKey(privateKey), + algorithm: algorithm, + ); + + return token; + } + + static Future fetchPublicKey(IamClient iamClient) async { + final jwk = await iamClient.getPublicKeyJWKS(); + return ECPublicKey.raw(_jwkToPublicKey(jwk)); + } + + static pce.ECPublicKey _jwkToPublicKey(Map jwk) { + if (jwk['alg'] != 'ES256' || jwk['kty'] != 'EC' || jwk['crv'] != 'P-256') { + throw UnimplementedError('Unsupported algorithm or key type'); + } + + if (jwk['x'] == null || jwk['y'] == null) { + throw Exception('Invalid public key'); + } + + // Decode base64url-encoded x and y coordinates + final Uint8List x = base64Url.decode(_addPadding(jwk['x']!)); + final Uint8List y = base64Url.decode(_addPadding(jwk['y']!)); + + // Create the EC domain parameters for P-256 (secp256r1) + final curve = pce.ECCurve_secp256r1(); + + // Convert Uint8List to BigInt + final xBigInt = _decodeBigInt(x); + final yBigInt = _decodeBigInt(y); + + // Create EC point from x,y coordinates + final q = curve.curve.createPoint(xBigInt, yBigInt); + + // Create and return the public key + return pce.ECPublicKey(q, curve); + } + + static String _addPadding(String value) { + final padding = (4 - (value.length % 4)) % 4; + return value + ('=' * padding); + } + + static BigInt _decodeBigInt(Uint8List bytes) { + BigInt result = BigInt.zero; + for (var i = 0; i < bytes.length; i++) { + result += BigInt.from(bytes[i]) << (8 * (bytes.length - i - 1)); + } + return result; + } +} diff --git a/packages/dart/auth_provider/pubspec.yaml b/packages/dart/auth_provider/pubspec.yaml new file mode 100644 index 000000000..2b7fd27f7 --- /dev/null +++ b/packages/dart/auth_provider/pubspec.yaml @@ -0,0 +1,23 @@ +name: affinidi_tdk_auth_provider +description: Provider of project scoped token +version: 1.0.0 +repository: https://github.com/affinidi/affinidi-tdk + +environment: + sdk: ^3.6.0 + +resolution: workspace + +dependencies: + affinidi_tdk_common: ^1.0.0 + dart_jsonwebtoken: ^2.12.0 + pointycastle: ^3.9.1 + http: ^1.1.0 + uuid: ^4.5.1 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 + dotenv: ^4.2.0 + mocktail: ^1.0.4 + path: ^1.9.1 diff --git a/packages/dart/auth_provider/test/auth_provider_test.dart b/packages/dart/auth_provider/test/auth_provider_test.dart new file mode 100644 index 000000000..75efda70d --- /dev/null +++ b/packages/dart/auth_provider/test/auth_provider_test.dart @@ -0,0 +1,74 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; + +void main() { + group('Auth Provider Tests', () { + final mockProjectId = 'test-project-id'; + final mockTokenId = 'test-token-id'; + late AuthProvider authProvider; + + setUp(() { + final testDir = Directory.current.path; + final keyOpensslRSA2048 = + File(path.join(testDir, 'test', 'pem', 'openssl-rsa2048.pem')) + .readAsStringSync(); + authProvider = AuthProvider( + projectId: mockProjectId, + tokenId: mockTokenId, + privateKey: keyOpensslRSA2048); + }); + + test('createIotaToken returns valid token and session', () { + final iotaConfigId = 'test-iota-config'; + final did = 'did:test:123'; + final sessionId = 'test-session'; + + final result = authProvider.createIotaToken( + iotaConfigId: iotaConfigId, did: did, iotaSessionId: sessionId); + + expect(result.iotaSessionId, equals(sessionId)); + expect(result.iotaJwt, isNotEmpty); + + final jwt = JWT.decode(result.iotaJwt); + expect(jwt.payload['aud'], equals(did)); + expect(jwt.payload['iss'], equals('token/$mockTokenId')); + expect(jwt.payload['sub'], equals('token/$mockTokenId')); + expect(jwt.payload['jti'], isNotEmpty); + expect(jwt.payload['iat'], isA()); + expect(jwt.payload['exp'], isA()); + expect(jwt.payload['exp'] - jwt.payload['iat'], equals(5 * 60)); + expect(jwt.payload['project_id'], equals(mockProjectId)); + expect(jwt.payload['iota_configuration_id'], equals(iotaConfigId)); + expect(jwt.payload['iota_session_id'], equals(sessionId)); + expect(jwt.payload['scope'], equals('iota_channel')); + }); + + test('createIotaToken generates session ID if not provided', () { + final iotaConfigId = 'test-iota-config'; + final did = 'did:test:123'; + + final result = + authProvider.createIotaToken(iotaConfigId: iotaConfigId, did: did); + + expect(result.iotaSessionId, isNotEmpty); + expect(result.iotaJwt, isNotEmpty); + + final jwt = JWT.decode(result.iotaJwt); + expect(jwt.payload['aud'], equals(did)); + expect(jwt.payload['iss'], equals('token/$mockTokenId')); + expect(jwt.payload['sub'], equals('token/$mockTokenId')); + expect(jwt.payload['jti'], isNotEmpty); + expect(jwt.payload['iat'], isA()); + expect(jwt.payload['exp'], isA()); + expect(jwt.payload['exp'] - jwt.payload['iat'], equals(5 * 60)); + expect(jwt.payload['project_id'], equals(mockProjectId)); + expect(jwt.payload['iota_configuration_id'], equals(iotaConfigId)); + expect(jwt.payload['iota_session_id'], equals(result.iotaSessionId)); + expect(jwt.payload['scope'], equals('iota_channel')); + }); + }); +} diff --git a/packages/dart/auth_provider/test/jwt_helper_test.dart b/packages/dart/auth_provider/test/jwt_helper_test.dart new file mode 100644 index 000000000..b33ac0251 --- /dev/null +++ b/packages/dart/auth_provider/test/jwt_helper_test.dart @@ -0,0 +1,223 @@ +import 'dart:io'; + +import 'package:affinidi_tdk_auth_provider/src/iam_client.dart'; +import 'package:affinidi_tdk_auth_provider/src/jwt_helper.dart'; +import 'package:test/test.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:path/path.dart' as path; +import 'package:mocktail/mocktail.dart'; + +class MockIamClient extends Mock implements IamClient {} + +void validateJWTClaims(String token, String audience, String tokenId) { + final jwt = JWT.decode(token); + + expect(jwt.payload['aud'], equals(audience)); + expect(jwt.payload['iss'], equals(tokenId)); + expect(jwt.payload['sub'], equals(tokenId)); + expect(jwt.payload['jti'], isNotEmpty); + expect(jwt.payload['iat'], isA()); + expect(jwt.payload['exp'], isA()); + expect(jwt.payload['exp'] - jwt.payload['iat'], equals(5 * 60)); + + expect(jwt.header!['alg'], equals('RS256')); +} + +void main() { + group('JWTHelper Tests', () { + final mockAudience = "test-audience"; + final mockTokenId = "test-token"; + group('signPayload with unencrypted private keys', () { + late String keyOpensslRsa2048; + late String keyOpensslRsa4096; + late String keySshRsa2048; + late String keySshRsa4096; + + setUp(() { + final testDir = Directory.current.path; + keyOpensslRsa2048 = + File(path.join(testDir, 'test', 'pem', 'openssl-rsa2048.pem')) + .readAsStringSync(); + keyOpensslRsa4096 = + File(path.join(testDir, 'test', 'pem', 'openssl-rsa4096.pem')) + .readAsStringSync(); + keySshRsa2048 = + File(path.join(testDir, 'test', 'pem', 'ssh-rsa2048.pem')) + .readAsStringSync(); + keySshRsa4096 = + File(path.join(testDir, 'test', 'pem', 'ssh-rsa4096.pem')) + .readAsStringSync(); + }); + test('creates JWT from OpenSSL RSA 2048 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }); + + test('creates JWT from OpenSSL RSA 4096 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa4096, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }); + + test('creates JWT from ssh-keygen RSA 2048 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keySshRsa2048, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'ssh-keygen key tags are not yet supported'); + + test('creates JWT from ssh-keygen RSA 4096 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keySshRsa4096, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'ssh-keygen key tags are not yet supported'); + + test('includes keyId in header when provided', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048, + keyId: 'test-key-id', + ); + final jwt = JWT.decode(token); + expect(jwt.header!['kid'], equals('test-key-id')); + }); + + test('includes additional payload when provided', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048, + additionalPayload: { + 'custom_claim': 'custom_value', + 'nested_claim': {'key': 'value'} + }, + ); + + final jwt = JWT.decode(token); + expect(jwt.payload['custom_claim'], equals('custom_value')); + expect(jwt.payload['nested_claim'], equals({'key': 'value'})); + }); + + test('throws error with invalid private key', () { + expect( + () => JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: 'invalid-key', + ), + throwsA(isA())); + }); + }); + + group('signPayload with encrypted private keys', () { + final String passphrase = 'hello'; + late String keyOpensslRsa2048Aes128; + late String keyOpensslRsa2048Aes192; + late String keyOpensslRsa2048Aes256; + late String keySshRsa4096Encrypted; + + setUp(() { + final testDir = Directory.current.path; + keyOpensslRsa2048Aes128 = File( + path.join(testDir, 'test', 'pem', 'openssl-rsa2048-aes128.pem')) + .readAsStringSync(); + keyOpensslRsa2048Aes192 = File( + path.join(testDir, 'test', 'pem', 'openssl-rsa2048-aes192.pem')) + .readAsStringSync(); + keyOpensslRsa2048Aes256 = File( + path.join(testDir, 'test', 'pem', 'openssl-rsa2048-aes256.pem')) + .readAsStringSync(); + keySshRsa4096Encrypted = + File(path.join(testDir, 'test', 'pem', 'ssh-rsa4096-encrypted.pem')) + .readAsStringSync(); + }); + + test('creates JWT from OpenSSL AES-128 encrypted RSA 2048 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048Aes128, + passphrase: passphrase, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'encrypted keys are not yet supported'); + + test('creates JWT from OpenSSL AES-192 encrypted RSA 2048 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048Aes192, + passphrase: passphrase, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'encrypted keys are not yet supported'); + + test('creates JWT from OpenSSL AES-256 encrypted RSA 2048 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keyOpensslRsa2048Aes256, + passphrase: passphrase, + ); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'encrypted keys are not yet supported'); + + test('creates JWT from ssh-keygen encrypted RSA 4096 key', () { + final token = JWTHelper.signPayload( + audience: mockAudience, + tokenId: mockTokenId, + privateKey: keySshRsa4096Encrypted, + passphrase: passphrase); + expect(token, isNotEmpty); + validateJWTClaims(token, mockAudience, mockTokenId); + }, skip: 'ssh-keygen key tags are not yet supported'); + }); + + group('Affinidi Elements Public Key', () { + late IamClient iamClient; + + setUp(() { + iamClient = MockIamClient(); + }); + + test('successfully parses JWKS', () async { + final mockPublicKeyJWKS = { + 'kid': 'a622a999-9846-48cf-a470-22759e1f435a', + 'alg': 'ES256', + 'use': 'sig', + 'kty': 'EC', + 'crv': 'P-256', + 'x': 'b3kdYEBrlWjQwY55F8MhXC97pwkjTpcQZZ09oDDBK4c', + 'y': 'wlopQwIPWuT55M3ZfCDZdoBs1nh2kwEvzPjnkakf96U' + }; + + when(() => iamClient.getPublicKeyJWKS()).thenAnswer( + (_) async => mockPublicKeyJWKS, + ); + + final publicKey = await JWTHelper.fetchPublicKey(iamClient); + expect(publicKey, isA()); + }); + }); + }); +} diff --git a/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes128.pem b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes128.pem new file mode 100644 index 000000000..59ca7f7b6 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes128.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ8KJC9coAjae+YbnN +5D3/DgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEMY+Dtssy+iN8uMn +bRwTi5YEggTQG7Fq68P7Xmd5+hvfOHhjaZzsaYh24VmcEUmfv/DeKkJVJCeLWaeK +iLbZUjtiCUBl7U3Dulgfmf6LWrbAnLlfpPb792N5xjTOtChNilFhNPK3eBW6Ax3F +Im34Q9D+Hq7Ogmw7d2cVCC0MTaCH88PGt+3YjGZlNErvvECjn1Dl3xJ/WieAGBjw +dCHaABGyd87LjF4ytu60nbkv0isPsFxHUqevrNGRPlPDX8HfUlOoQlifXF0CRkC4 +99tOcsg6HYIqrgKoUXL5SGm/IgG8ItgLiBznPkOWyKk3J5giyt6wSpKfCTDjVRLR +IbIuLO5c8Toosj47+yt3gt9RwIsTU9O0ouPwer8C8LQUerGUxvU84qFzoakGW9/W +r0lLmRqqwNid9EVTVEMIYCDZCnJXWc9p4zJJzndlcqeGEgUUZHmm4UiNmuXANUqE +kNx29VzP93cERn2zMxLNnXie3Z7KhHXmHTOtqIu/avsNG1ZTOP5Crt5ue4Kkrbjv +YNsWtoYjOCoHCRjBi1hveaiNelacp9bGZTlCHubF3ddZyXBlln+Dh3Os4oi+fQL5 +kZnmn0BEs5M2uExK4NvdBJOadGPFU77mufIckpgp2DKq93nQ/BrHRfCveBQqduF+ +7SHZyrawTVP3yjq6vl1I1/7mP3vXCbB5QQaQCka46OSnb0nWarerTS4k3b/KNUUV +yVveM6z6FcXnesOkDT5sDmpfpxjmmkAA0YFg59tU2zYMIDwEAuJR0i2ZZBFjqQNk +A0H3BZJJAzHoZLBusPArJD5puKBGlsjLDKjBXvSWKD/0MqigzAaFXLkriGY+mHD8 +FfVrN6h0qEwP9oYAaMb3n7KaPn635an9s1E8aBY0gp388K76iwRx102YSYM/QSjM +TGNHh5Au3m0zISpmoMPb13BtoLWHqPNv1WOiafvBFPVPR2lq8q08DZG9h++6NuY4 +StWQxYl0XNxXbOVFdznxXNu+rtdK4h+oDm1MJLcbrQsN11TfQNuqOYxniHBTzPpk +afvf9hDtJhtvNAKrkydE5UnlwSh2Ahvu5dIFIfQgTV3hc3gEP4y5NvxGoo3McRIm +VO13Q36GdERF24wLOeF26BSo04kmHftPnOiV+iukOCDmYWN8SMGBwNedrSVkfdSJ +cfwvo4bH2mFHjzFvgvDo9isYZaHfX4L7IhpYQ0uWAHSgwaGuxpnsAuQ3j6c8QiUL +iBWsiqkINHYCYXtd8aBKFxRdMc7MU8/IuUiBrCuBEuUxFDjPK2Y2y9v0QMFr9kd/ +rFO25Y6R6jZ4VoH12r/EidnDJA+/Ze+t8cpNEXx9CyAcNnJ92zc7OZVoKroZuniC +69YKND1guVGYFkuJgDXaKbthslSIDGU/bymTsO9Kb04rd2M/Am0sH9SI+8YiM8pJ +rtO9fXy2bl1yzz9ABB10xEaRj7vTfz1lxZ4hokjWrLQWMkV5thY50oPfHlByLPLK +vLCYkSWOFGOQpH3FOq2noSflu4rdOr4Q236g8Hf7GaARNeApfmJBx+2aOeKyZFXJ +ETaeMrsKJFKHLFhCV3JdCX7UM8t1ajqxfC24CqKFPeWme4RoPuNEoYLMreaSdhMd +1Nk06xcYBGllRUEz9ATqaTVI4zZ9/OyiZidJ67c0y3nINUs7/7zF4lE= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes192.pem b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes192.pem new file mode 100644 index 000000000..4d65291a7 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes192.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQoix8SJM/RJRNGt+C +vHlZpgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEARYEEPZ44hE6ScmgmsA5 +qLU8pegEggTQiXzvOZKVWu/1Jz6gEgL+QyX1nFjJdWA9tZbphlPis57vdDaWDjOP +yxv2nvCMlWR3SwhcvzVm6tOqkv1Tbrn87kHMzndI7BzgRQx/e9/bEn0K4xOtgXvZ +qKav3jcBbxD7fn0Pt7BdbTI/IcY9biL7UuGcZzh3iWMc1+rf6CErCpn509ijQIL/ +7LlsAOJ0geAMxu8rDPnawq3ZpuWT0hMOwcwhQb/+A9a5QaxS7KOY/1PTHR7DcGlA +3HD9VGxr+59g9xNVr1rHAnmtfUY4JwMU0QEiH2QDdFaRTwDdJwwAHhiihLpK3Row +Vl2E+fCVAc3xYkxsgR1PbZ9jEYTaGakWMQL96UWO5NA2Gnpro716KVWahegPWL/6 +ykPiuA8AlFd3pfMRq4LxnZgJV/xqAX3ooYOMQFR+n2Y5/NujJZkwI7CDovatMm5E +893Fm0bJ+nVYdgSXTfIYfcVlGkqv1Pudu3L1Qhuqaz+pOLsDSAxxrahWU6iMCqWD +GgFJU+7XQX6t+qfLo14V9u3SWKc/L7l40chHQ0dtjdxUJd3o+R1D6j3QM1ydLGZX +IrocgU5M/rLnCnGNUhzd1a0ExWtErnPLW7D2l2EJurZBndY1jEcPhrmi8LZCvJ0a +pGezd7FgSOeXzNt/+BUFZTAAgF+hvcSyFhOQErI4Rg3H5j4s4PPIrUzt6EDp84mc +53s7QYuNHm0ZLx7yIgTcMJfYQyFY7Tm+U7oA3sMBHlHhnk0+D6RxeuXsiwzpoCMC +xiCZ+zV2Yplhd1fkEQ8jtIX6NlXkvDszVoOSPKCXsTkUp+yLP9DKrsc2uSkiS/Ik +1Z8IjdZPFqLHPLUW9Z98tNdD6T1MEK6Qs9sdMXM4ittUeArpMMtpRxxmqWbbpcLX +W52TKuG2yEkS6gdxLy+tBs833oPTyCFvCpAJOHGDzJA0Q4DfJ/3UmRrRdVZx8WTJ +AK7c7awXn8lQIhOnZ8HYi2K95XYJ3FA30+Jr8R0mjhm2XzVqxCG5ULYNpwJ2IEO3 +imDTP2XX26gFSmVa7Q5LrMG/SLUU3jOAHxgdJK1oi+cqpgWgjkorMORwgTFnZOQq +CkazpHSsvrq5Cbk7MyP5nfw9ZZRUPyHdygeSRaW2h6pMp6p/sa9TnJMEsTaLjele +Hze/OAfHkVt8y4sJhVhVf1d8QIsqi+UxyY30/A4gWU3nc0+37VzePz2A4MVOYIwu +8CTNtsDP853o6rzJj/EqoMhQgEztXEXdkaSrgiUHgjcoW3w+cPEmv3apJsJCU5sF +Ai/RjqZ5ni+cxd/a74kvCMSFeEHV5wk+uT8uklLVK+EQ2oJHagd5DvLw5sLy52Bi +ZQ/g7OUF5H9JUihJWQpmS4SV6JRsoY/WNL+eGopjs3PCp5zcI3ijckmkgsOPgsLL +be8aQFfhMMHb3CTD1klxvq1htH0Qn4Hlc+hlUVffQ1khRVY0I3+xrZN/1R0ZebOT +IZTxziU8gRk2N77OHQOKBjlmo0bLAK06g+gGEjZuGyq11KsmhnshAatj9VXUeas3 +3gXQcMEJ1qjX8BWDnRxsDghVbQ2f1RVZpyXQbObOHNISuysJNZ69sDLeglfVWpb/ +E+ozVuqJpapmhyJWLJKdLdHS+gDOm2T9c1xAn4uTErGjwqa8Nof8u9Y= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes256.pem b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes256.pem new file mode 100644 index 000000000..11ec5f0d8 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/openssl-rsa2048-aes256.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQEn45uhBQPIPUnD2U +Dvm7fgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEDLTRNVvkduIUTiG +WmNIqIkEggTQ7D3WyMsJW+DGUIeO82C4p22NwEeqUoEopTKSRhWnNSHZV5ln/5BU +jLuK6KxwLE6Ia70LekLl6CrvN+6V84RkAH7caNO9DZKeEC8HQHPdEeZJq+wx7M3Q +8hMfxA6F03id11toHw5HC3Iu382uZmEoroXQVqXJFLSyQ+hmoOJVXKh1+TgAtW5E +VHtn6xk+HWsi5Ux1duBLDEc4Q49jWSkEJMfKHLqcZmalKYnT2RCGVB0pRvqy62Xx +PJG8sH8/dTfZ+0mgGRRn6EO9GMfbCDzsmdshfwvxGyBUkTUTmpcfSFeOjOAu6XQj +FoytPG4ECrZQdWHxD8glCa2ROyZuzPSuu6QRSA5a8A1lnBAnnEcg+ecIzissEdDc +T5n3vDvQry0pAQb+DXirijnZ1JshD8kOlafdmO9bHwG8O522QwxSc8pN16hfmfKD +XUBJB6JJ/l6o+81CdcJGe5rgrF36/Xn5D+dVTIosVXwT3JyEU2pXlBDlLWaqAsVU +JsMHhn/HA/SC/ClXUgc4n8QpvIQWG1udzU5LnXn78phoXsDAOh80KtT9hiettUT7 +hZ9N2sDnFZHh7oQXlR3Bkd9pK7wvfdl8ixorFYDNI8B6ojV9PuHPc4NSGa616QoA +D7wG7YSqR/6k/VwQwG9IRlS7ntlEUxOz910sOdLaZaAXqrwxPlicg4X0gbIqIUK2 +6tw5JB9vH3WKmxYTtTbqCPXZ6SMy6126gfL/ebTx9R17e7PvVRqfxPCnH1LvrtYi +CIyre439kknh5huJ0VMyOJmIChgTB2vo8PmkQ6qfEK8Sy4hSji0r/TEk2kAApelT +PIf5avZ+2FdAMK2qu/6oiGzWshjrJRDLQrbuRZkNtfVcW6t3BM9f/pIJB8RLWGTK +HcjCidCBVbIWJv8gOPZkGsbNI6TmE6aZWuaG1rg2zoSNH8iN6qvr+NgKWgWa8fEq +LRR1hnVek6WFbaSlMG1Zj2ZSANaCV0nFSwc5K5PLmqgfmLHYOZL3ybbUEi5L/1g7 +iT6YQXx8k0Galb5R64UjlaVbOnYU6Qqgla2pcA2zz2gzzT5vSiIep45rkdmDRyeU +rEQuowx0xTbBWqoh4/cPaWo+Q1sSBk0U9C2IFhJFKEF3mMVZfWMmyp8jTDMLAQ8N +kl8pVZNY/vuHL6gHPlvEJ2xUnfuyhOPM17bsU0UhX/+jxMZ2X16L1flPCr4QGNpK +Avq/t5vl2R5VEBDZ7AdoqL/truopCs6ZYlH4rF57HP+BuJEywgSs0xGfWRtxwpj0 ++N2UCkuFq10jYN1jRWGe6+ey9XJXjyhXsxI6LTyHCduySlzVzWYv6ValdaflhHRm +GO9gqB3ECyFjOYA5kwD1//JTRlkS78JO8K9mEbYTgCaAkBguJGPVXEFUNb8xCPew +Fl08wcOf01rz9r4nh10/T7KHGNy48gcWCyWIwrNc/f45kVg9ywUdapbdFjQyVMVf +SaqXCQEObHiqlW4H44mjwvVCXCh6SdfLdViEXXreWPveantkUDy3I2ILzD0Pyj+3 +Z1YLfhHORM2RCacNc6GM+R9TBrAFSfDMrNtekPs/MtAcrBJyDadZdbvoWQCuxGUR +qXpxPclzXq/L4DEjxKusSE3d8N3Sq+L/OzSNUeG25tIfG1Ut2MjYAg4= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/openssl-rsa2048.pem b/packages/dart/auth_provider/test/pem/openssl-rsa2048.pem new file mode 100644 index 000000000..e335a0ba4 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/openssl-rsa2048.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCfrwnf+Z0OeCMi +ZoeexcCbcsAT6oUddM7AXoBY7s1CrTUqx5VGy6ea6FTYpsu2XsOcKRzRyl69mmKk +cVKUvb08rZcj8wWPp3hY3E0kzg+A0C+qOj6XPX9xGzZynuQCrMjgO/4bZHK50Upx +ajv7oXa2LxGteR7DAT6/9PRFL/0oyc7iKHcG2Fvb97cCC8blbqntnFqdwus7gKLy +XSuBte9llT/MTv72NsVyIwDhK60aHVgkvvw/JfqBIMMxiZlYgyRJgpV+DtuDi5yn +60S6X26vbRWMfs1K1VlfAXiJIOTCY42GZNmqun6LINXZ5st3coh3Nrzm3tpLMEyj +105wvDNPAgMBAAECggEAGCOKt5vxPQW2MfYT3FGCiz0ughQ6qThGJNhkSI1Y5BJw +7hWboEcbABTM9q9ILjpnEY05eRFBfyH+dWNYG3oPSEPpjBqppYyoaa5rzvuDZnKS +MgZ5/bzjLgLyGSOhzjG2cAdBo3xsx2A3A6wOgzxmSKYW4wVjPBFNHhF2d5sIhwi1 +jPgW0fCHJQuUDjlUU8gdSqhMchCCVuQyWL31W9NgMn6OCFdh8cjA8D6AKxsCW0mp +e/myUD0oWxrhJe1nlGan+nsnQZo5TpyFjkzH3yuK/NNmOX8ajS840anWzdHvX283 +WoP2PrmKUGdqtr1CGbd7EdjTiNMWIypzTIm3ba1cYQKBgQDWWGepr5aE0OZ7zfaR +RcCCLawFTVAqopBcx1HkXFk8ofKyOwDPmbeYgrHtr0Ot2kAN/3mOjtNrcChmtkpw ++poSovlGn0SIq9IYJbiWb30EitrRqjkqPAVn9admiGwFZFmwQVWF8+1QXEQCBCvi +1z5mNvzB5KxKDlVKHxD3NqnM8QKBgQC+tz3MEu0ovEmPf+oKudhZwbTfPY9mGCL3 +dcrNwPQYmq86GycbaIU0F9g4Bw7zy2bRU12bh/k9RmBCNPJffT+maODCLRL2i5qB +Qd9rPPi8kF39KWBG7gA9DJFvHT2xJ8I28U2iWq+yEs6WH8oHiA3ENaJ4CHXuG+t1 +RppCDa8EPwKBgEduDp2ttitssmJvsMuYwx6eucTKjvymUBWbFt9TJyndjlN29j44 +q8ZXR5Q94//7y3zetlObpTkYl14jQYuE9/Nd/FRcnyosmEcTyv/XB4KMA3/7ijFY +7zRF2ROCQv1JA9qI60dIkr1FAiTp3vYpZNILYQ/8dK35ONMKp0y7GrsBAoGASGYp +qIH60/7+ceJeR6obbp9xeVnWSSyagZSO46L/RyPZp1ZNd0MrZgYzR7muPHCX3Jko +LPXmcRN5UUjmRce3VQX1ZOFVlJCUm8MU/JHN309yzrtZWDPblVFjGGpiVBFC0jay +gRKqJhCrqiPxPwCwMS8nOSgFFNo2fXPK9Y5aRWMCgYEAhbMmW9S/H78F01u/xwjG +CJhnYj3Ke8033Nu7q4XANH23/jRb6B32QM0CFARM2FkbOR1jozRghgsCJFhL7pcG +pbTBpCSSXOJ3oLHFERWUg9dZOU01hkLc/MsU5XJKYdy+nEIpZymg0kpGyagC0IN9 +YfFw+va3KhGX3fJEmRoAvTA= +-----END PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/openssl-rsa4096.pem b/packages/dart/auth_provider/test/pem/openssl-rsa4096.pem new file mode 100644 index 000000000..5b596177b --- /dev/null +++ b/packages/dart/auth_provider/test/pem/openssl-rsa4096.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +4GRX2gkZaFVx2nHe8Qb8kdqzFljK +-----END PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/ssh-rsa2048.pem b/packages/dart/auth_provider/test/pem/ssh-rsa2048.pem new file mode 100644 index 000000000..451b8b5b6 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/ssh-rsa2048.pem @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAqyxJwBjoG7Z6I6giEryc8FpggzH8oBLdU6wM/s3aEwJIiQ+9cHkV +z5gzshBFlRPN1VjJeGqVWWxyDZIugtFQZv3iFPa2ZNOTLR7Qu/6njaKclvi78zoTxzqoCI +AQWouQ5n/+D9nFa1zI3WUPnl6BTUM2wCmOjJqL0tLNJlbcmScIFuKtlxNyInqxIRwvc9Gt +e5+aeT4BIOohDxht66/Gr45EQclWEFlVJWafXLfJ1aad4D7a8Yu2uoChVARA47JIGsoXqd +pTtL1kWmZmvnqDAamnKwbN3VXqvKeAffqdfEweF6JEUJUZhUa5Bk4Ijp43wHPvIzbvkhfJ +P4eDG5qkSQAAA9j4GEJQ+BhCUAAAAAdzc2gtcnNhAAABAQCrLEnAGOgbtnojqCISvJzwWm +CDMfygEt1TrAz+zdoTAkiJD71weRXPmDOyEEWVE83VWMl4apVZbHINki6C0VBm/eIU9rZk +05MtHtC7/qeNopyW+LvzOhPHOqgIgBBai5Dmf/4P2cVrXMjdZQ+eXoFNQzbAKY6MmovS0s +0mVtyZJwgW4q2XE3IierEhHC9z0a17n5p5PgEg6iEPGG3rr8avjkRByVYQWVUlZp9ct8nV +pp3gPtrxi7a6gKFUBEDjskgayhep2lO0vWRaZma+eoMBqacrBs3dVeq8p4B9+p18TB4Xok +RQlRmFRrkGTgiOnjfAc+8jNu+SF8k/h4MbmqRJAAAAAwEAAQAAAQBacM0KzV4d/l0sZ9KQ ++c0mBWqHBytGXJFKe1ZmbtdxQbyXhpR+T8vhYra1t3k8WjlgJ0hT8mS02eKtHvaqMfP8Zt +pEX1JhlZRmu6hoHBXldOytrACKc74tfuV3kEqTvLgzwZ64O0TfBCgxKguFjsNc0k2kXJCv +45xLKQwx3KUz2ZjK0Ssz9UFT+FVXoLgwLGSWHWle056tjoOhNn4l43t0nvAjRQ5eBME+tb +vkAdi24iIaz8CANynR3hkUGEGB3ryj4YsfmfZ3Oo15KYP6tUrqKuywg8jwHp+NROR49gaA +Ve++udKrKWc5EIZW7k4vKp8ztnfB9heG8cRXn6YqY8dNAAAAgQDaBY+hTwO15kT3lYB+nM +FYpc3drdNbZbK0Mywm81Rk53tnrwtKaC4UYXK3Oya8Zsmcc7v9ReYEZXjNww8jd5d71y6O +YMqCJhc+R0yZvSofjcRIs3jwDCzaeCmvvhqrCpuiBvJLWZ5NronbuZK12Q2X/lo1r48Bgd +fxiwODK6ER3AAAAIEA4b15yGqg7QttyeS4ksmKFO41vXXx/0lVLqCVSZrvexynlm9Ss3pG +HGKigou45jSYZeRad35rC9qMyB2p34YC16oC65N3GvlUiteu5oZ5IEU9Surch9pcn77gW/ +KkmQ1Vlh/L/DJyCgFs2WGetyMGm37oTAGT16URjO458vmOjWsAAACBAMIeR7yz92ahABLv +yBe1edOEC+iWszm/S02esSk8hN/gznBaxKJ9VF4Xp0wcnxHQzvxd2fglmVJ7pNbO4m4+Z0 +WKlfxuPSWTQBBzXm2URNuQpmtSZnyaUDNx9tR6dhmPaHFqEhTVAvMbfgbFf3gemiiD1Y6e +rWGJ2BX/E8fCGa4bAAAAG2Nhcmxvc0BNYWMtQ1dGVjY3VDk3TC5sb2NhbAECAwQFBgc= +-----END OPENSSH PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/ssh-rsa4096-encrypted.pem b/packages/dart/auth_provider/test/pem/ssh-rsa4096-encrypted.pem new file mode 100644 index 000000000..132ad4c4c --- /dev/null +++ b/packages/dart/auth_provider/test/pem/ssh-rsa4096-encrypted.pem @@ -0,0 +1,50 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/packages/dart/auth_provider/test/pem/ssh-rsa4096.pem b/packages/dart/auth_provider/test/pem/ssh-rsa4096.pem new file mode 100644 index 000000000..50c380105 --- /dev/null +++ b/packages/dart/auth_provider/test/pem/ssh-rsa4096.pem @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/packages/dart/common/.gitignore b/packages/dart/common/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/packages/dart/common/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/dart/common/CHANGELOG.md b/packages/dart/common/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/packages/dart/common/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/dart/common/README.md b/packages/dart/common/README.md new file mode 100644 index 000000000..8b55e735b --- /dev/null +++ b/packages/dart/common/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/dart/common/analysis_options.yaml b/packages/dart/common/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/packages/dart/common/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/dart/common/example/common_example.dart b/packages/dart/common/example/common_example.dart new file mode 100644 index 000000000..29288decc --- /dev/null +++ b/packages/dart/common/example/common_example.dart @@ -0,0 +1,21 @@ +import 'package:affinidi_tdk_common/affinidi_tdk_common.dart'; + +void main() { + final Environment local = Environment.environments[EnvironmentType.local]!; + final Environment development = + Environment.environments[EnvironmentType.dev]!; + + final localApiGwUrl = Environment.fetchApiGwUrl(local); + final devApiGwUrl = Environment.fetchApiGwUrl(development); + final prodApiGwUrl = Environment.fetchApiGwUrl(); // Defaults to prod + print("Local API Gateway URL: $localApiGwUrl"); + print("Development API Gateway URL: $devApiGwUrl"); + print("Production API Gateway URL: $prodApiGwUrl"); + + final localWebVaultUrl = VaultUtils.fetchWebVaultUrl(local); + final devWebVaultUrl = VaultUtils.fetchWebVaultUrl(development); + final prodWebVaultUrl = VaultUtils.fetchWebVaultUrl(); // Defaults to prod + print("Local Web Vault URL: $localWebVaultUrl"); + print("Development Web Vault URL: $devWebVaultUrl"); + print("Production Web Vault URL: $prodWebVaultUrl"); +} diff --git a/packages/dart/common/lib/affinidi_tdk_common.dart b/packages/dart/common/lib/affinidi_tdk_common.dart new file mode 100644 index 000000000..d8c2fde33 --- /dev/null +++ b/packages/dart/common/lib/affinidi_tdk_common.dart @@ -0,0 +1,7 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/environment.dart'; +export 'src/vault_utils.dart'; diff --git a/packages/dart/common/lib/src/environment.dart b/packages/dart/common/lib/src/environment.dart new file mode 100644 index 000000000..98d1b2e87 --- /dev/null +++ b/packages/dart/common/lib/src/environment.dart @@ -0,0 +1,100 @@ +enum EnvironmentType { + local('local'), + dev('dev'), + prod('prod'); + + final String value; + const EnvironmentType(this.value); +} + +class Environment { + final String environmentName; + final String apiGwUrl; + final String elementsAuthTokenUrl; + final String iotUrl; + final String webVaultUrl; + final String consumerAudienceEndpoint; + final String consumerCisEndpoint; + + const Environment._({ + required this.environmentName, + required this.apiGwUrl, + required this.elementsAuthTokenUrl, + required this.iotUrl, + required this.webVaultUrl, + required this.consumerAudienceEndpoint, + required this.consumerCisEndpoint, + }); + + static const enviromentVariableName = "AFFINIDI_TDK_ENVIRONMENT"; + static const _consumerAudienceEndpoint = '/iam/v1/consumer/oauth2/token'; + static const _consumerCisEndpoint = '/cis'; + + static final environments = { + EnvironmentType.local: Environment._( + environmentName: EnvironmentType.local.value, + apiGwUrl: 'https://apse1.dev.api.affinidi.io', + elementsAuthTokenUrl: + 'https://apse1.dev.auth.developer.affinidi.io/auth/oauth2/token', + iotUrl: 'a3sq1vuw0cw9an-ats.iot.ap-southeast-1.amazonaws.com', + webVaultUrl: 'http://localhost:3001', + consumerAudienceEndpoint: _consumerAudienceEndpoint, + consumerCisEndpoint: _consumerCisEndpoint, + ), + EnvironmentType.dev: Environment._( + environmentName: EnvironmentType.dev.value, + apiGwUrl: 'https://apse1.dev.api.affinidi.io', + elementsAuthTokenUrl: + 'https://apse1.dev.auth.developer.affinidi.io/auth/oauth2/token', + iotUrl: 'a3sq1vuw0cw9an-ats.iot.ap-southeast-1.amazonaws.com', + webVaultUrl: 'https://vault.dev.affinidi.com', + consumerAudienceEndpoint: _consumerAudienceEndpoint, + consumerCisEndpoint: _consumerCisEndpoint, + ), + EnvironmentType.prod: Environment._( + environmentName: EnvironmentType.prod.value, + apiGwUrl: 'https://apse1.api.affinidi.io', + elementsAuthTokenUrl: + 'https://apse1.auth.developer.affinidi.io/auth/oauth2/token', + iotUrl: 'a13pfgsvt8xhx-ats.iot.ap-southeast-1.amazonaws.com', + webVaultUrl: 'https://vault.affinidi.com', + consumerAudienceEndpoint: _consumerAudienceEndpoint, + consumerCisEndpoint: _consumerCisEndpoint, + ), + }; + + static Environment fetchEnvironment() { + final envValue = String.fromEnvironment(enviromentVariableName); + final environmentType = EnvironmentType.values.firstWhere( + (e) => e.value == envValue, + orElse: () => EnvironmentType.prod, + ); + + return environments[environmentType] ?? environments[EnvironmentType.prod]!; + } + + static String fetchApiGwUrl([Environment? env]) { + env ??= fetchEnvironment(); + return env.apiGwUrl; + } + + static String fetchElementsAuthTokenUrl([Environment? env]) { + env ??= fetchEnvironment(); + return env.elementsAuthTokenUrl; + } + + static String fetchIotUrl([Environment? env]) { + env ??= fetchEnvironment(); + return env.iotUrl; + } + + static String fetchConsumerAudienceUrl([Environment? env]) { + env ??= fetchEnvironment(); + return env.apiGwUrl + env.consumerAudienceEndpoint; + } + + static String fetchConsumerCisUrl([Environment? env]) { + env ??= fetchEnvironment(); + return env.apiGwUrl + env.consumerCisEndpoint; + } +} diff --git a/packages/dart/common/lib/src/vault_utils.dart b/packages/dart/common/lib/src/vault_utils.dart new file mode 100644 index 000000000..5ce7c0ef7 --- /dev/null +++ b/packages/dart/common/lib/src/vault_utils.dart @@ -0,0 +1,33 @@ +import 'dart:core'; + +import 'environment.dart'; + +const String sharePath = '/login'; +const String claimPath = '/claim'; + +class VaultUtils { + static String fetchWebVaultUrl([Environment? env]) { + env ??= Environment.fetchEnvironment(); + return env.webVaultUrl; + } + + static String buildShareLink(String request, String clientId, + [Environment? env]) { + final vaultUrl = fetchWebVaultUrl(env); + Map params = { + 'request': request, + 'client_id': clientId, + }; + String queryString = Uri(queryParameters: params).query; + return '$vaultUrl$sharePath?$queryString'; + } + + static String buildClaimLink(String credentialOfferUri, [Environment? env]) { + final vaultUrl = fetchWebVaultUrl(env); + Map params = { + 'credential_offer_uri': credentialOfferUri, + }; + String queryString = Uri(queryParameters: params).query; + return '$vaultUrl$claimPath?$queryString'; + } +} diff --git a/packages/dart/common/pubspec.yaml b/packages/dart/common/pubspec.yaml new file mode 100644 index 000000000..80adede88 --- /dev/null +++ b/packages/dart/common/pubspec.yaml @@ -0,0 +1,13 @@ +name: affinidi_tdk_common +description: Common package for Affinidi TDK +version: 1.0.0 +repository: https://github.com/affinidi/affinidi-tdk + +environment: + sdk: ^3.6.0 + +resolution: workspace + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/packages/dart/common/test/common_test.dart b/packages/dart/common/test/common_test.dart new file mode 100644 index 000000000..f97ba52d9 --- /dev/null +++ b/packages/dart/common/test/common_test.dart @@ -0,0 +1,125 @@ +import 'package:affinidi_tdk_common/affinidi_tdk_common.dart'; +import 'package:test/test.dart'; + +void main() { + final Environment local = Environment.environments[EnvironmentType.local]!; + final Environment dev = Environment.environments[EnvironmentType.dev]!; + final Environment prod = Environment.environments[EnvironmentType.prod]!; + + group('Environment Tests', () { + test('fetchEnvironment returns prod by default', () { + expect(Environment.fetchEnvironment(), equals(prod)); + }); + + test('API Gateway URLs are correct', () { + expect(Environment.fetchApiGwUrl(local), + equals('https://apse1.dev.api.affinidi.io')); + expect(Environment.fetchApiGwUrl(dev), + equals('https://apse1.dev.api.affinidi.io')); + expect(Environment.fetchApiGwUrl(prod), + equals('https://apse1.api.affinidi.io')); + }); + + test('API Gateway URL defaults to prod', () { + expect( + Environment.fetchApiGwUrl(), equals('https://apse1.api.affinidi.io')); + }); + + test('Elements Auth Token URLs are correct', () { + expect( + Environment.fetchElementsAuthTokenUrl(local), + equals( + 'https://apse1.dev.auth.developer.affinidi.io/auth/oauth2/token')); + expect( + Environment.fetchElementsAuthTokenUrl(dev), + equals( + 'https://apse1.dev.auth.developer.affinidi.io/auth/oauth2/token')); + expect(Environment.fetchElementsAuthTokenUrl(prod), + equals('https://apse1.auth.developer.affinidi.io/auth/oauth2/token')); + }); + + test('Elements Auth Token URL defaults to prod', () { + expect(Environment.fetchElementsAuthTokenUrl(), + equals('https://apse1.auth.developer.affinidi.io/auth/oauth2/token')); + }); + + test('IoT URLs are correct', () { + expect(Environment.fetchIotUrl(local), + equals('a3sq1vuw0cw9an-ats.iot.ap-southeast-1.amazonaws.com')); + expect(Environment.fetchIotUrl(dev), + equals('a3sq1vuw0cw9an-ats.iot.ap-southeast-1.amazonaws.com')); + expect(Environment.fetchIotUrl(prod), + equals('a13pfgsvt8xhx-ats.iot.ap-southeast-1.amazonaws.com')); + }); + + test('IoT URL defaults to prod', () { + expect(Environment.fetchIotUrl(), + equals('a13pfgsvt8xhx-ats.iot.ap-southeast-1.amazonaws.com')); + }); + + test('Consumer Audience URLs are correct', () { + expect( + Environment.fetchConsumerAudienceUrl(local), + equals( + 'https://apse1.dev.api.affinidi.io/iam/v1/consumer/oauth2/token')); + expect( + Environment.fetchConsumerAudienceUrl(dev), + equals( + 'https://apse1.dev.api.affinidi.io/iam/v1/consumer/oauth2/token')); + expect(Environment.fetchConsumerAudienceUrl(prod), + equals('https://apse1.api.affinidi.io/iam/v1/consumer/oauth2/token')); + }); + + test('Consumer Audience URL defaults to prod', () { + expect(Environment.fetchConsumerAudienceUrl(), + equals('https://apse1.api.affinidi.io/iam/v1/consumer/oauth2/token')); + }); + + test('Consumer CIS URLs are correct', () { + expect(Environment.fetchConsumerCisUrl(local), + equals('https://apse1.dev.api.affinidi.io/cis')); + expect(Environment.fetchConsumerCisUrl(dev), + equals('https://apse1.dev.api.affinidi.io/cis')); + expect(Environment.fetchConsumerCisUrl(prod), + equals('https://apse1.api.affinidi.io/cis')); + }); + + test('Consumer CIS URL defaults to prod', () { + expect(Environment.fetchConsumerCisUrl(), + equals('https://apse1.api.affinidi.io/cis')); + }); + }); + + group('Vault Utils Tests', () { + test('Web Vault URLs are correct', () { + expect( + VaultUtils.fetchWebVaultUrl(local), equals('http://localhost:3001')); + expect(VaultUtils.fetchWebVaultUrl(dev), + equals('https://vault.dev.affinidi.com')); + expect(VaultUtils.fetchWebVaultUrl(prod), + equals('https://vault.affinidi.com')); + }); + + test('Web Vault URL defaults to prod', () { + expect( + VaultUtils.fetchWebVaultUrl(), equals('https://vault.affinidi.com')); + }); + + test('buildShareLink constructs correct URL', () { + const request = 'test/request'; + const clientId = 'test-client-id'; + expect( + VaultUtils.buildShareLink(request, clientId), + equals( + 'https://vault.affinidi.com/login?request=test%2Frequest&client_id=test-client-id')); + }); + + test('buildClaimLink constructs correct URL', () { + const credentialOfferUri = 'test/credential/uri'; + expect( + VaultUtils.buildClaimLink(credentialOfferUri), + equals( + 'https://vault.affinidi.com/claim?credential_offer_uri=test%2Fcredential%2Furi')); + }); + }); +} diff --git a/packages/dart/consumer_auth_provider/.gitignore b/packages/dart/consumer_auth_provider/.gitignore new file mode 100644 index 000000000..c94d8d3f0 --- /dev/null +++ b/packages/dart/consumer_auth_provider/.gitignore @@ -0,0 +1,8 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ +.pub/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/dart/consumer_auth_provider/CHANGELOG.md b/packages/dart/consumer_auth_provider/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/packages/dart/consumer_auth_provider/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/dart/consumer_auth_provider/README.md b/packages/dart/consumer_auth_provider/README.md new file mode 100644 index 000000000..770acd8f4 --- /dev/null +++ b/packages/dart/consumer_auth_provider/README.md @@ -0,0 +1,46 @@ +# Affinidi TDK - Consumer Auth Provider + +[[_TOC_]] + +## Installation + +Add the following to your `pubspec.yaml` file: + +```yaml +dependencies: + affinidi_tdk_consumer_auth_provider: ^ +``` + +Then run: + +```bash +dart pub get +``` + +## Getting Started + +Once you've installed the TDK, import it into your Dart code: + +```dart +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; +``` + +### Initialize the provider + +```dart +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; +import 'package:affinidi_tdk_vault_data_manager_client/affinidi_tdk_vault_data_manager_client.dart'; + +void main() { + final consumerAuthProvider = ConsumerAuthProvider( + encryptedSeed: 'encryptedSeed', + encryptionKey: 'encryptionKey', + ); + + // Actual Consumer client that accepts a hook for + // the token which requires a separate import + final apiClient = AffinidiTdkVaultDataManagerClient( + authTokenHook: consumerAuthProvider.fetchConsumerToken, + ); +} +``` diff --git a/packages/dart/consumer_auth_provider/analysis_options.yaml b/packages/dart/consumer_auth_provider/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/packages/dart/consumer_auth_provider/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/dart/consumer_auth_provider/example/affinidi_tdk_consumer_auth_provider_example.dart b/packages/dart/consumer_auth_provider/example/affinidi_tdk_consumer_auth_provider_example.dart new file mode 100644 index 000000000..62f379093 --- /dev/null +++ b/packages/dart/consumer_auth_provider/example/affinidi_tdk_consumer_auth_provider_example.dart @@ -0,0 +1,15 @@ +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; + +Future main() async { + // Paste here your Wallet seed and password + final walletSeed = ''; + final walletPassword = ''; + + final consumerAuthProvider = ConsumerAuthProvider( + encryptedSeed: walletSeed, + encryptionKey: walletPassword, + ); + print('Fetching consumer token...'); + final token = await consumerAuthProvider.fetchConsumerToken(); + print('Consumer token: $token'); +} diff --git a/packages/dart/consumer_auth_provider/lib/affinidi_tdk_consumer_auth_provider.dart b/packages/dart/consumer_auth_provider/lib/affinidi_tdk_consumer_auth_provider.dart new file mode 100644 index 000000000..b6d45a3c8 --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/affinidi_tdk_consumer_auth_provider.dart @@ -0,0 +1,4 @@ +library; + +export 'src/consumer_auth_provider_interface.dart'; +export 'src/consumer_auth_provider.dart'; diff --git a/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider.dart b/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider.dart new file mode 100644 index 000000000..cbf552603 --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider.dart @@ -0,0 +1,34 @@ +import 'provider/base_consumer_auth_provider.dart'; +import 'provider/consumer_auth_provider_abstract.dart'; +import 'consumer_auth_provider_interface.dart'; + +class ConsumerAuthProvider implements ConsumerAuthProviderInterface { + /// A provider for handling Consumer authentication token + /// + /// Example usage: + ///```dart + ///void main() { + /// final consumerAuthProvider = ConsumerAuthProvider( + /// encryptedSeed: 'encryptedSeed', + /// encryptionKey: 'encryptionKey', + /// ); + /// final token = await consumerAuthProvider.fetchConsumerToken(); + ///} + ///``` + ConsumerAuthProvider({ + required String encryptedSeed, + required String encryptionKey, + }) { + ConsumerAuthProviderAbstract.instance = BaseConsumerAuthProvider( + encryptedSeed: encryptedSeed, + encryptionKey: encryptionKey, + ); + } + + /// Retrieves a valid Consumer token. Checks its validity and, if necessary, + /// sends a request to the server to obtain a fresh token. + @override + Future fetchConsumerToken() async { + return await ConsumerAuthProviderAbstract.instance.fetchConsumerToken(); + } +} diff --git a/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider_interface.dart b/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider_interface.dart new file mode 100644 index 000000000..cd845ce8c --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/src/consumer_auth_provider_interface.dart @@ -0,0 +1,3 @@ +abstract interface class ConsumerAuthProviderInterface { + Future fetchConsumerToken(); +} diff --git a/packages/dart/consumer_auth_provider/lib/src/provider/base_consumer_auth_provider.dart b/packages/dart/consumer_auth_provider/lib/src/provider/base_consumer_auth_provider.dart new file mode 100644 index 000000000..87fb9cc87 --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/src/provider/base_consumer_auth_provider.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; + +import 'package:affinidi_tdk_cryptography/affinidi_tdk_cryptography.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; + +import 'consumer_auth_provider_abstract.dart'; +import 'consumer_token_provider.dart'; + +class BaseConsumerAuthProvider implements ConsumerAuthProviderAbstract { + final String _encryptedSeed; + final String _encryptionKey; + + late final CryptographyService _cryptographyService; + late final ConsumerTokenProvider _tokenProvider; + + String? _consumerToken; + + BaseConsumerAuthProvider({ + required String encryptedSeed, + required String encryptionKey, + }) : _encryptedSeed = encryptedSeed, + _encryptionKey = encryptionKey { + _cryptographyService = CryptographyService(); + _tokenProvider = ConsumerTokenProvider(); + } + + @override + Future fetchConsumerToken() async { + try { + if (_consumerToken != null && !_isTokenExpired(_consumerToken!)) { + return _consumerToken!; + } + + final seed = _cryptographyService.decryptSeed( + encryptedSeedHex: _encryptedSeed, + encryptionKeyHex: _encryptionKey, + ); + + _consumerToken = await _tokenProvider.getToken(utf8.encode(seed)); + + return _consumerToken!; + } catch (e) { + throw Exception('Failed to fetch consumer token'); + } + } + + bool _isTokenExpired(String token) { + return JwtDecoder.isExpired(token); + } +} diff --git a/packages/dart/consumer_auth_provider/lib/src/provider/consumer_auth_provider_abstract.dart b/packages/dart/consumer_auth_provider/lib/src/provider/consumer_auth_provider_abstract.dart new file mode 100644 index 000000000..abb4f4285 --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/src/provider/consumer_auth_provider_abstract.dart @@ -0,0 +1,17 @@ +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; + +abstract class ConsumerAuthProviderAbstract + implements ConsumerAuthProviderInterface { + static ConsumerAuthProviderAbstract? _instance; + + static ConsumerAuthProviderAbstract get instance { + if (_instance == null) { + throw StateError('ConsumerAuthProviderAbstract instance not set.'); + } + return _instance!; + } + + static set instance(ConsumerAuthProviderAbstract instance) { + _instance = instance; + } +} diff --git a/packages/dart/consumer_auth_provider/lib/src/provider/consumer_token_provider.dart b/packages/dart/consumer_auth_provider/lib/src/provider/consumer_token_provider.dart new file mode 100644 index 000000000..86a0e2055 --- /dev/null +++ b/packages/dart/consumer_auth_provider/lib/src/provider/consumer_token_provider.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:affinidi_tdk_common/affinidi_tdk_common.dart'; +import 'package:base_codecs/base_codecs.dart'; +import 'package:bip32/bip32.dart'; +import 'package:crypto/crypto.dart'; +import 'package:dio/dio.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web3dart/credentials.dart'; +import 'package:web3dart/crypto.dart'; + +class ConsumerTokenProvider { + static const etheriumIdentityKey = "m/44'/60'/0'/0/0"; + static final String affConsumerAuthTokenEndpoint = + Environment.fetchConsumerAudienceUrl(); + static const secondsBetweenApiAuthRefresh = 300; + + Future getToken(Uint8List seedBytes) async { + final master = BIP32.fromSeed(seedBytes); + + final key = master.derivePath(etheriumIdentityKey); + final myDiD = _getDID(key.privateKey!); + final header = json.encode(_getHeader(_getKid(myDiD))); + final payload = json.encode( + _getPayload(myDiD, affConsumerAuthTokenEndpoint), + ); + final b64header = _base64Unpadded(base64UrlEncode(utf8.encode(header))); + final b64payload = _base64Unpadded(base64UrlEncode(utf8.encode(payload))); + final msgHashHex = + sha256.convert(utf8.encode("$b64header.$b64payload")).bytes; + final assertion = (key.sign(Uint8List.fromList(msgHashHex))); + final jwt = '$b64header.$b64payload.${_base64Unpadded( + base64UrlEncode(Uint8List.fromList(assertion)), + )}'; + + final token = await _getConsumerToken(jwt, myDiD); + return token; + } + + String _getDID(Uint8List privateKey) { + final private = EthPrivateKey.fromHex(bytesToHex(privateKey)); + return 'did:key:z${base58BitcoinEncode( + Uint8List.fromList([231, 1] + private.publicKey.getEncoded().toList()), + )}'; + } + + String _getKid(String did) { + return "$did#${did.substring("did:key:".length)}"; + } + + Map _getHeader(String kid) { + return {'alg': 'ES256K', 'kid': kid}; + } + + Map _getPayload(String did, String tokenEndpoint) { + final issueTimeS = + (DateTime.timestamp().millisecondsSinceEpoch / 1000).floor(); + final payload = { + 'iss': did, + 'sub': did, + 'aud': tokenEndpoint, + 'jti': const Uuid().v4(), + 'exp': issueTimeS + 5 * 60, + 'iat': issueTimeS, + }; + return payload; + } + + String _base64Unpadded(String value) { + if (value.endsWith('==')) return value.substring(0, value.length - 2); + if (value.endsWith('=')) return value.substring(0, value.length - 1); + return value; + } + + Future _getConsumerToken(String clientAssertion, String did) async { + final dioInstance = Dio(); + final data = { + "grant_type": 'client_credentials', + "client_assertion_type": + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + "client_assertion": clientAssertion, + "client_id": did, + }; + + final response = await dioInstance.post( + affConsumerAuthTokenEndpoint, + data: data, + options: Options( + contentType: 'application/json', + headers: { + 'Content-Type': 'application/json', + "Accept": 'application/json', + }, + ), + ); + + return response.data['access_token']; + } +} diff --git a/packages/dart/consumer_auth_provider/pubspec.yaml b/packages/dart/consumer_auth_provider/pubspec.yaml new file mode 100644 index 000000000..1c7ebf9b8 --- /dev/null +++ b/packages/dart/consumer_auth_provider/pubspec.yaml @@ -0,0 +1,25 @@ +name: affinidi_tdk_consumer_auth_provider +description: Provider of Consumer auth token +version: 1.0.0 +repository: https://github.com/affinidi/affinidi-tdk + +environment: + sdk: ^3.6.0 + +resolution: workspace + +dependencies: + affinidi_tdk_common: ^1.0.0 + affinidi_tdk_cryptography: ^1.0.0 + jwt_decoder: ^2.0.1 + bip32: ^2.0.0 + web3dart: ^2.7.3 + base_codecs: ^1.0.1 + uuid: ^4.5.1 + crypto: ^3.0.6 + dio: ^5.7.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 + mocktail: ^1.0.4 diff --git a/packages/dart/consumer_auth_provider/test/affinidi_consumer_auth_provider_test.dart b/packages/dart/consumer_auth_provider/test/affinidi_consumer_auth_provider_test.dart new file mode 100644 index 000000000..f13998895 --- /dev/null +++ b/packages/dart/consumer_auth_provider/test/affinidi_consumer_auth_provider_test.dart @@ -0,0 +1,40 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; + +void main() { + group('Consumer Auth Provider Tests', () { + late MockConsumerAuthProvider authProvider; + + setUp(() { + authProvider = MockConsumerAuthProvider(); + }); + + test('Fetch Consumer Token returns a token', () async { + final testToken = 'testToken'; + + when( + () => authProvider.fetchConsumerToken(), + ).thenAnswer((_) async => testToken); + + final token = await authProvider.fetchConsumerToken(); + + expect(token, isNotNull); + expect(token, isA()); + expect(token, testToken); + }); + + test('Fetch Consumer Token throws error with invalid credentials', + () async { + when( + () => authProvider.fetchConsumerToken(), + ).thenThrow(Exception('Failed to decrypt seed')); + + expect( + () async => await authProvider.fetchConsumerToken(), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/dart/consumer_auth_provider/test/mocks.dart b/packages/dart/consumer_auth_provider/test/mocks.dart new file mode 100644 index 000000000..2377947ae --- /dev/null +++ b/packages/dart/consumer_auth_provider/test/mocks.dart @@ -0,0 +1,4 @@ +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockConsumerAuthProvider extends Mock implements ConsumerAuthProvider {} diff --git a/packages/dart/cryptography/.gitignore b/packages/dart/cryptography/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/packages/dart/cryptography/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/dart/cryptography/CHANGELOG.md b/packages/dart/cryptography/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/packages/dart/cryptography/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/dart/cryptography/README.md b/packages/dart/cryptography/README.md new file mode 100644 index 000000000..8831761b8 --- /dev/null +++ b/packages/dart/cryptography/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/dart/cryptography/analysis_options.yaml b/packages/dart/cryptography/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/packages/dart/cryptography/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/dart/cryptography/lib/affinidi_tdk_cryptography.dart b/packages/dart/cryptography/lib/affinidi_tdk_cryptography.dart new file mode 100644 index 000000000..2db27dd30 --- /dev/null +++ b/packages/dart/cryptography/lib/affinidi_tdk_cryptography.dart @@ -0,0 +1,4 @@ +library; + +export 'src/cryptography_service.dart'; +export 'src/cryptography_interface.dart'; diff --git a/packages/dart/cryptography/lib/src/cryptography/base_cryptography_service.dart b/packages/dart/cryptography/lib/src/cryptography/base_cryptography_service.dart new file mode 100644 index 000000000..d93e6f4a2 --- /dev/null +++ b/packages/dart/cryptography/lib/src/cryptography/base_cryptography_service.dart @@ -0,0 +1,538 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:affinidi_tdk_cryptography/src/models/verify_jwt_result.dart'; +import 'package:crypto/crypto.dart' as crypto; +import 'package:cryptography/cryptography.dart' as cryptography; +import 'package:convert/convert.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:bs58/bs58.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:pointycastle/ecc/ecc_fp.dart' as ecc_fp; +import 'package:pointycastle/pointycastle.dart' as pc; +import "package:pointycastle/export.dart" as pce; +import 'package:secp256k1/secp256k1.dart' as secp256k1; + +import 'cryptography_abstract.dart'; + +class BaseCryptographyService implements CryptographyServiceAbstract { + final _aes256NonceLength = 16; + final _aes256MacLength = 32; + + final _aes256Algorithm = cryptography.AesCbc.with256bits( + macAlgorithm: cryptography.Hmac.sha256(), + ); + + final _pbkdf2Algorithm = cryptography.Pbkdf2( + macAlgorithm: cryptography.Hmac.sha256(), + iterations: 600000, + bits: 256, // 256 bits = 32 bytes output + ); + + final _ivLength = 16; + final _blockSizeBytes = 16; + final _secureRandom = pce.FortunaRandom(); + final _didMethodSeparator = '++'; + + BaseCryptographyService() { + _initializeSecureRandomSeed(); + } + + @override + String getSha256HexFromString(String input) { + print('Started creating SHA256 HEX from string'); + final hex = getSha256HexFromBytes(utf8.encode(input)); + + print('Completed creating SHA256 HEX from string'); + return hex; + } + + @override + String getSha256HexFromBytes(List bytes) { + print('Started creating SHA256 HEX from bytes'); + final hex = crypto.sha256.convert(bytes).toString(); + + print('Completed creating SHA256 HEX from bytes'); + return hex; + } + + @override + List getRandomBytes(int length) { + print('Started generating random bytes'); + + final random = Random.secure(); + final bytes = List.generate(length, (_) => random.nextInt(256)); + + print('Completed generating random bytes'); + return bytes; + } + + @override + Future> Pbkdf2({ + required String password, + required List nonce, + }) async { + print('Started creating PDKDF2'); + + final keyDerivedFromPassword = await _pbkdf2Algorithm.deriveKeyFromPassword( + password: password, + nonce: nonce, + ); + + final bytes = await keyDerivedFromPassword.extractBytes(); + print('Completed creating PDKDF2'); + + return bytes; + } + + @override + Future> Aes256Encrypt({ + required List key, + required List data, + }) async { + print('Started encrypting with AES256'); + + final nonce = getRandomBytes(_aes256NonceLength); + final secretKey = await _aes256Algorithm.newSecretKeyFromBytes(key); + + final secretBox = await _aes256Algorithm.encrypt( + data, + secretKey: secretKey, + nonce: nonce, + ); + + final encryptedData = secretBox.concatenation(); + + print('Completed encrypting with AES256'); + return encryptedData; + } + + @override + Future?> Aes256Decrypt({ + required List key, + required List encryptedData, + }) async { + print('Started decrypting with AES256'); + + final secretBox = cryptography.SecretBox.fromConcatenation( + encryptedData, + nonceLength: _aes256NonceLength, + macLength: _aes256MacLength, + copy: true, + ); + + try { + final decrypted = await _aes256Algorithm.decrypt( + secretBox, + secretKey: cryptography.SecretKey(key), + ); + + print('Completed decrypting with AES256'); + return decrypted; + } on cryptography.SecretBoxAuthenticationError catch (_) { + print('Failed decrypting with AES256'); + return null; + } + } + + @override + Future Aes256EncryptStringToHex({ + required List key, + required String data, + }) async { + final encryptedBytes = await Aes256Encrypt( + key: key, + data: utf8.encode(data), + ); + return hex.encode(encryptedBytes); + } + + @override + Future Aes256DecryptStringFromHex({ + required List key, + required String encryptedData, + }) async { + final decryptedBytes = await Aes256Decrypt( + key: key, + encryptedData: hex.decode(encryptedData), + ); + + if (decryptedBytes == null) { + return null; + } + + return utf8.decode(decryptedBytes); + } + + @override + Map decodeJwtToken({required String token}) { + final decodedToken = JwtDecoder.decode(token); + return decodedToken; + } + + @override + String createHash({required String hashSource}) { + List bytes = utf8.encode(hashSource); + crypto.Digest digest = crypto.sha1.convert(bytes); + + return digest.toString(); + } + + @override + String createSha256Hex({required List bytes}) { + crypto.Digest digest = crypto.sha256.convert(bytes); + return digest.toString(); + } + + @override + String createMd5Base64({required List bytes}) { + crypto.Digest digest = crypto.md5.convert(bytes); + return base64.encode(digest.bytes); + } + + @override + VerifyJwtResult verifyJwt({ + required String jwtToken, + required String didKey, + }) { + try { + final key = _ecPublicKeyFromDid(didKey); + + final jwt = JWT.verify( + jwtToken, + key, + checkHeaderType: false, + checkExpiresIn: true, + ); + + return VerifyJwtResult( + isValid: true, + isExpired: false, + errorMessage: '', + jwtPayload: jwt.payload, + ); + } on JWTExpiredException { + return VerifyJwtResult( + isValid: false, + isExpired: true, + errorMessage: 'Jwt is expired', + jwtPayload: null, + ); + } on JWTException catch (ex) { + return VerifyJwtResult( + isValid: false, + isExpired: false, + errorMessage: ex.message, + jwtPayload: null, + ); + } + } + + @override + List encryptWithRsaPublicKeyFromJwk({ + required Map jwk, + required List data, + }) { + final publicKey = _getRsaPublicKeyFromJwk(jwk); + final encryptor = pce.OAEPEncoding.withSHA256(pce.RSAEngine()); + + encryptor.init( + true, + pce.PublicKeyParameter(publicKey), + ); // true=encrypt + + return _processInBlocks(encryptor, Uint8List.fromList(data)); + } + + @override + String encryptToHex(Uint8List key, Uint8List data) { + final bytes = encryptToBytes(key, data); + + return hex.encode(bytes); + } + + @override + Uint8List? decryptFromHex(Uint8List key, String hexStr) { + final ivAndBytes = hex.decode(hexStr); + + final result = decryptFromBytes(key, Uint8List.fromList(ivAndBytes)); + + return result; + } + + @override + Uint8List encryptToBytes(Uint8List key, Uint8List data) { + final iv = _secureRandom.nextBytes(_ivLength); + final bytes = _aesCbcEncrypt( + key: key, + iv: iv, + paddedPlaintext: _pad(data, _blockSizeBytes), + enforce256KeyLength: true, + enforce128BitAlignment: true, + ); + + return Uint8List.fromList([...iv, ...bytes]); + } + + @override + Uint8List? decryptFromBytes(Uint8List key, Uint8List ivAndBytes) { + try { + final iv = Uint8List.fromList(ivAndBytes.take(_ivLength).toList()); + final bytes = Uint8List.fromList(ivAndBytes.skip(_ivLength).toList()); + + final decryptedAndPadding = _aesCbcDecrypt( + key: key, + iv: iv, + cipherText: bytes, + enforceAssertions: true, + ); + + return _unpad(decryptedAndPadding); + } catch (error) { + return null; + } + } + + @override + String decryptSeed({ + required String encryptedSeedHex, + required String encryptionKeyHex, + }) { + final List walletSeedBuff = hex.decode(encryptedSeedHex); + final nonce = walletSeedBuff.sublist(0, _ivLength); + final ciphertextAndMac = walletSeedBuff.sublist(_ivLength); + final key = hex.decode(encryptionKeyHex); + + final decryptedSeed = _aesCbcDecrypt( + key: Uint8List.fromList(key), + iv: Uint8List.fromList(nonce), + cipherText: Uint8List.fromList(ciphertextAndMac), + ); + + final decryptedSeedEncoded = utf8.decode(decryptedSeed); + final [seed, ...didMethod] = decryptedSeedEncoded.split( + _didMethodSeparator, + ); + + return seed; + } + + void _initializeSecureRandomSeed() { + final seed = Uint8List.fromList( + List.generate(32, (n) => Random.secure().nextInt(255)), + ); + + _secureRandom.seed(pc.KeyParameter(seed)); + } + + (BigInt x, BigInt y) _ecPointFromDid(String did) { + if (!did.startsWith("did:key:")) { + throw "only did:key supported"; + } + + final keyStr = did.split(':')[2]; + + if (!keyStr.startsWith('z')) { + throw "unsupported encoding"; + } + + final compressedWithHeader = base58.decode(keyStr.substring(1)); + + if (compressedWithHeader.isEmpty || compressedWithHeader[0] != 0xe7) { + throw "expected secp256k1 curve"; + } + + final compressed = Uint8List.sublistView(compressedWithHeader, 2); + + if (compressed.length != 33) { + throw "invalid key length"; + } + + final bigCompressed = _uint8ListToBigInt(compressed); + final hex = bigCompressed.toRadixString(16).padLeft(66, '0'); + + final publicKey = secp256k1.PublicKey.fromCompressedHex(hex); + + return (publicKey.X, publicKey.Y); + } + + final _b256 = BigInt.from(256); + + BigInt _uint8ListToBigInt(Uint8List compressed) => + compressed.fold(BigInt.zero, (a, b) => a * _b256 + BigInt.from(b)); + + ECPublicKey _ecPublicKeyFromDid(String did) { + final (x, y) = _ecPointFromDid(did); + + final params = pc.ECDomainParameters('secp256k1'); + final pcKey = pc.ECPublicKey( + ecc_fp.ECPoint( + params.curve as ecc_fp.ECCurve, + params.curve.fromBigInteger(x) as ecc_fp.ECFieldElement?, + params.curve.fromBigInteger(y) as ecc_fp.ECFieldElement?, + false, + ), + params, + ); + + return ECPublicKey.raw(pcKey); + } + + pc.RSAPublicKey _getRsaPublicKeyFromJwk(Map jwk) { + print('Started getting RSA public key from JWK'); + + const alg = 'RSAES_OAEP_SHA_256'; + + if (jwk['alg'] != alg) { + throw UnimplementedError('Only alg=$alg is supported'); + } + + final n = BigInt.parse(hex.encode(_base64UrlDecode(jwk['n']!)), radix: 16); + final e = BigInt.parse(hex.encode(_base64UrlDecode(jwk['e']!)), radix: 16); + + print('Completed getting RSA public key from JWK'); + + // print(bu.CryptoUtils.encodeRSAPublicKeyToPem(pc.RSAPublicKey(n, e))); + return pc.RSAPublicKey(n, e); + } + + Uint8List _processInBlocks( + pce.AsymmetricBlockCipher engine, + Uint8List input, + ) { + final numBlocks = input.length ~/ engine.inputBlockSize + + ((input.length % engine.inputBlockSize != 0) ? 1 : 0); + + final output = Uint8List(numBlocks * engine.outputBlockSize); + + var inputOffset = 0; + var outputOffset = 0; + while (inputOffset < input.length) { + final chunkSize = (inputOffset + engine.inputBlockSize <= input.length) + ? engine.inputBlockSize + : input.length - inputOffset; + + outputOffset += engine.processBlock( + input, + inputOffset, + chunkSize, + output, + outputOffset, + ); + + inputOffset += chunkSize; + } + + return (output.length == outputOffset) + ? output + : output.sublist(0, outputOffset); + } + + Uint8List _base64UrlDecode(String input) { + // Pad the string to a multiple of 4 for correct decoding + String padded = input.padRight((input.length + 3) ~/ 4 * 4, '='); + return base64Url.decode(padded); + } + + Uint8List _aesCbcEncrypt({ + required Uint8List key, + required Uint8List iv, + required Uint8List paddedPlaintext, + bool enforce256KeyLength = false, + bool enforce128BitAlignment = false, + }) { + if (enforce256KeyLength) { + // enforce 256-bit key length + assert(256 == key.length * 8); + } else { + // allow 128, 192, or 256-bit key lengths + assert([128, 192, 256].contains(key.length * 8)); + } + + assert(128 == iv.length * 8); // IV must be 128 bits + + if (enforce128BitAlignment) { + // padded plaintext is a multiple of 128 bits + assert(paddedPlaintext.length * 8 % 128 == 0); + } else { + // padded plaintext is exactly 128 bits + assert(128 == paddedPlaintext.length * 8); + } + + // Create a CBC block cipher with AES, and initialize with key and IV + + final cbc = pce.CBCBlockCipher(pce.AESEngine()) + ..init( + true, + pc.ParametersWithIV(pc.KeyParameter(key), iv), + ); // true=encrypt + + final cipherText = Uint8List(paddedPlaintext.length); // allocate space + + var offset = 0; + + while (offset < paddedPlaintext.length) { + offset += cbc.processBlock(paddedPlaintext, offset, cipherText, offset); + } + + assert(offset == paddedPlaintext.length); + + return cipherText; + } + + Uint8List _aesCbcDecrypt({ + required Uint8List key, + required Uint8List iv, + required Uint8List cipherText, + bool enforceAssertions = false, + }) { + if (enforceAssertions) { + assert(256 == key.length * 8); + assert(128 == iv.length * 8); + assert(cipherText.length * 8 % 128 == 0); + } + + // Create a CBC block cipher with AES, and initialize with key and IV + + final cbc = pce.CBCBlockCipher(pce.AESEngine()) + ..init( + false, + pc.ParametersWithIV(pc.KeyParameter(key), iv), + ); // false=decrypt + + final paddedPlainText = Uint8List(cipherText.length); // allocate space + + var offset = 0; + + while (offset < cipherText.length) { + offset += cbc.processBlock(cipherText, offset, paddedPlainText, offset); + } + + assert(offset == cipherText.length); + + return paddedPlainText; + } + + Uint8List _pad(List bytes, int blockSizeBytes) { + // The PKCS #7 padding just fills the extra bytes with the same value. + // That value is the number of bytes of padding there is. + // + // For example, something that requires 3 bytes of padding with append + // [0x03, 0x03, 0x03] to the bytes. If the bytes is already a multiple of the + // block size, a full block of padding is added. + + final padLength = blockSizeBytes - (bytes.length % blockSizeBytes); + final padded = Uint8List(bytes.length + padLength)..setAll(0, bytes); + + pce.PKCS7Padding().addPadding(padded, bytes.length); + return padded; + } + + Uint8List _unpad(Uint8List padded) { + final unpadded = + padded.sublist(0, padded.length - pce.PKCS7Padding().padCount(padded)); + return unpadded; + } +} diff --git a/packages/dart/cryptography/lib/src/cryptography/cryptography_abstract.dart b/packages/dart/cryptography/lib/src/cryptography/cryptography_abstract.dart new file mode 100644 index 000000000..d00f84ef5 --- /dev/null +++ b/packages/dart/cryptography/lib/src/cryptography/cryptography_abstract.dart @@ -0,0 +1,17 @@ +import 'package:affinidi_tdk_cryptography/affinidi_tdk_cryptography.dart'; + +abstract class CryptographyServiceAbstract + implements CryptographyServiceInterface { + static CryptographyServiceAbstract? _instance; + + static CryptographyServiceAbstract get instance { + if (_instance == null) { + throw StateError('CryptographyServiceAbstract instance not set.'); + } + return _instance!; + } + + static set instance(CryptographyServiceAbstract instance) { + _instance = instance; + } +} diff --git a/packages/dart/cryptography/lib/src/cryptography_interface.dart b/packages/dart/cryptography/lib/src/cryptography_interface.dart new file mode 100644 index 000000000..04644a707 --- /dev/null +++ b/packages/dart/cryptography/lib/src/cryptography_interface.dart @@ -0,0 +1,64 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:typed_data'; + +import 'models/verify_jwt_result.dart'; + +abstract interface class CryptographyServiceInterface { + String getSha256HexFromString(String input); + + String getSha256HexFromBytes(List bytes); + + List getRandomBytes(int length); + + Future> Pbkdf2({ + required String password, + required List nonce, + }); + + Future> Aes256Encrypt({ + required List key, + required List data, + }); + + Future?> Aes256Decrypt({ + required List key, + required List encryptedData, + }); + + Future Aes256EncryptStringToHex({ + required List key, + required String data, + }); + + Future Aes256DecryptStringFromHex({ + required List key, + required String encryptedData, + }); + + List encryptWithRsaPublicKeyFromJwk({ + required Map jwk, + required List data, + }); + + Map decodeJwtToken({required String token}); + VerifyJwtResult verifyJwt({ + required String jwtToken, + required String didKey, + }); + String createHash({required String hashSource}); + String createSha256Hex({required List bytes}); + String createMd5Base64({required List bytes}); + + /* New */ + String encryptToHex(Uint8List key, Uint8List data); + Uint8List encryptToBytes(Uint8List key, Uint8List data); + + Uint8List? decryptFromHex(Uint8List key, String hexStr); + Uint8List? decryptFromBytes(Uint8List key, Uint8List ivAndBytes); + + String decryptSeed({ + required String encryptedSeedHex, + required String encryptionKeyHex, + }); +} diff --git a/packages/dart/cryptography/lib/src/cryptography_service.dart b/packages/dart/cryptography/lib/src/cryptography_service.dart new file mode 100644 index 000000000..30876aefb --- /dev/null +++ b/packages/dart/cryptography/lib/src/cryptography_service.dart @@ -0,0 +1,284 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:typed_data'; + +import 'package:affinidi_tdk_cryptography/src/models/verify_jwt_result.dart'; + +import 'cryptography/base_cryptography_service.dart'; +import 'cryptography/cryptography_abstract.dart'; +import 'cryptography_interface.dart'; + +class CryptographyService implements CryptographyServiceInterface { + /// A service class that provides cryptographic operations. + /// + /// Example usage: + /// ```dart + /// final cryptographyService = CryptographyService(); + /// final encryptionKey = cryptographyService.getRandomBytes(32); + /// final nonce = utf8.encode('nonce'); + /// final passphrase = 'your-passphrase'; + /// + /// final passphraseEncryptionKey = await cryptographyService.Pbkdf2( + /// password: passphrase, + /// nonce: nonce, + /// ); + /// + /// final encryptedKey = await cryptographyService.Aes256Encrypt( + /// key: passphraseEncryptionKey, + /// data: encryptionKey, + /// ); + /// ``` + CryptographyService() { + CryptographyServiceAbstract.instance = BaseCryptographyService(); + } + + /// Decrypts the given encrypted data using AES-256 algorithm. + /// + /// [key] - The encryption key. + /// + /// [encryptedData] - The data to be decrypted. + @override + Future?> Aes256Decrypt({ + required List key, + required List encryptedData, + }) async { + return await CryptographyServiceAbstract.instance.Aes256Decrypt( + key: key, + encryptedData: encryptedData, + ); + } + + /// Decrypts the given encrypted hex string using AES-256 algorithm. + /// + /// [key] - The encryption key. + /// + /// [encryptedData] - The hex string to be decrypted. + @override + Future Aes256DecryptStringFromHex({ + required List key, + required String encryptedData, + }) { + return CryptographyServiceAbstract.instance.Aes256DecryptStringFromHex( + key: key, + encryptedData: encryptedData, + ); + } + + /// Encrypts the given data using AES-256 algorithm. + /// + /// [key] - The encryption key. + /// + /// [data] - The data to be encrypted. + @override + Future> Aes256Encrypt({ + required List key, + required List data, + }) { + return CryptographyServiceAbstract.instance.Aes256Encrypt( + key: key, + data: data, + ); + } + + /// Encrypts the given string to a hex string using AES-256 algorithm. + /// + /// [key] - The encryption key. + /// + /// [data] - The string to be encrypted. + @override + Future Aes256EncryptStringToHex({ + required List key, + required String data, + }) { + return CryptographyServiceAbstract.instance.Aes256EncryptStringToHex( + key: key, + data: data, + ); + } + + /// Derives a key using PBKDF2 algorithm. + /// + /// [password] - The password to derive the key from. + /// + /// [nonce] - The nonce to use in the derivation. + @override + Future> Pbkdf2({ + required String password, + required List nonce, + }) { + return CryptographyServiceAbstract.instance.Pbkdf2( + password: password, + nonce: nonce, + ); + } + + /// Creates a hash from the given source string. + /// + /// [hashSource] - The source string to hash. + @override + String createHash({required String hashSource}) { + return CryptographyServiceAbstract.instance.createHash( + hashSource: hashSource, + ); + } + + /// Creates a base64-encoded MD5 hash from the given bytes. + /// + /// [bytes] - The bytes to hash. + @override + String createMd5Base64({required List bytes}) { + return CryptographyServiceAbstract.instance.createMd5Base64( + bytes: bytes, + ); + } + + /// Creates a hex-encoded SHA-256 hash from the given bytes. + /// + /// [bytes] - The bytes to hash. + @override + String createSha256Hex({required List bytes}) { + return CryptographyServiceAbstract.instance.createSha256Hex( + bytes: bytes, + ); + } + + /// Decodes the given JWT token. + /// + /// [token] - The JWT token to decode. + @override + Map decodeJwtToken({required String token}) { + return CryptographyServiceAbstract.instance.decodeJwtToken( + token: token, + ); + } + + /// Encrypts the given data using an RSA public key from a JWK. + /// + /// [jwk] - The JWK containing the RSA public key. + /// + /// [data] - The data to be encrypted. + @override + List encryptWithRsaPublicKeyFromJwk({ + required Map jwk, + required List data, + }) { + return CryptographyServiceAbstract.instance.encryptWithRsaPublicKeyFromJwk( + jwk: jwk, + data: data, + ); + } + + /// Generates a list of random bytes of the given length. + /// + /// [length] - The length of the random byte list. + @override + List getRandomBytes(int length) { + return CryptographyServiceAbstract.instance.getRandomBytes( + length, + ); + } + + /// Creates a hex-encoded SHA-256 hash from the given bytes. + /// + /// [bytes] - The bytes to hash. + @override + String getSha256HexFromBytes(List bytes) { + return CryptographyServiceAbstract.instance.getSha256HexFromBytes( + bytes, + ); + } + + /// Creates a hex-encoded SHA-256 hash from the given string. + /// + /// [input] - The string to hash. + @override + String getSha256HexFromString(String input) { + return CryptographyServiceAbstract.instance.getSha256HexFromString( + input, + ); + } + + /// Verifies the given JWT token using the provided DID key. + /// + /// [jwtToken] - The JWT token to verify. + /// + /// [didKey] - The DID key to use for verification. + @override + VerifyJwtResult verifyJwt({ + required String jwtToken, + required String didKey, + }) { + return CryptographyServiceAbstract.instance.verifyJwt( + jwtToken: jwtToken, + didKey: didKey, + ); + } + + /// Decrypts the given bytes using the provided key. + /// + /// [key] - The key to use for decryption. + /// + /// [ivAndBytes] - The initialization vector and bytes to decrypt. + @override + Uint8List? decryptFromBytes(Uint8List key, Uint8List ivAndBytes) { + return CryptographyServiceAbstract.instance.decryptFromBytes( + key, + ivAndBytes, + ); + } + + /// Decrypts the given hexadecimal string using the provided key. + /// + /// [key] - The key to use for decryption. + /// + /// [hexStr] - The hexadecimal string to decrypt. + @override + Uint8List? decryptFromHex(Uint8List key, String hexStr) { + return CryptographyServiceAbstract.instance.decryptFromHex( + key, + hexStr, + ); + } + + /// Decrypts the given encrypted seed using the provided encryption key. + /// + /// [encryptedSeedHex] - The encrypted seed in hexadecimal format. + /// + /// [encryptionKeyHex] - The encryption key in hexadecimal format. + @override + String decryptSeed({ + required String encryptedSeedHex, + required String encryptionKeyHex, + }) { + return CryptographyServiceAbstract.instance.decryptSeed( + encryptedSeedHex: encryptedSeedHex, + encryptionKeyHex: encryptionKeyHex, + ); + } + + /// Encrypts the given data to bytes using the provided key. + /// + /// [key] - The key to use for encryption. + /// + /// [data] - The data to encrypt. + @override + Uint8List encryptToBytes(Uint8List key, Uint8List data) { + return CryptographyServiceAbstract.instance.encryptToBytes( + key, + data, + ); + } + + /// Encrypts the given data to a hexadecimal string using the provided key. + /// + /// [key] - The key to use for encryption. + /// + /// [data] - The data to encrypt. + @override + String encryptToHex(Uint8List key, Uint8List data) { + return CryptographyServiceAbstract.instance.encryptToHex( + key, + data, + ); + } +} diff --git a/packages/dart/cryptography/lib/src/models/verify_jwt_result.dart b/packages/dart/cryptography/lib/src/models/verify_jwt_result.dart new file mode 100644 index 000000000..67b54b30d --- /dev/null +++ b/packages/dart/cryptography/lib/src/models/verify_jwt_result.dart @@ -0,0 +1,13 @@ +class VerifyJwtResult { + final bool isValid; + final bool isExpired; + final String? errorMessage; + final dynamic jwtPayload; + + VerifyJwtResult({ + required this.isValid, + required this.isExpired, + required this.errorMessage, + required this.jwtPayload, + }); +} diff --git a/packages/dart/cryptography/pubspec.yaml b/packages/dart/cryptography/pubspec.yaml new file mode 100644 index 000000000..109912a89 --- /dev/null +++ b/packages/dart/cryptography/pubspec.yaml @@ -0,0 +1,23 @@ +name: affinidi_tdk_cryptography +description: Cryptography package for Affinidi TDK +version: 1.0.0 +repository: https://github.com/affinidi/affinidi-tdk + +environment: + sdk: ^3.6.0 + +resolution: workspace + +dependencies: + bs58: ^1.0.2 + convert: ^3.1.1 + crypto: ^3.0.3 + cryptography: ^2.7.0 + dart_jsonwebtoken: ^2.14.0 + jwt_decoder: ^2.0.1 + pointycastle: ^3.9.1 + secp256k1: ^0.3.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 000000000..fddfcc541 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,10 @@ +name: _ +publish_to: none +environment: + sdk: ^3.6.0 +workspace: + - packages/dart/common + - packages/dart/cryptography + - packages/dart/auth_provider + - packages/dart/consumer_auth_provider + - tests/integration/dart diff --git a/scripts/generate_dart_codes.sh b/scripts/generate_dart_codes.sh new file mode 100755 index 000000000..2bbc6fd26 --- /dev/null +++ b/scripts/generate_dart_codes.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +ROOT_DIR="./clients/dart" + +if [[ $# -gt 0 ]]; then + ROOT_DIR="$1" +fi + +if [ ! -d "$ROOT_DIR" ]; then + echo "\033[1;31mError: Directory $ROOT_DIR does not exist.\033[0m" + exit 1 +fi + +for dir in "$ROOT_DIR"/*/; do + if [ -f "$dir/pubspec.yaml" ]; then + echo "\033[1;32m====== Found package in $dir ======\033[0m" + + echo "\033[1;34mRunning dart pub get in $dir\033[0m" + (cd "$dir" && dart pub get) + + echo "\033[1;34mRunning build_runner in $dir\033[0m" + (cd "$dir" && dart run build_runner build --delete-conflicting-outputs) + + if [ $? -ne 0 ]; then + echo "\033[1;31mError during build_runner in $dir\033[0m" + echo "\033[1;33mRunning dart pub upgrade in $dir...\033[0m" + + (cd "$dir" && dart pub upgrade) + + echo "\033[1;34mRetrying build_runner in $dir after upgrading...\033[0m" + (cd "$dir" && dart run build_runner build --delete-conflicting-outputs) + fi + + echo "\033[1;33mFinished processing $dir\033[0m" + else + echo "\033[1;31mNo pubspec.yaml found in $dir\033[0m" + fi +done + +echo "\033[1;32m======== All packages processed. ========\033[0m" diff --git a/tests/.env.example b/tests/.env.example index 064f2f5c9..cb99f45fc 100644 --- a/tests/.env.example +++ b/tests/.env.example @@ -18,3 +18,7 @@ DID="" PRESENTATION_SUBMISSION='{"descriptor_map":[{"id":"email_vc","path":"$.verifiableCredential[0]","format":"ldp_vc"}],"id":"U_bMR52s-hQ_JvbtsXyWt","definition_id":"vp_token_with_email_vc"}' VP_TOKEN='{"@context":["https://www.w3.org/2018/credentials/v1"],"type":["VerifiablePresentation"],"verifiableCredential":[{"@context":["https://www.w3.org/2018/credentials/v1","https://schema.affinidi.com/EmailV1-0.jsonld"],"id":"claimId:4df7855b19b7d826","type":["VerifiableCredential","Email"],"holder":{"id":"did:key:zQ3shNb7dEAa7z4LY8eAbPafNM4iSxppwuvndoHkTUUp8Hbt6"},"credentialSubject":{"email":"roman.b@affinidi.com"},"credentialSchema":{"id":"https://schema.affinidi.com/EmailV1-0.json","type":"JsonSchemaValidator2018"},"issuanceDate":"2024-08-30T03:29:13.418Z","issuer":"did:key:zQ3shXLA2cHanJgCUsDfXxBi2BGnMLArHVz5NWoC9axr8pEy6","proof":{"type":"EcdsaSecp256k1Signature2019","created":"2024-08-30T03:29:16Z","proofPurpose":"assertionMethod","verificationMethod":"did:key:zQ3shXLA2cHanJgCUsDfXxBi2BGnMLArHVz5NWoC9axr8pEy6#zQ3shXLA2cHanJgCUsDfXxBi2BGnMLArHVz5NWoC9axr8pEy6","jws":"eyJhbGciOiJFUzI1NksiLCJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdfQ..5Nrz8cwnDxTbtRkiT9jPHiqTpJhrgRK3SPSLwTbO8PVk19XibqdD_5cvdO-tar0EWSoiDO_wVMfjkxzY3pWgKQ"}}],"holder":{"id":"did:key:zQ3shNb7dEAa7z4LY8eAbPafNM4iSxppwuvndoHkTUUp8Hbt6"},"id":"claimId:T1XCoHAhwVRIvXPgu6Gqm","proof":{"type":"EcdsaSecp256k1Signature2019","created":"2024-09-15T06:17:09Z","verificationMethod":"did:key:zQ3shNb7dEAa7z4LY8eAbPafNM4iSxppwuvndoHkTUUp8Hbt6#zQ3shNb7dEAa7z4LY8eAbPafNM4iSxppwuvndoHkTUUp8Hbt6","proofPurpose":"authentication","challenge":"8172396fd87519e71e87b662ce4ae7e0ec812c6c340471c9e830fc88f8e3e786","domain":"0ac115fa-40b7-4967-b251-07659995eadf","jws":"eyJhbGciOiJFUzI1NksiLCJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdfQ..8uIpJI0uMzcCyJS-iqV7q-8Oto1RTZy7rL2XXeraCPUO6CN9ME_5XjvGfbZ5pJcvvpOuDJHN-JnriW8jY9GIsw"}}' + +# Consumer Vault +VAULT_ENCRYPTED_SEED = "" +VAULT_ENCRYPTION_KEY = "" \ No newline at end of file diff --git a/tests/integration/dart/.gitignore b/tests/integration/dart/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/tests/integration/dart/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/tests/integration/dart/analysis_options.yaml b/tests/integration/dart/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/tests/integration/dart/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/tests/integration/dart/pubspec.yaml b/tests/integration/dart/pubspec.yaml new file mode 100644 index 000000000..b235a1655 --- /dev/null +++ b/tests/integration/dart/pubspec.yaml @@ -0,0 +1,25 @@ +name: dart_tdk_integration_tests + +environment: + sdk: ^3.6.0 + +resolution: workspace + +dev_dependencies: + affinidi_tdk_common: ^1.0.0 + affinidi_tdk_auth_provider: ^1.0.0 + affinidi_tdk_consumer_auth_provider: ^1.0.0 + affinidi_tdk_wallets_client: + path: ../../../clients/dart/wallets_client + affinidi_tdk_credential_issuance_client: + path: ../../../clients/dart/credential_issuance_client + affinidi_tdk_vault_data_manager_client: + path: ../../../clients/dart/vault_data_manager_client + dotenv: ^4.2.0 + lints: ^5.0.0 + test: ^1.24.0 + pointycastle: ^3.9.1 + dart_jsonwebtoken: ^2.12.0 + built_collection: ^5.1.1 + dio: '^5.2.0' + one_of: ^1.5.0 \ No newline at end of file diff --git a/tests/integration/dart/test/auth_provider_test.dart b/tests/integration/dart/test/auth_provider_test.dart new file mode 100644 index 000000000..5ec0a7d06 --- /dev/null +++ b/tests/integration/dart/test/auth_provider_test.dart @@ -0,0 +1,49 @@ +import 'package:test/test.dart'; +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; +import 'environment.dart'; + +void main() { + group('Auth Provider Integration Tests', () { + late ProjectEnvironment env; + setUp(() { + env = getProjectEnvironment(); + }); + + test('obtain project scoped token and cache it', () async { + final authProvider = AuthProvider( + projectId: env.projectId, + tokenId: env.tokenId, + privateKey: env.privateKey, + keyId: env.keyId, + passphrase: env.passphrase, + ); + final projectScopedToken1 = await authProvider.fetchProjectScopedToken(); + expect(projectScopedToken1, isNotEmpty); + + final projectScopedToken2 = await authProvider.fetchProjectScopedToken(); + expect(projectScopedToken2, equals(projectScopedToken1)); + }); + + // test('identify expired project scoped token and request a new one', + // () async { + // final expiredToken = ""; + // final authProvider = AuthProvider( + // projectId: env.projectId, + // tokenId: env.tokenId, + // privateKey: env.privateKey, + // keyId: env.keyId, + // passphrase: env.passphrase, + // ); + // expect(authProvider.projectScopedToken, isNull); + // authProvider.projectScopedToken = expiredToken; + // expect(authProvider.projectScopedToken, isNotEmpty); + // expect(authProvider.publicKey, isNull); + + // final projectScopedToken = await authProvider.fetchProjectScopedToken(); + // expect(projectScopedToken, equals(authProvider.projectScopedToken)); + // expect(projectScopedToken, isNotEmpty); + // expect(projectScopedToken, isNot(equals(expiredToken))); + // expect(authProvider.publicKey, isNotNull); + // }); + }); +} diff --git a/tests/integration/dart/test/consumer_auth_provider_test.dart b/tests/integration/dart/test/consumer_auth_provider_test.dart new file mode 100644 index 000000000..881cca5d0 --- /dev/null +++ b/tests/integration/dart/test/consumer_auth_provider_test.dart @@ -0,0 +1,47 @@ +import 'package:test/test.dart'; +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; +import 'environment.dart'; + +void main() { + group('Consumer Auth Provider Integration Tests', () { + late VaultEnvironment env; + + setUp(() { + env = getVaultEnvironment(); + }); + + test('obtain consumer scoped token and cache it', () async { + final consumerAuthProvider = ConsumerAuthProvider( + encryptedSeed: env.encryptedSeed, + encryptionKey: env.encryptionKey, + ); + final consumerAuthToken1 = + await consumerAuthProvider.fetchConsumerToken(); + expect(consumerAuthToken1, isNotEmpty); + + final consumerAuthToken2 = + await consumerAuthProvider.fetchConsumerToken(); + expect(consumerAuthToken2, equals(consumerAuthToken1)); + }); + + // test('identify expired consumer scoped token and request a new one', + // () async { + // final expiredToken = ""; + + // final consumerAuthProvider = ConsumerAuthProvider( + // encryptedSeed: env.encryptedSeed, + // encryptionKey: env.encryptionKey, + // ); + + // expect(consumerAuthProvider.consumerToken, isNull); + // consumerAuthProvider.consumerToken = expiredToken; + // expect(consumerAuthProvider.consumerToken, isNotEmpty); + + // final consumerAuthToken = await consumerAuthProvider.fetchConsumerToken(); + + // expect(consumerAuthToken, equals(consumerAuthProvider.consumerToken)); + // expect(consumerAuthToken, isNotEmpty); + // expect(consumerAuthToken, isNot(equals(expiredToken))); + // }); + }); +} diff --git a/tests/integration/dart/test/credential_issuance_client_test.dart b/tests/integration/dart/test/credential_issuance_client_test.dart new file mode 100644 index 000000000..35703b5f0 --- /dev/null +++ b/tests/integration/dart/test/credential_issuance_client_test.dart @@ -0,0 +1,136 @@ +import 'package:dio/dio.dart'; +import 'package:test/test.dart'; +import 'package:one_of/one_of.dart'; +// import 'package:built_collection/built_collection.dart'; +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; +import 'package:affinidi_tdk_credential_issuance_client/affinidi_tdk_credential_issuance_client.dart'; +import 'package:affinidi_tdk_wallets_client/affinidi_tdk_wallets_client.dart'; +import 'environment.dart'; + +void main() { + group('Credential Issuance Client Integration Tests', () { + late ConfigurationApi configurationApi; + late WalletApi walletApi; + late String walletId; + + setUp(() async { + final env = getProjectEnvironment(); + final authProvider = AuthProvider( + projectId: env.projectId, + tokenId: env.tokenId, + privateKey: env.privateKey, + keyId: env.keyId, + passphrase: env.passphrase, + ); + + // issuance client + final dio = Dio(BaseOptions( + baseUrl: AffinidiTdkCredentialIssuanceClient.basePath, + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 5), + )); + final issuanceClient = AffinidiTdkCredentialIssuanceClient( + dio: dio, authTokenHook: authProvider.fetchProjectScopedToken); + configurationApi = issuanceClient.getConfigurationApi(); + + // wallet client + final walletClient = AffinidiTdkWalletsClient( + authTokenHook: authProvider.fetchProjectScopedToken); + walletApi = walletClient.getWalletApi(); + + // Create wallet + final didKeyInputBuilder = DidKeyInputParamsBuilder() + ..name = 'Test Wallet'; + final walletInputBuilder = CreateWalletInputBuilder() + ..oneOf = OneOf2( + value: didKeyInputBuilder.build(), typeIndex: 0); + final createdWallet = (await walletApi.createWallet( + createWalletInput: walletInputBuilder.build())) + .data; + walletId = createdWallet!.wallet!.id!; + }); + + tearDown(() async { + final configs = + (await configurationApi.getIssuanceConfigList()).data!.configurations; + if (configs.isNotEmpty) { + for (var config in configs) { + await configurationApi.deleteIssuanceConfigById( + configurationId: config.id); + } + } + if (walletId.isNotEmpty) { + await walletApi.deleteWallet(walletId: walletId); + } + }); + + test('CRUDL IssuanceConfig', () async { + final name = 'TestConfig'; + final description = 'Test issuance config'; + final format = CreateIssuanceConfigInputFormatEnum.ldpVc; + final credentialOfferDuration = 600; + // final credentialSupported = [ + // CredentialSupportedObject((b) => b + // ..credentialTypeId = 'TDriversLicenseV1R1' + // ..jsonSchemaUrl = + // 'https://schema.affinidi.io/TDriversLicenseV1R1.jsonld' + // ..jsonLdContextUrl = + // 'https://schema.affinidi.io/TDriversLicenseV1R1.json') + // ]; + + // create config + final configInputBuilder = CreateIssuanceConfigInputBuilder() + ..issuerWalletId = walletId + ..name = name + ..description = description + ..format = format + ..credentialOfferDuration = credentialOfferDuration + // ..credentialSupported = ListBuilder(credentialSupported) + // + ; + final createdConfig = (await configurationApi.createIssuanceConfig( + createIssuanceConfigInput: configInputBuilder.build())) + .data; + expect(createdConfig!.id, isNotEmpty); + + // get config + final configDetails = (await configurationApi.getIssuanceConfigById( + configurationId: createdConfig.id!)) + .data; + print(configDetails); + expect(configDetails, isNotNull); + expect(configDetails!.id, equals(createdConfig.id)); + expect(configDetails.issuerWalletId, equals(walletId)); + expect(configDetails.name, equals(name)); + expect(configDetails.description, equals(description)); + expect(configDetails.format.toString(), equals(format.toString())); + expect(configDetails.credentialOfferDuration, + equals(credentialOfferDuration)); + expect(configDetails.issuerUri, isNotEmpty); + expect(configDetails.issuerDid, isNotEmpty); + // expect(configDetails.credentialSupported, equals(credentialSupported)); + + // list config + var configs = + (await configurationApi.getIssuanceConfigList()).data!.configurations; + expect(configs, isNotNull); + expect(configs.length, equals(1)); + expect(configs.first.id, equals(createdConfig.id)); + expect(configs.first.issuerWalletId, equals(walletId)); + expect(configs.first.name, equals(name)); + expect(configs.first.format.toString(), equals(format.toString())); + expect(configs.first.credentialOfferDuration, + equals(credentialOfferDuration)); + expect(configs.first.issuerUri, isNotEmpty); + expect(configs.first.issuerDid, isNotEmpty); + + // delete config + await configurationApi.deleteIssuanceConfigById( + configurationId: createdConfig.id!); + configs = + (await configurationApi.getIssuanceConfigList()).data!.configurations; + expect(configs, isNotNull); + expect(configs.length, equals(0)); + }); + }); +} diff --git a/tests/integration/dart/test/elements_public_key_test.dart b/tests/integration/dart/test/elements_public_key_test.dart new file mode 100644 index 000000000..f52e055a1 --- /dev/null +++ b/tests/integration/dart/test/elements_public_key_test.dart @@ -0,0 +1,39 @@ +import 'package:test/test.dart'; +import 'package:affinidi_tdk_auth_provider/src/iam_client.dart'; +import 'package:affinidi_tdk_auth_provider/src/jwt_helper.dart'; +import 'package:affinidi_tdk_common/affinidi_tdk_common.dart'; +import 'package:pointycastle/export.dart' as pce; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; + +void main() { + group('Affinidi Elements Public Key Tests', () { + test('fetches public key from development environment', () async { + final env = Environment.environments[EnvironmentType.dev]!; + final devIamClient = IamClient(apiGatewayUrl: env.apiGwUrl); + final publicKey = await JWTHelper.fetchPublicKey(devIamClient); + + expect(publicKey, isA()); + expect(publicKey.key.Q, isNotNull); + expect(publicKey.key.parameters, isA()); + }); + + test('fetches public key from production environment', () async { + final env = Environment.environments[EnvironmentType.prod]!; + final prodIamClient = IamClient(apiGatewayUrl: env.apiGwUrl); + final publicKey = await JWTHelper.fetchPublicKey(prodIamClient); + + expect(publicKey, isA()); + expect(publicKey.key.Q, isNotNull); + expect(publicKey.key.parameters, isA()); + }); + + test('throws exception on invalid JWKS endpoint', () { + final invalidIamClient = + IamClient(apiGatewayUrl: 'https://invalid-endpoint.example.com'); + expect( + () => JWTHelper.fetchPublicKey(invalidIamClient), + throwsA(isA()), + ); + }); + }); +} diff --git a/tests/integration/dart/test/environment.dart b/tests/integration/dart/test/environment.dart new file mode 100644 index 000000000..f6be94e3d --- /dev/null +++ b/tests/integration/dart/test/environment.dart @@ -0,0 +1,68 @@ +import 'package:dotenv/dotenv.dart'; + +class ProjectEnvironment { + final String projectId; + final String tokenId; + final String privateKey; + final String? keyId; + final String? passphrase; + + ProjectEnvironment({ + required this.projectId, + required this.tokenId, + required this.privateKey, + this.keyId, + this.passphrase, + }); +} + +class VaultEnvironment { + final String encryptedSeed; + final String encryptionKey; + + VaultEnvironment({ + required this.encryptedSeed, + required this.encryptionKey, + }); +} + +ProjectEnvironment getProjectEnvironment() { + final env = DotEnv()..load(['../../.env']); + + if (!env.isEveryDefined(['PROJECT_ID', 'TOKEN_ID', 'PRIVATE_KEY'])) { + throw Exception( + 'Missing environment variables. Please provide PROJECT_ID, TOKEN_ID and PRIVATE_KEY'); + } + + // Workaround for dotenv multiline limitations + final privateKey = env['PRIVATE_KEY']!.replaceAll('\\n', '\n'); + final token = env['TOKEN_ID']!; + final projectId = env['PROJECT_ID']!; + final keyId = env['KEY_ID'] ?? ''; + final passphrase = env['PASSPHRASE'] ?? ''; + + return ProjectEnvironment( + projectId: projectId, + tokenId: token, + privateKey: privateKey, + keyId: keyId, + passphrase: passphrase, + ); +} + +VaultEnvironment getVaultEnvironment() { + final env = DotEnv()..load(['../../.env']); + + if (!env.isEveryDefined(['VAULT_ENCRYPTED_SEED', 'VAULT_ENCRYPTION_KEY'])) { + throw Exception( + 'Missing environment variables. Please provide VAULT_ENCRYPTED_SEED and VAULT_ENCRYPTION_KEY'); + } + + final encryptedSeed = env['VAULT_ENCRYPTED_SEED']!; + final encryptionKey = env['VAULT_ENCRYPTION_KEY']!; + + return VaultEnvironment( + encryptedSeed: encryptedSeed, + encryptionKey: encryptionKey, + ); +} diff --git a/tests/integration/dart/test/vault_data_manager_client_test.dart b/tests/integration/dart/test/vault_data_manager_client_test.dart new file mode 100644 index 000000000..c0f9f2ffc --- /dev/null +++ b/tests/integration/dart/test/vault_data_manager_client_test.dart @@ -0,0 +1,34 @@ +import 'package:affinidi_tdk_consumer_auth_provider/affinidi_tdk_consumer_auth_provider.dart'; +import 'package:test/test.dart'; +import 'package:affinidi_tdk_vault_data_manager_client/affinidi_tdk_vault_data_manager_client.dart'; +import 'environment.dart'; + +void main() { + group('Vault Data Manager Client Integration Tests', () { + // late ConfigApi configApi; + // late FilesApi filesApi; + late NodesApi nodesApi; + // late ProfileDataApi profileDataApi; + + setUp(() async { + final env = getVaultEnvironment(); + final consumerAuthProvider = ConsumerAuthProvider( + encryptedSeed: env.encryptedSeed, + encryptionKey: env.encryptionKey, + ); + final apiClient = AffinidiTdkVaultDataManagerClient( + authTokenHook: consumerAuthProvider.fetchConsumerToken, + ); + + // configApi = apiClient.getConfigApi(); + // filesApi = apiClient.getFilesApi(); + nodesApi = apiClient.getNodesApi(); + // profileDataApi = apiClient.getProfileDataApi(); + }); + + test('list root node children', () async { + final children = (await nodesApi.listRootNodeChildren()).data; + print(children); + }); + }); +} diff --git a/tests/integration/dart/test/wallets_client_test.dart b/tests/integration/dart/test/wallets_client_test.dart new file mode 100644 index 000000000..72a2e3d95 --- /dev/null +++ b/tests/integration/dart/test/wallets_client_test.dart @@ -0,0 +1,58 @@ +import 'package:dio/dio.dart'; +import 'package:one_of/one_of.dart'; +import 'package:test/test.dart'; +import 'package:affinidi_tdk_auth_provider/affinidi_tdk_auth_provider.dart'; +import 'package:affinidi_tdk_wallets_client/affinidi_tdk_wallets_client.dart'; +import 'environment.dart'; + +void main() { + group('Credential Issuance Client Integration Tests', () { + late WalletApi walletApi; + + setUp(() async { + final env = getProjectEnvironment(); + final authProvider = AuthProvider( + projectId: env.projectId, + tokenId: env.tokenId, + privateKey: env.privateKey, + keyId: env.keyId, + passphrase: env.passphrase, + ); + final dio = Dio(BaseOptions( + baseUrl: AffinidiTdkWalletsClient.basePath, + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 5), + )); + final apiClient = AffinidiTdkWalletsClient( + dio: dio, authTokenHook: authProvider.fetchProjectScopedToken); + walletApi = apiClient.getWalletApi(); + }); + + test('Create wallet', () async { + final name = 'Test Wallet'; + final description = 'Test wallet description'; + // Create wallet + final didKeyInputBuilder = DidKeyInputParamsBuilder() + ..name = name + ..description = description; + final walletInputBuilder = CreateWalletInputBuilder() + ..oneOf = OneOf2( + value: didKeyInputBuilder.build(), typeIndex: 0); + final createdWallet = (await walletApi.createWallet( + createWalletInput: walletInputBuilder.build())) + .data; + print(createdWallet); + expect(createdWallet, isNotNull); + expect(createdWallet!.wallet, isNotNull); + expect(createdWallet.wallet!.id, isNotEmpty); + expect(createdWallet.wallet!.did, isNotEmpty); + expect(createdWallet.wallet!.name, equals(name)); + expect(createdWallet.wallet!.description, equals(description)); + expect(createdWallet.wallet!.ari, isNotEmpty); + expect(createdWallet.wallet!.keys, isNotNull); + expect(createdWallet.wallet!.keys!.length, greaterThan(0)); + expect(createdWallet.wallet!.keys!.first.id, isNotEmpty); + expect(createdWallet.wallet!.keys!.first.ari, isNotEmpty); + }); + }); +}