save method
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();
}