attested_secure_keys 0.1.0 copy "attested_secure_keys: ^0.1.0" to clipboard
attested_secure_keys: ^0.1.0 copied to clipboard

Hardware-backed, attestable EC P-256 keys for Flutter — Android Keystore/StrongBox and iOS Secure Enclave, non-exportable, ES256/JOSE, with key attestation.

example/lib/main.dart

import 'dart:convert';

import 'package:attested_secure_keys/attested_secure_keys.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'reenroll_on_invalidation.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'attested_secure_keys',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const DemoPage(),
    );
  }
}

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

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  static const _alias = 'demo.holderKey';

  /// A real flow uses a fresh server-issued random nonce. This fixed demo value
  /// is bound BOTH as the attestation challenge at generateKey and echoed at
  /// attest, so the exported bundle's freshness check (challenge == nonce)
  /// passes end-to-end.
  static final Uint8List _demoNonce = Uint8List.fromList(
    List<int>.generate(32, (i) => (i * 7 + 3) & 0xff),
  );
  final AttestedSecureKeys _keys = const AttestedSecureKeys();

  final List<String> _log = <String>[];
  DeviceKeyCapabilities? _caps;
  HwKey? _key;
  KeyAttestation? _att;
  bool _busy = false;

  void _append(String line) => setState(() => _log.insert(0, line));

  Future<void> _run(String label, Future<void> Function() action) async {
    setState(() => _busy = true);
    try {
      await action();
    } on AttestedSecureKeysException catch (e) {
      _append('✗ $label: ${e.runtimeType} — ${e.message}');
    } catch (e) {
      _append('✗ $label: $e');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  Future<void> _capabilities() => _run('capabilities', () async {
    final caps = await _keys.capabilities();
    setState(() => _caps = caps);
    _append(
      '✓ capabilities: best=${caps.bestAvailableLevel.name}, '
      'attestation=${caps.supportsKeyAttestation}, '
      'biometric=${caps.supportsBiometricGating}',
    );
  });

  Future<void> _generate({bool gated = false}) => _run('generateKey', () async {
    final key = await _keys.generateKey(
      alias: _alias,
      // Demo: accept any level and honestly report what was achieved.
      minSecurityLevel: KeySecurityLevel.software,
      userAuth: gated
          ? const UserAuthPolicy.perUseBiometric()
          : UserAuthPolicy.none,
      // Bind the (demo) server nonce so the attestation is replay-checkable.
      attestationChallenge: _demoNonce,
    );
    setState(() => _key = key);
    _append(
      '✓ generateKey${gated ? ' (biometric)' : ''}: '
      'level=${key.effectiveLevel.name}, '
      'attestation=${key.attestationType.name}, '
      'keyId=${_short(key.keyId)}',
    );
  });

  Future<void> _sign() => _run('sign', () async {
    final payload = Uint8List.fromList(
      utf8.encode('attested_secure_keys demo payload'),
    );
    final sig = await _keys.sign(
      alias: _alias,
      payload: payload,
      promptTitle: 'Confirm signature',
    );
    _append('✓ sign: ${sig.bytes.length}-byte R‖S, jose=${_short(sig.jose)}');
  });

  Future<void> _attest() => _run('attest', () async {
    // Same nonce that was bound at generateKey, so challenge == nonce.
    final att = await _keys.attest(alias: _alias, serverNonce: _demoNonce);
    // Re-read the now-persisted key info so the card reflects the attestation
    // (proves the native side recorded it; survives restarts).
    final info = await _keys.getKeyInfo(alias: _alias);
    setState(() {
      _att = att;
      if (info != null) {
        _key = _key?.copyWith(
          attestationType: info.attestationType,
          gatedByUserAuth: info.gatedByUserAuth,
          userAuthType: info.userAuthType,
        );
      }
    });
    _append(
      '✓ attest: type=${att.type.name}, x5c=${att.x5c.length} cert(s), '
      'raw=${att.raw?.length ?? 0} bytes',
    );
  });

  /// Copies the server-verifiable bundle (public JWK + keyId + attestation) to
  /// the clipboard, ready to paste into attested_secure_keys_verifier.
  Future<void> _copyAttestation() async {
    final key = _key;
    final att = _att;
    if (key == null || att == null) return;
    final json = const JsonEncoder.withIndent('  ').convert({
      'keyId': key.keyId,
      'publicJwk': key.publicJwk.toJson(),
      'attestation': att.toJson(),
    });
    await Clipboard.setData(ClipboardData(text: json));
    _append('✓ copied attestation JSON (${json.length} chars) to clipboard');
  }

  /// Runs the `KeyInvalidatedError` recovery template (see
  /// reenroll_on_invalidation.dart) end-to-end against a fake backend:
  /// step-up → regenerate → re-attest → "rotate" → sign. On a real device this
  /// is exactly what you'd run inside `catch (KeyInvalidatedError)`.
  Future<void> _reenrollDemo() => _run('re-enroll', () async {
    final backend = _FakeWalletBackend(_append);
    final sig = await reEnrollAndSign(
      keys: _keys,
      alias: _alias,
      payload: Uint8List.fromList(utf8.encode('post-reenroll payload')),
      backend: backend,
      // Demo floor so it also runs on emulators; use trustedEnvironment in prod.
      minSecurityLevel: KeySecurityLevel.software,
      stepUp: () async {
        _append('• step-up: re-proving identity (demo: auto-approved)');
        return true;
      },
    );
    _append(
      '✓ re-enroll: signed with the fresh key (${sig.bytes.length}-byte R‖S)',
    );
  });

  Future<void> _delete() => _run('deleteKey', () async {
    await _keys.deleteKey(alias: _alias);
    setState(() => _key = null);
    _append('✓ deleteKey: "$_alias" removed');
  });

  String _short(String s) => s.length <= 18 ? s : '${s.substring(0, 18)}…';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('attested_secure_keys')),
      body: Column(
        children: [
          if (_busy) const LinearProgressIndicator(minHeight: 2),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton.tonal(
                  onPressed: _busy ? null : _capabilities,
                  child: const Text('Capabilities'),
                ),
                FilledButton(
                  onPressed: _busy ? null : () => _generate(),
                  child: const Text('Generate'),
                ),
                FilledButton(
                  onPressed: _busy ? null : () => _generate(gated: true),
                  child: const Text('Generate (biometric)'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _sign,
                  child: const Text('Sign'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _attest,
                  child: const Text('Attest'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _delete,
                  child: const Text('Delete'),
                ),
                OutlinedButton(
                  onPressed: (_busy || _att == null) ? null : _copyAttestation,
                  child: const Text('Copy JSON'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _reenrollDemo,
                  child: const Text('Re-enroll'),
                ),
              ],
            ),
          ),
          if (_caps != null) _CapabilitiesCard(caps: _caps!),
          if (_key != null) _KeyCard(hwKey: _key!),
          const Divider(height: 1),
          Expanded(
            child: _log.isEmpty
                ? const Center(child: Text('Tap a button to begin.'))
                : ListView.separated(
                    padding: const EdgeInsets.all(12),
                    itemCount: _log.length,
                    separatorBuilder: (_, _) => const SizedBox(height: 6),
                    itemBuilder: (_, i) => Text(
                      _log[i],
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 12,
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

/// A no-op stand-in for your real backend, so the demo can run the rotation
/// end-to-end. A real implementation verifies the attestation against the
/// manufacturer roots and stores the rotated public key for the user.
class _FakeWalletBackend implements WalletKeyBackend {
  _FakeWalletBackend(this._log);
  final void Function(String) _log;

  @override
  Future<Uint8List> freshAttestationNonce() async {
    _log('• backend: issued a fresh attestation nonce');
    return _DemoPageState._demoNonce;
  }

  @override
  Future<void> reEnrollKey({
    required Jwk publicJwk,
    required String keyId,
    required KeyAttestation attestation,
  }) async {
    _log('• backend: verified attestation + rotated public key (keyId=$keyId)');
  }
}

class _CapabilitiesCard extends StatelessWidget {
  const _CapabilitiesCard({required this.caps});
  final DeviceKeyCapabilities caps;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Device capabilities',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 4),
            Text('Best available level: ${caps.bestAvailableLevel.name}'),
            Text(
              'StrongBox: ${caps.hasStrongBox} · TEE: ${caps.hasTee} · '
              'Secure Enclave: ${caps.hasSecureEnclave}',
            ),
            Text(
              'Attestation: ${caps.supportsKeyAttestation} · '
              'Biometric gating: ${caps.supportsBiometricGating}',
            ),
            if (caps.androidApiLevel != null)
              Text('Android API level: ${caps.androidApiLevel}'),
            if (caps.iosVersion != null)
              Text('iOS version: ${caps.iosVersion}'),
          ],
        ),
      ),
    );
  }
}

class _KeyCard extends StatelessWidget {
  const _KeyCard({required this.hwKey});
  final HwKey hwKey;

  @override
  Widget build(BuildContext context) {
    final ok = hwKey.isHardwareBacked && hwKey.hasHardwareAttestation;
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  ok ? Icons.verified_user : Icons.warning_amber,
                  color: ok ? Colors.green : Colors.orange,
                  size: 20,
                ),
                const SizedBox(width: 6),
                const Text(
                  'Current key',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
              ],
            ),
            const SizedBox(height: 4),
            Text('Alias: ${hwKey.alias}'),
            Text(
              'Effective level: ${hwKey.effectiveLevel.name} '
              '(requested ${hwKey.requestedLevel.name})',
            ),
            Text('Attestation: ${hwKey.attestationType.name}'),
            Text(
              'Hardware-backed: ${hwKey.isHardwareBacked} · '
              'Attested: ${hwKey.hasHardwareAttestation}',
            ),
            Text('Gated by user auth: ${hwKey.gatedByUserAuth}'),
            Text(
              'keyId: ${hwKey.keyId}',
              style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
150
points
257
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Hardware-backed, attestable EC P-256 keys for Flutter — Android Keystore/StrongBox and iOS Secure Enclave, non-exportable, ES256/JOSE, with key attestation.

Repository (GitHub)
View/report issues

Topics

#security #cryptography #keystore #attestation #secure-enclave

License

Apache-2.0 (license)

Dependencies

attested_secure_keys_android, attested_secure_keys_ios, attested_secure_keys_platform_interface, flutter

More

Packages that depend on attested_secure_keys

Packages that implement attested_secure_keys