I want to encrypt with window.crypto.subtle
and decrypt in C#
.
The crypt / decrypt in js are working.
In C#, The computed authentification tag don't match the input.
I don't know if I can put any 12 bytes as salt nor if I need to derive the password.
export async function deriveKey(password, salt) {
const buffer = utf8Encoder.encode(password);
const key = await crypto.subtle.importKey(
'raw',
buffer,
{ name: 'PBKDF2' },
false,
['deriveKey'],
);
const privateKey = crypto.subtle.deriveKey(
{
name: 'PBKDF2',
hash: { name: 'SHA-256' },
iterations,
salt,
},
key,
{
name: 'AES-GCM',
length: 256,
},
false,
['encrypt', 'decrypt'],
);
return privateKey;
}
const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff));
const base64_to_buf = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));
export async function encrypt(key, data) {
const salt = crypto.getRandomValues(new Uint8Array(12));
const iv = crypto.getRandomValues(new Uint8Array(12));
console.log('encrypt');
console.log('iv', iv);
console.log('salt', salt);
const buffer = new TextEncoder().encode(data);
const privatekey = await deriveKey(key, salt);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
tagLength: 128,
},
privatekey,
buffer,
);
const bytes = new Uint8Array(encrypted);
console.log('concat');
const buff = new Uint8Array(iv.byteLength encrypted.byteLength salt.byteLength);
buff.set(iv, 0);
buff.set(salt, iv.byteLength);
buff.set(bytes, iv.byteLength salt.byteLength);
console.log('iv', iv);
console.log('salt', salt);
console.log('buff', buff);
const base64Buff = buff_to_base64(buff);
console.log(base64Buff);
return base64Buff;
}
export async function decrypt(key, data) {
console.log('decryption');
console.log('buff', base64_to_buf(data));
const d = base64_to_buf(data);
const iv = d.slice(0, 12);
const salt = d.slice(12, 24);
const ec = d.slice(24);
console.log('iv', iv);
console.log('salt', salt);
console.log(ec);
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv,
tagLength: 128,
},
await deriveKey(key, salt),
ec,
);
return new TextDecoder().decode(new Uint8Array(decrypted));
}
Span<byte> encryptedData = Convert.FromBase64String(enc).AsSpan();
Span<byte> nonce = encryptedData[..12];
Span<byte> salt = encryptedData.Slice(12, 12);
Span<byte> data = encryptedData.Slice(12 12, encryptedData.Length - 16 - 12 - 12);
Span<byte> tag = encryptedData[^16..];
Span<byte> result = new byte[data.Length];
using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 1000, HashAlgorithmName.SHA256);
using AesGcm aes = new(pbkdf2.GetBytes(16));
aes.Decrypt(nonce, data, tag, result);
CodePudding user response:
There are a few inconsistencies and/or minor flaws in both codes. Concerning the JavaScript code:
- The salt should be concatenated like the IV along with the ciphertext/tag (ciphertext/tag = implicit concatenation of the actual ciphertext and tag), e.g. salt|IV|ciphertext|tag. The IV should be randomly generated like the salt.
- In both codes the same iteration count must be used for key derivation with PBKDF2, e.g. 25000 (in practice the value should be set as high as possible while maintaining acceptable performance).
- In both codes, the PBKDF2 key derivation must generate AES keys of the same length, so that the same AES variant is used, e.g. a 32 bytes key for AES-256.
With these changes, the Java code
(async () => {
const utf8Encoder = new TextEncoder('utf-8');
const salt = crypto.getRandomValues(new Uint8Array(16)); // Fix 1: consider salt
const iv = crypto.getRandomValues(new Uint8Array(12));
const iterations = 25000; // Fix 2: apply the same iteration count
async function deriveKey(password) {
const buffer = utf8Encoder.encode(password);
const key = await crypto.subtle.importKey(
'raw',
buffer,
{ name: 'PBKDF2' },
false,
['deriveKey'],
);
const privateKey = crypto.subtle.deriveKey(
{
name: 'PBKDF2',
hash: { name: 'SHA-256' },
iterations,
salt,
},
key,
{
name: 'AES-GCM',
length: 256, // Fix 3: use the same key size
},
false,
['encrypt', 'decrypt'],
);
return privateKey;
}
const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff));
const base64_to_buf = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));
async function encrypt(key, data, iv, salt) {
const buffer = new TextEncoder().encode(data);
const privatekey = await deriveKey(key);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
tagLength: 128,
},
privatekey,
buffer,
);
const bytes = new Uint8Array(encrypted);
let buff = new Uint8Array(salt.byteLength iv.byteLength encrypted.byteLength);
buff.set(salt, 0); // Fix 1: consider salt
buff.set(iv, salt.byteLength);
buff.set(bytes, salt.byteLength iv.byteLength);
const base64Buff = buff_to_base64(buff);
return base64Buff;
}
async function decrypt(key, data) {
const d = base64_to_buf(data);
const salt = d.slice(0, 16); // Fix 1: consider salt
const iv = d.slice(16, 16 12)
const ec = d.slice(16 12);
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv,
tagLength: 128,
},
await deriveKey(key),
ec
);
return new TextDecoder().decode(new Uint8Array(decrypted));
}
var data = 'The quick brown fox jumps over the lazy dog';
var passphrase = 'my passphrase';
var ct = await encrypt(passphrase, data, iv, salt);
var dt = await decrypt(passphrase, ct);
console.log(ct);
console.log(dt);
})();
returns, e.g.:
P/y3nrZU70XtanEUvubyVUp LzOVHLGAl55cd N6T0c9ak15KVXh5UxFEjMYGsvGWzf286wAGc5HgEjmwxWCkdjSt5vt42Anb4jwKlVMdLyYoP9Gg/be
In the C# code, salt, IV, and ciphertext/tag must be correctly separated, and keysize and iteration count of the JavaScript code must be used:
string ciphertext = "P/y3nrZU70XtanEUvubyVUp LzOVHLGAl55cd N6T0c9ak15KVXh5UxFEjMYGsvGWzf286wAGc5HgEjmwxWCkdjSt5vt42Anb4jwKlVMdLyYoP9Gg/be";
Span<byte> encryptedData = Convert.FromBase64String(ciphertext).AsSpan();
Span<byte> salt = encryptedData[..16]; // Fix 1: consider salt (and apply the correct parameters)
Span<byte> nonce = encryptedData[16..(16 12)];
Span<byte> data = encryptedData[(16 12)..^16];
Span<byte> tag = encryptedData[^16..];
string password = "my passphrase";
using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 25000, HashAlgorithmName.SHA256); // Fix 2: apply the same iteration count
using AesGcm aes = new(pbkdf2.GetBytes(32)); // Fix 3: use the same key size (e.g. 32 bytes for AES-256)
Span<byte> result = new byte[data.Length];
aes.Decrypt(nonce, data, tag, result);
Console.WriteLine(Encoding.UTF8.GetString(result.ToArray())); // The quick brown fox jumps over the lazy dog
Then the ciphertext of the JavaScript code can be successfully decrypted with the C# code.