1

I'm attempting to use SubtleCrypto to encrypt a string, store the encrypted string, and decrypt that string again, all using a generated RSA-OAEP key pair.

The below code produces a DOMException during the decryption phase, however I can't seem to get any detail on the error. I've tried using "SHA-1" for hashing but have the same issue.

Any hints?

let encoder = new TextEncoder();
let decoder = new TextDecoder('utf-8');

// Generate a key pair

let keyPair = await window.crypto.subtle.generateKey(
    {
        name: "RSA-OAEP",
        modulusLength: 4096,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-256"
    },
    true,
    ["encrypt", "decrypt"]
);

let publicKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
let privateKey = await crypto.subtle.exportKey('jwk', keyPair.privateKey);

// Encrypt a string

let encodedSecret = encoder.encode("MYSECRETVALUE");
let pubcryptokey = await window.crypto.subtle.importKey(
    'jwk',
    publicKey,
    {
        name: "RSA-OAEP",
        hash: "SHA-256"
    },
    false,
    ["encrypt"]
);
let encrypted = await window.crypto.subtle.encrypt(
    {
        name: "RSA-OAEP"
    },
    pubcryptokey,
    encodedSecret
);
let encDV = new DataView(encrypted);
let ct = decoder.decode(encDV);

// Decrypt the string

let encodedCiphertext = encoder.encode(ct);
let privcryptokey = await window.crypto.subtle.importKey(
    'jwk',
    privateKey,
    {
        name: "RSA-OAEP",
        hash: "SHA-256"
    },
    false,
    ["decrypt"]
);
console.log("Before decrypt");
let decrypted = await window.crypto.subtle.decrypt(
    {
        name: "RSA-OAEP"
    },
    privcryptokey,
    encodedCiphertext
);
console.log("After decrypt");
let decDV = new DataView(decrypted);
let pt = decoder.decode(decDV);

console.log(pt);
IanMckay
  • 116
  • 5
  • 1
    For modern crypto algorithms such as those in WebCrypto, both RSA and symmetric, **ciphertext is binary** so trying to decode it as UTF8 and then encode it again will alter most of it into garbage, and for modern crypto even a single bit changed will destroy your data. Don't do that. If you can't store or process it as what it is, `UInt8Array`, and need a string, use one of the data encodings (not character encodings) that preserves binary, like base64 or hex. Also since it contains no elements larger than a byte `DataView` is useless and unnecessary. – dave_thompson_085 Jun 30 '21 at 10:48

1 Answers1

1

The problem is the UTF-8 encoding/decoding of the ciphertext, which corrupts the data. If arbitrary binary data, such as ciphertexts, is to be stored in a string, a binary-to-text encoding such as Base64 must be used, see e.g. here.

If this is fixed, decryption works:

(async () => {

    let encoder = new TextEncoder();
    let decoder = new TextDecoder('utf-8');

    // Generate a key pair

    let keyPair = await window.crypto.subtle.generateKey(
        {
            name: "RSA-OAEP",
            modulusLength: 4096,
            publicExponent: new Uint8Array([1, 0, 1]),
            hash: "SHA-256"
        },
        true,
        ["encrypt", "decrypt"]
    );

    let publicKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
    let privateKey = await crypto.subtle.exportKey('jwk', keyPair.privateKey);

    // Encrypt a string

    let encodedSecret = encoder.encode("MYSECRETVALUE");
    let pubcryptokey = await window.crypto.subtle.importKey(
        'jwk',
        publicKey,
        {
            name: "RSA-OAEP",
            hash: "SHA-256"
        },
        false,
        ["encrypt"]
    );
    
    let encrypted = await window.crypto.subtle.encrypt(
        {
            name: "RSA-OAEP"
        },
        pubcryptokey,
        encodedSecret
    );
    
    //let encDV = new DataView(encrypted);         // Bug: UTF-8 decoding damages the ciphertext
    //let ct = decoder.decode(encDV);
    let ct = ab2b64(encrypted);                    // Fix: Use a binary to text encoding like Base64
    console.log(ct.replace(/(.{48})/g,'$1\n'));

    // Decrypt the string

    //let encodedCiphertext = encoder.encode(ct);  // Bug: s. above
    let encodedCiphertext = b642ab(ct);            // Fix: s. above 
    
    let privcryptokey = await window.crypto.subtle.importKey(
        'jwk',
        privateKey,
        {
            name: "RSA-OAEP",
            hash: "SHA-256"
        },
        false,
        ["decrypt"]
    );
    
    console.log("Before decrypt");
    let decrypted = await window.crypto.subtle.decrypt(
        {
            name: "RSA-OAEP"
        },
        privcryptokey,
        encodedCiphertext
    );
    console.log("After decrypt");

    let decDV = new DataView(decrypted);
    let pt = decoder.decode(decDV);

    console.log(pt);
    
})();

// https://stackoverflow.com/a/11562550/9014097
function ab2b64(arrayBuffer) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

// https://stackoverflow.com/a/41106346 
function b642ab(base64string){
    return Uint8Array.from(atob(base64string), c => c.charCodeAt(0));
}
Topaco
  • 40,594
  • 4
  • 35
  • 62