使用 WebCrypto API 到 encrypt/decrypt 数据以及从字符串生成的加密密钥
Use WebCrypto API to encrypt/decrypt data with encryption key generated from string
在我的网络应用程序中,我试图在用户注销我的应用程序时将数据存储在本地存储中,并在再次登录后恢复它。此数据是私有的,因此需要在保存前对其进行加密。由于该要求,程序如下所示:
加密:
- 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
- 使用 window.crypto.subtle.importKey()
从该字符串生成 AES-GCM 加密密钥
- 加密数据并将其放入本地存储(以及用于从后端获取密钥的初始化向量和日期时间)。
解密:
- 等到用户再次登录。
- 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
- 使用 window.crypto.subtle.importKey()
从该字符串生成 AES-GCM 加密密钥
- 从本地存储中获取数据并解密。
这是代码 (TypeScript):
interface Data {
queue: string;
initializationVector: string;
date: string;
}
private getEncryptionKey(): void {
const date: string = this.getDateParamForEncryptionKeyGeneration();
const params = new HttpParams().set('date', date);
this.encryptionKeyDate = DateSerializer.deserialize(date);
this.http.get(this.ENCRYPTION_KEY_ENDPOINT, {params}).subscribe((response: {key: string}) => {
const seed = response.key.slice(0, 32);
window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(seed),
'AES-GCM',
true,
['encrypt', 'decrypt']
).then(
(key: CryptoKey) => {
this.encryptionKey = key;
this.decrypt();
}
);
});
}
private getDateParamForEncryptionKeyGeneration(): string {
const dataAsString: string = this.localStorageService.getItem(...);
const data: Data = dataAsString ? JSON.parse(dataAsString) : null;
return data ? data.date : DateSerializer.serialize(moment());
}
private decrypt(data: Data): void {
const encoder = new TextEncoder();
const encryptionAlgorithm: AesGcmParams = {
name: 'AES-GCM',
iv: encoder.encode(data.initializationVector)
};
window.crypto.subtle.decrypt(
encryptionAlgorithm,
this.encryptionKey,
encoder.encode(data.queue)
).then(
(decryptedData: ArrayBuffer) => {
const decoder = new TextDecoder();
console.log(JSON.parse(decoder.decode(decryptedData)));
}
);
}
private encrypt(queue: any[]): void {
const initializationVector: Uint8Array = window.crypto.getRandomValues(new Uint8Array(12));
const encryptionAlgorithm: AesGcmParams = {
name: 'AES-GCM',
iv: initializationVector
};
window.crypto.subtle.encrypt(
encryptionAlgorithm,
this.encryptionKey,
new TextEncoder().encode(JSON.stringify(queue))
).then((encryptedQueue: ArrayBuffer) => {
const decoder = new TextDecoder();
const newState: Data = {
queue: decoder.decode(encryptedQueue),
initializationVector: decoder.decode(initializationVector),
date: DateSerializer.serialize(this.encryptionKeyDate)
};
this.localStorageService.setItem('...', JSON.stringify(newState));
});
}
第一个问题是我解密后收到DOMException
。这几乎不可能调试,因为实际错误由于安全问题被浏览器隐藏:
error: DOMException
code: 0
message: ""
name: "OperationError"
另一件事是我质疑我的方法 - 像那样生成加密密钥是否正确?我怀疑这可能是问题的根源,但我无法找到任何方法来使用 Web Crypto API.
从字符串生成加密密钥
此外,作为加密密钥源的字符串有 128 个字符长,到目前为止我只取前 32 个字符来获取 256 位数据。我不确定这是否正确,因为开头的字符可能不是唯一的。散列可能是一个很好的答案吗?
任何 help/guidance 将不胜感激,尤其是验证我的方法。我正在努力寻找任何此类问题的例子。谢谢!
另外,我不是专业的安全专家。综上所述...
一种方法是在客户端生成密钥,而无需从后端服务器请求唯一的字符串。使用该密钥加密,将密钥保存到您的后端服务器,然后再次获取密钥进行解密。
这在 JavaScript 中,在 TypeScript 中同样有效。
const runDemo = async () => {
const messageOriginalDOMString = 'Do the messages match?';
//
// Encode the original data
//
const encoder = new TextEncoder();
const messageUTF8 = encoder.encode(messageOriginalDOMString);
//
// Configure the encryption algorithm to use
//
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const algorithm = {
iv,
name: 'AES-GCM',
};
//
// Generate/fetch the cryptographic key
//
const key = await window.crypto.subtle.generateKey({
name: 'AES-GCM',
length: 256
},
true, [
'encrypt',
'decrypt'
]
);
//
// Run the encryption algorithm with the key and data.
//
const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
algorithm,
key,
messageUTF8,
);
//
// Export Key
//
const exportedKey = await window.crypto.subtle.exportKey(
'raw',
key,
);
// This is where to save the exported key to the back-end server,
// and then to fetch the exported key from the back-end server.
//
// Import Key
//
const importedKey = await window.crypto.subtle.importKey(
'raw',
exportedKey,
"AES-GCM",
true, [
"encrypt",
"decrypt"
]
);
//
// Run the decryption algorithm with the key and cyphertext.
//
const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
algorithm,
importedKey,
messageEncryptedUTF8,
);
//
// Decode the decryped data.
//
const decoder = new TextDecoder();
const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);
//
// Assert
//
console.log(messageOriginalDOMString);
console.log(messageDecryptedDOMString);
};
runDemo();
另一方面,如果要求加密密钥来自后端的唯一、低熵字符串,则 deriveKey
method might be appropriate with the PBKDF2 algorithm.
const runDemo = async() => {
const messageOriginalDOMString = 'Do the messages match?';
//
// Encode the original data
//
const encoder = new TextEncoder();
const messageUTF8 = encoder.encode(messageOriginalDOMString);
//
// Configure the encryption algorithm to use
//
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const algorithm = {
iv,
name: 'AES-GCM',
};
//
// Generate/fetch the cryptographic key
//
function getKeyMaterial() {
let input = 'the-username' + new Date();
let enc = new TextEncoder();
return window.crypto.subtle.importKey(
"raw",
enc.encode(input), {
name: "PBKDF2"
},
false, ["deriveBits", "deriveKey"]
);
}
let keyMaterial = await getKeyMaterial();
let salt = window.crypto.getRandomValues(new Uint8Array(16));
let key = await window.crypto.subtle.deriveKey({
"name": "PBKDF2",
salt: salt,
"iterations": 100000,
"hash": "SHA-256"
},
keyMaterial, {
"name": "AES-GCM",
"length": 256
},
true, ["encrypt", "decrypt"]
);
//
// Run the encryption algorithm with the key and data.
//
const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
algorithm,
key,
messageUTF8,
);
//
// Export Key
//
const exportedKey = await window.crypto.subtle.exportKey(
'raw',
key,
);
// This is where to save the exported key to the back-end server,
// and then to fetch the exported key from the back-end server.
//
// Import Key
//
const importedKey = await window.crypto.subtle.importKey(
'raw',
exportedKey,
"AES-GCM",
true, [
"encrypt",
"decrypt"
]
);
//
// Run the decryption algorithm with the key and cyphertext.
//
const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
algorithm,
importedKey,
messageEncryptedUTF8,
);
//
// Decode the decryped data.
//
const decoder = new TextDecoder();
const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);
//
// Assert
//
console.log(messageOriginalDOMString);
console.log(messageDecryptedDOMString);
};
runDemo();
在我的网络应用程序中,我试图在用户注销我的应用程序时将数据存储在本地存储中,并在再次登录后恢复它。此数据是私有的,因此需要在保存前对其进行加密。由于该要求,程序如下所示:
加密:
- 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
- 使用 window.crypto.subtle.importKey() 从该字符串生成 AES-GCM 加密密钥
- 加密数据并将其放入本地存储(以及用于从后端获取密钥的初始化向量和日期时间)。
解密:
- 等到用户再次登录。
- 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
- 使用 window.crypto.subtle.importKey() 从该字符串生成 AES-GCM 加密密钥
- 从本地存储中获取数据并解密。
这是代码 (TypeScript):
interface Data {
queue: string;
initializationVector: string;
date: string;
}
private getEncryptionKey(): void {
const date: string = this.getDateParamForEncryptionKeyGeneration();
const params = new HttpParams().set('date', date);
this.encryptionKeyDate = DateSerializer.deserialize(date);
this.http.get(this.ENCRYPTION_KEY_ENDPOINT, {params}).subscribe((response: {key: string}) => {
const seed = response.key.slice(0, 32);
window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(seed),
'AES-GCM',
true,
['encrypt', 'decrypt']
).then(
(key: CryptoKey) => {
this.encryptionKey = key;
this.decrypt();
}
);
});
}
private getDateParamForEncryptionKeyGeneration(): string {
const dataAsString: string = this.localStorageService.getItem(...);
const data: Data = dataAsString ? JSON.parse(dataAsString) : null;
return data ? data.date : DateSerializer.serialize(moment());
}
private decrypt(data: Data): void {
const encoder = new TextEncoder();
const encryptionAlgorithm: AesGcmParams = {
name: 'AES-GCM',
iv: encoder.encode(data.initializationVector)
};
window.crypto.subtle.decrypt(
encryptionAlgorithm,
this.encryptionKey,
encoder.encode(data.queue)
).then(
(decryptedData: ArrayBuffer) => {
const decoder = new TextDecoder();
console.log(JSON.parse(decoder.decode(decryptedData)));
}
);
}
private encrypt(queue: any[]): void {
const initializationVector: Uint8Array = window.crypto.getRandomValues(new Uint8Array(12));
const encryptionAlgorithm: AesGcmParams = {
name: 'AES-GCM',
iv: initializationVector
};
window.crypto.subtle.encrypt(
encryptionAlgorithm,
this.encryptionKey,
new TextEncoder().encode(JSON.stringify(queue))
).then((encryptedQueue: ArrayBuffer) => {
const decoder = new TextDecoder();
const newState: Data = {
queue: decoder.decode(encryptedQueue),
initializationVector: decoder.decode(initializationVector),
date: DateSerializer.serialize(this.encryptionKeyDate)
};
this.localStorageService.setItem('...', JSON.stringify(newState));
});
}
第一个问题是我解密后收到DOMException
。这几乎不可能调试,因为实际错误由于安全问题被浏览器隐藏:
error: DOMException
code: 0
message: ""
name: "OperationError"
另一件事是我质疑我的方法 - 像那样生成加密密钥是否正确?我怀疑这可能是问题的根源,但我无法找到任何方法来使用 Web Crypto API.
从字符串生成加密密钥此外,作为加密密钥源的字符串有 128 个字符长,到目前为止我只取前 32 个字符来获取 256 位数据。我不确定这是否正确,因为开头的字符可能不是唯一的。散列可能是一个很好的答案吗?
任何 help/guidance 将不胜感激,尤其是验证我的方法。我正在努力寻找任何此类问题的例子。谢谢!
另外,我不是专业的安全专家。综上所述...
一种方法是在客户端生成密钥,而无需从后端服务器请求唯一的字符串。使用该密钥加密,将密钥保存到您的后端服务器,然后再次获取密钥进行解密。
这在 JavaScript 中,在 TypeScript 中同样有效。
const runDemo = async () => {
const messageOriginalDOMString = 'Do the messages match?';
//
// Encode the original data
//
const encoder = new TextEncoder();
const messageUTF8 = encoder.encode(messageOriginalDOMString);
//
// Configure the encryption algorithm to use
//
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const algorithm = {
iv,
name: 'AES-GCM',
};
//
// Generate/fetch the cryptographic key
//
const key = await window.crypto.subtle.generateKey({
name: 'AES-GCM',
length: 256
},
true, [
'encrypt',
'decrypt'
]
);
//
// Run the encryption algorithm with the key and data.
//
const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
algorithm,
key,
messageUTF8,
);
//
// Export Key
//
const exportedKey = await window.crypto.subtle.exportKey(
'raw',
key,
);
// This is where to save the exported key to the back-end server,
// and then to fetch the exported key from the back-end server.
//
// Import Key
//
const importedKey = await window.crypto.subtle.importKey(
'raw',
exportedKey,
"AES-GCM",
true, [
"encrypt",
"decrypt"
]
);
//
// Run the decryption algorithm with the key and cyphertext.
//
const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
algorithm,
importedKey,
messageEncryptedUTF8,
);
//
// Decode the decryped data.
//
const decoder = new TextDecoder();
const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);
//
// Assert
//
console.log(messageOriginalDOMString);
console.log(messageDecryptedDOMString);
};
runDemo();
另一方面,如果要求加密密钥来自后端的唯一、低熵字符串,则 deriveKey
method might be appropriate with the PBKDF2 algorithm.
const runDemo = async() => {
const messageOriginalDOMString = 'Do the messages match?';
//
// Encode the original data
//
const encoder = new TextEncoder();
const messageUTF8 = encoder.encode(messageOriginalDOMString);
//
// Configure the encryption algorithm to use
//
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const algorithm = {
iv,
name: 'AES-GCM',
};
//
// Generate/fetch the cryptographic key
//
function getKeyMaterial() {
let input = 'the-username' + new Date();
let enc = new TextEncoder();
return window.crypto.subtle.importKey(
"raw",
enc.encode(input), {
name: "PBKDF2"
},
false, ["deriveBits", "deriveKey"]
);
}
let keyMaterial = await getKeyMaterial();
let salt = window.crypto.getRandomValues(new Uint8Array(16));
let key = await window.crypto.subtle.deriveKey({
"name": "PBKDF2",
salt: salt,
"iterations": 100000,
"hash": "SHA-256"
},
keyMaterial, {
"name": "AES-GCM",
"length": 256
},
true, ["encrypt", "decrypt"]
);
//
// Run the encryption algorithm with the key and data.
//
const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
algorithm,
key,
messageUTF8,
);
//
// Export Key
//
const exportedKey = await window.crypto.subtle.exportKey(
'raw',
key,
);
// This is where to save the exported key to the back-end server,
// and then to fetch the exported key from the back-end server.
//
// Import Key
//
const importedKey = await window.crypto.subtle.importKey(
'raw',
exportedKey,
"AES-GCM",
true, [
"encrypt",
"decrypt"
]
);
//
// Run the decryption algorithm with the key and cyphertext.
//
const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
algorithm,
importedKey,
messageEncryptedUTF8,
);
//
// Decode the decryped data.
//
const decoder = new TextDecoder();
const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);
//
// Assert
//
console.log(messageOriginalDOMString);
console.log(messageDecryptedDOMString);
};
runDemo();