save method

Uint8List save(
  1. String storePassword, {
  2. String? keyPassword,
})

Saves the keystore to bytes.

storePassword is used for integrity checking. keyPassword is used for private key encryption (defaults to storePassword if not provided).

Implementation

Uint8List save(String storePassword, {String? keyPassword}) {
  final entryList = entries.values.toList();
  // Sort alias to determine order? KeyStore doesn't strictly require sorting, but deterministic output is nice.
  // However, Hashtable order is not guaranteed. Java JKS implementation just iterates keys().

  final body = BytesBuilder();

  // Magic
  body.add(_int32ToBytes(0xFEEDFEED));
  // Version
  body.add(_int32ToBytes(2));
  // Count
  body.add(_int32ToBytes(entryList.length));

  for (final entry in entryList) {
    if (entry is JksPrivateKeyEntry || entry is PrivateKeyEntry) {
      // Tag (1)
      body.add(_int32ToBytes(1));

      // Alias
      _writeJavaUtf(body, entry.alias);

      // Timestamp
      body.add(_int64ToBytes(entry.timestamp));

      // Certificate Chain
      final pke = entry as PrivateKeyEntry;
      if (pke.certChain.isEmpty) {
        // PrivateKeyEntry must have a chain (even if empty? Java says usually accompanied by chain)
        // If empty, write 0.
        body.add(_int32ToBytes(0));
      } else {
        body.add(_int32ToBytes(pke.certChain.length));
        for (final cert in pke.certChain) {
          _writeJavaUtf(body, cert.$1); // One of the chain items is type
          body.add(_int32ToBytes(cert.$2.length));
          body.add(cert.$2);
        }
      }

      // Private Key
      // We need to encrypt the Pkcs8 key
      final kPwd = keyPassword ?? storePassword;

      Uint8List? pkcs8 = pke.pkcs8PrivateKey;
      if (pkcs8 == null && pke.rawPrivateKey != null) {
        // Assuming rawPrivateKey IS pkcs8 for now as we don't have a converter here
        pkcs8 = pke.rawPrivateKey;
      }

      if (pkcs8 == null) {
        throw KeystoreException(
            'Cannot save private key entry ${entry.alias}: no private key loaded');
      }

      final protectedKey = _jksProtectPrivateKey(pkcs8, kPwd);
      body.add(_int32ToBytes(protectedKey.length));
      body.add(protectedKey);
    } else if (entry is TrustedCertEntry) {
      // Tag (2)
      body.add(_int32ToBytes(2));

      // Alias
      _writeJavaUtf(body, entry.alias);

      // Timestamp
      body.add(_int64ToBytes(entry.timestamp));

      // Cert
      _writeJavaUtf(body, entry.certType);
      body.add(_int32ToBytes(entry.certData.length));
      body.add(entry.certData);
    } else {
      // Skip or throw?
      throw KeystoreException('Unsupported entry type: ${entry.runtimeType}');
    }
  }

  final bodyBytes = body.toBytes();

  // Integrity Hash
  final passwordBytes = utf16BeEncode(storePassword);
  final phrase = ascii.encode("Mighty Aphrodite");

  // According to JavaKeyStore.java (engineStore):
  // MessageDigest md = getPreKeyedHash(password);
  // ... writer uses DigestOutputStream(stream, md) ...
  // ... finally writes md.digest()

  // getPreKeyedHash:
  // md = SHA1()
  // md.update(passwordBytes) (utf16be)
  // md.update("Mighty Aphrodite" as UTF8)

  // Then ALL body data is updated to md.

  // So: Hash = SHA1( passwordUTF16BE || "Mighty Aphrodite" || Body )

  final hashInput = BytesBuilder();
  hashInput.add(passwordBytes);
  hashInput.add(phrase);
  hashInput.add(bodyBytes);

  final computedDigest = _pkiCrypto.sha1Sync(hashInput.toBytes());

  final finalKeystore = BytesBuilder();
  finalKeystore.add(bodyBytes);
  finalKeystore.add(computedDigest);

  return finalKeystore.toBytes();
}