Cross platform end to end encryption (Swift & Node)

107 views Asked by At

I've generated 2 sets of private/public keys and exported to pem, now I'm trying to import them into both Swift & a JS script but they seem to be generating different shared keys so swift can't decrypt a message from JS and vice versa. Given the fact that I'm able to import the keys then export them and the output matches the original input, I'm sure they key pairs are fine so my issue must lie in deriving the shared key.

In swift I'm using CryptoKit and JS Webcrypto.

JS:

async function deriveKey(publicKey, privateKey) {
    return await crypto.subtle.deriveKey(
        { name: "ECDH", public: publicKey },
        privateKey,
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
    );
}
let sharedKey1 = await deriveKey(publicKey2, privateKey);
let exportedKey = await crypto.subtle.exportKey('raw', sharedKey1);
let keyString = Buffer.from(exportedKey).toString('base64');
console.log(keyString);

Which outputs: 1vF4AK9IqDDHNZ86zxt5zavx3h+V7AFCfpBU5Yv8Zro=

Swift:

func deriveSymmetricKey(privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) throws -> SymmetricKey {
    let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)

    let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
        using: SHA256.self,
        salt: Data(),
        sharedInfo: Data(),
        outputByteCount: 32
    )
    
    return symmetricKey
}

let sharedKey1 = try deriveSymmetricKey(privateKey: privateKey, publicKey: publicKey2)

let keyData = sharedKey1.withUnsafeBytes {
    Data(Array($0))
}
let keyString = keyData.base64EncodedString()
print(keyString)

Which outputs: SeQZEg38dcfRl8+5LIwiiJXABJnOMuv3srlrIDZ0wQc=

1

There are 1 answers

1
Topaco On BEST ANSWER

The Swift code derives a key from the shared secret using HKDF. This step is missing in the WebCrypto code and can be implemented there as follows:

(async () => {

const sharedSecret = b642ab("1vF4AK9IqDDHNZ86zxt5zavx3h+V7AFCfpBU5Yv8Zro="); 
const sharedSecretAsCK = await window.crypto.subtle.importKey(
    "raw", 
    sharedSecret, 
    { name: "HKDF" }, 
    false, 
    ["deriveKey"]
); 
const aesKeyViaHkdfAsCK = await window.crypto.subtle.deriveKey(
    { name: "HKDF", salt: new Uint8Array(0), info: new Uint8Array(0), hash: "SHA-256" },
    sharedSecretAsCK,
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
);
console.log(ab2b64(await window.crypto.subtle.exportKey("raw", aesKeyViaHkdfAsCK))); // Base64 encoded AES key: SeQZEg38dcfRl8+5LIwiiJXABJnOMuv3srlrIDZ0wQc=

// helper
function b642ab(base64String){
    return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}
function ab2b64(arrayBuffer) {
    return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

})();

This results in the same key that the Swift code provides.


Note that it is not necessary to export the shared secret (raw or as Base64 encoded data), but that it can be processed end-to-end as a CryptoKey. For this, however, your deriveKey() method must be adapted:

(async () => {

// Create shared secret as CryptoKey 
function deriveSharedSecretAsCK(publicKey, privateKey) {
    return window.crypto.subtle.deriveKey(
        { name: "ECDH", public: publicKey },
        privateKey,
        { name: "HKDF"},
        false,
        ["deriveKey"]
    );
}
// Create AES key via HKDF as CryptoKey
function deriveAesKeyViaHkdfAsCK(sharedSecretKey) {
    return window.crypto.subtle.deriveKey(
        { name: "HKDF", salt: new Uint8Array(0), info: new Uint8Array(0), hash: "SHA-256" },
        sharedSecretKey,
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
  );
}

// Test
// Create some test keys
let alicesKeyPair = await window.crypto.subtle.generateKey({name: "ECDH", namedCurve: "P-384"}, true, ["deriveKey"]);
let bobsKeyPair = await window.crypto.subtle.generateKey({name: "ECDH", namedCurve: "P-384"}, true, ["deriveKey"]);
// Create shared secrets
let alicesSharedSecretAsCK = await deriveSharedSecretAsCK(bobsKeyPair.publicKey, alicesKeyPair.privateKey);
let bobsSharedSecretAsCK = await deriveSharedSecretAsCK(alicesKeyPair.publicKey, bobsKeyPair.privateKey);
// Create AES keys via HKDF
let alicesAesKeyViaHkdfAsCK = await deriveAesKeyViaHkdfAsCK(alicesSharedSecretAsCK)
let bobsAesKeyViasHkdfAsCK = await deriveAesKeyViaHkdfAsCK(bobsSharedSecretAsCK)
console.log("Alice's derived AES key:", ab2b64(await window.crypto.subtle.exportKey("raw", alicesAesKeyViaHkdfAsCK)))
console.log("Bob's derived AES key  :", ab2b64(await window.crypto.subtle.exportKey("raw", bobsAesKeyViasHkdfAsCK)))

// helper
function b642ab(base64String){
    return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}
function ab2b64(arrayBuffer) {
    return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

})();