Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate RSA encryption for mails #8418

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6934dcf
Prevent adding new admin when rotating admin group
hdcos Nov 26, 2024
59ac854
Generate distribution key pairs, AdminGroupKeyRotationService PUT
sarashub Nov 27, 2024
fbc14af
Handle multiple admin group key rotation
hdcos Dec 9, 2024
70a1609
Fix derivation of targetUserGroupKeyAuthKey
sarashub Dec 12, 2024
37d14ef
Handle userGroupKeyRotation as admin
bedhub Dec 10, 2024
5153a24
Model changes
vitoreiji Jan 3, 2025
6656c83
Authenticate pubAdminEncGKey on a user group after a user group key r…
vitoreiji Dec 17, 2024
94ceffd
Update keynames according to new convention
hdcos Jan 13, 2025
45ff3ab
Expose HMAC-SHA-256 interface
vitoreiji Jan 7, 2025
d42bc74
use HMAC-SHA256 implementation in AES
vaf-hub Jan 8, 2025
de2a01c
add HMAC-SHA256 compatibility test
vaf-hub Jan 8, 2025
800ffd4
Expose HMAC-SHA-256 interface in the SDK
vitoreiji Jan 14, 2025
3344bee
Use hmac instead of encrypted hashes to authenticate keys.
vitoreiji Jan 14, 2025
72a5d60
Use new key derivation for user group key distribution
hdcos Jan 16, 2025
3a3c9d0
add constraints for key versions only accepting non-negative integers
sarashub Jan 20, 2025
bf312c4
enforce constraint that the client only accepts rsa keys in version 0
vaf-hub Jan 22, 2025
a99fdee
extract public_key_provider to load public keys in one place, enforce…
vaf-hub Jan 22, 2025
0332a5b
Refactor key derivations
vaf-hub Jan 16, 2025
933f0e3
enforce version constraints for rsa key pair in typescript
vaf-hub Jan 23, 2025
5be8417
enforce version constraints for rsa key pair in rust
vaf-hub Jan 27, 2025
fe1347a
Fix rust error by removing superfluous to_string
vitoreiji Jan 29, 2025
a5c0e47
Remove Local Admin Group handling
hdcos Dec 12, 2024
4c53ea5
Deprecate RSA encryption for mails
vitoreiji Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,075 changes: 3,553 additions & 3,522 deletions app-ios/tutanotaTests/CompatibilityTestData.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/node-mimimi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/node-mimimi/src/importer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ impl ImportEssential {
ownerGroup: self.target_owner_group.clone(),
encImports: serialized_imports,
targetMailFolder: self.target_mailset.clone(),
ownerKeyVersion: owner_enc_sk_for_import_post.version,
ownerKeyVersion: owner_enc_sk_for_import_post.version as i64,
ownerEncSessionKey: owner_enc_sk_for_import_post.object,
newImportedMailSetName: "@internal-imported-mailset".to_string(),
mailState: remote_state_id,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-mimimi/src/importer/importable_mail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl ImportableMailAttachmentMetaData {
ImportAttachment {
_id: None,
ownerEncFileSessionKey: owner_enc_file_session_key.object,
ownerFileKeyVersion: owner_enc_file_session_key.version,
ownerFileKeyVersion: owner_enc_file_session_key.version as i64,
existingAttachmentFile: None,
newAttachment: Some(NewImportAttachment {
_id: None,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-mimimi/src/reduce_to_chunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl AttachmentUploadData {
let eml_file_path = importable_mail.eml_file_path.clone();
let attachments = importable_mail.take_out_attachments();
let import_mail_data = importable_mail
.make_import_mail_data(owner_enc_session_key.object, owner_enc_session_key.version);
.make_import_mail_data(owner_enc_session_key.object, owner_enc_session_key.version as i64);

AttachmentUploadData {
keyed_import_mail_data: KeyedImportMailData {
Expand Down
13 changes: 4 additions & 9 deletions packages/tutanota-crypto/lib/encryption/Aes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import sjcl from "../internal/sjcl.js"
import { random } from "../random/Randomizer.js"
import { BitArray, bitArrayToUint8Array, uint8ArrayToBitArray } from "../misc/Utils.js"
import { arrayEquals, concat, uint8ArrayToBase64 } from "@tutao/tutanota-utils"
import { assertNotNull, concat, uint8ArrayToBase64 } from "@tutao/tutanota-utils"
import { sha256Hash } from "../hashes/Sha256.js"
import { CryptoError } from "../misc/CryptoError.js"
import { sha512Hash } from "../hashes/Sha512.js"
import { hmacSha256, MacTag, verifyHmacSha256 } from "./Hmac.js"

export const ENABLE_MAC = true
export const IV_BYTE_LENGTH = 16
Expand Down Expand Up @@ -60,8 +61,7 @@ export function aesEncrypt(key: AesKey, bytes: Uint8Array, iv: Uint8Array = gene
let data = concat(iv, bitArrayToUint8Array(encryptedBits))

if (useMac) {
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
let macBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(data)))
const macBytes = hmacSha256(assertNotNull(subKeys.mKey), data)
data = concat(new Uint8Array([MAC_ENABLED_PREFIX]), data, macBytes)
}

Expand Down Expand Up @@ -151,12 +151,7 @@ function aesDecryptImpl(key: AesKey, encryptedBytes: Uint8Array, usePadding: boo
if (hasMac) {
cipherTextWithoutMac = encryptedBytes.subarray(1, encryptedBytes.length - MAC_LENGTH_BYTES)
const providedMacBytes = encryptedBytes.subarray(encryptedBytes.length - MAC_LENGTH_BYTES)
const hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
const computedMacBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(cipherTextWithoutMac)))

if (!arrayEquals(providedMacBytes, computedMacBytes)) {
throw new CryptoError("invalid mac")
}
verifyHmacSha256(assertNotNull(subKeys.mKey), cipherTextWithoutMac, providedMacBytes as MacTag)
} else {
cipherTextWithoutMac = encryptedBytes
}
Expand Down
26 changes: 26 additions & 0 deletions packages/tutanota-crypto/lib/encryption/Hmac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AesKey } from "./Aes.js"
import sjcl from "../internal/sjcl.js"
import { bitArrayToUint8Array, uint8ArrayToBitArray } from "../misc/Utils.js"
import { arrayEquals } from "@tutao/tutanota-utils"
import { CryptoError } from "../misc/CryptoError.js"

export type MacTag = Uint8Array & { __brand: "macTag" }

/**
* Create an HMAC-SHA-256 tag over the given data using the given key.
*/
export function hmacSha256(key: AesKey, data: Uint8Array): MacTag {
const hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256)
return bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(data))) as MacTag
}

/**
* Verify an HMAC-SHA-256 tag against the given data and key.
* @throws CryptoError if the tag does not match the data and key.
*/
export function verifyHmacSha256(key: AesKey, data: Uint8Array, tag: MacTag) {
const computedTag = hmacSha256(key, data)
if (!arrayEquals(computedTag, tag)) {
throw new CryptoError("invalid mac")
}
}
20 changes: 20 additions & 0 deletions packages/tutanota-crypto/lib/encryption/KeyEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import type { PQKeyPairs } from "./PQKeyPairs.js"

export type EncryptedKeyPairs = EncryptedPqKeyPairs | EncryptedRsaKeyPairs | EncryptedRsaEccKeyPairs

export type AbstractEncryptedKeyPair = {
pubEccKey: null | Uint8Array
pubKyberKey: null | Uint8Array
pubRsaKey: null | Uint8Array
symEncPrivEccKey: null | Uint8Array
symEncPrivKyberKey: null | Uint8Array
symEncPrivRsaKey: null | Uint8Array
}

export type EncryptedPqKeyPairs = {
pubEccKey: Uint8Array
pubKyberKey: Uint8Array
Expand Down Expand Up @@ -38,6 +47,17 @@ export type EncryptedRsaEccKeyPairs = {
symEncPrivRsaKey: Uint8Array
}

export function isEncryptedPqKeyPairs(keyPair: AbstractEncryptedKeyPair): keyPair is EncryptedPqKeyPairs {
return (
keyPair.pubEccKey != null &&
keyPair.pubKyberKey != null &&
keyPair.symEncPrivEccKey != null &&
keyPair.symEncPrivKyberKey != null &&
keyPair.pubRsaKey == null &&
keyPair.symEncPrivRsaKey == null
)
}

export function encryptKey(encryptionKey: AesKey, keyToBeEncrypted: AesKey): Uint8Array {
const keyLength = getKeyLengthBytes(encryptionKey)
if (keyLength === KEY_LENGTH_BYTES_AES_128) {
Expand Down
3 changes: 3 additions & 0 deletions packages/tutanota-crypto/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ export {
} from "./hashes/Argon2id/Argon2id.js"
export { KeyLength, EntropySource, HkdfKeyDerivationDomains } from "./misc/Constants.js"
export {
AbstractEncryptedKeyPair,
EncryptedKeyPairs,
EncryptedPqKeyPairs,
EncryptedRsaKeyPairs,
EncryptedRsaEccKeyPairs,
isEncryptedPqKeyPairs,
encryptKey,
decryptKey,
encryptRsaKey,
Expand Down Expand Up @@ -94,3 +96,4 @@ export {
} from "./misc/Utils.js"
export { murmurHash } from "./hashes/MurmurHash.js"
export { hkdf } from "./hashes/HKDF.js"
export { hmacSha256, verifyHmacSha256, MacTag } from "./encryption/Hmac.js"
10 changes: 9 additions & 1 deletion packages/tutanota-crypto/lib/misc/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ export enum KeyLength {
b128 = "128",
b256 = "256",
}

export type EntropySource = "mouse" | "touch" | "key" | "random" | "static" | "time" | "accel"

export type HkdfKeyDerivationDomains = "userGroupKeyDistributionKey" | "adminGroupKeyRotationHash"
export type HkdfKeyDerivationDomains =
| "userGroupKeyDistributionKey"
| "newAdminPubKeyAuthKeyForUserGroupKeyRotation"
| "adminGroupDistributionKeyPairEncryptionKey"
| "adminGroupDistKeyPairAuthKeyForMultiAdminRotation"
| "newAdminSymKeyAuthKeyForMultiAdminRotationAsUser"
| "newUserGroupKeyAuthKeyForRotationAsNonAdminUser"
| "versionedUserGroupKeyDistributionKey"
6 changes: 5 additions & 1 deletion packages/tutanota-crypto/lib/misc/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function padAes(bytes: Uint8Array): Uint8Array {
padding.fill(paddingLength)
return concat(bytes, padding)
}

export function unpadAes(bytes: Uint8Array): Uint8Array {
let paddingLength = bytes[bytes.byteLength - 1]

Expand All @@ -38,6 +39,7 @@ export function createAuthVerifier(passwordKey: AesKey): Uint8Array {
// TODO Compatibility Test
return sha256Hash(bitArrayToUint8Array(passwordKey))
}

export function createAuthVerifierAsBase64Url(passwordKey: AesKey): Base64Url {
return base64ToBase64Url(uint8ArrayToBase64(createAuthVerifier(passwordKey)))
}
Expand Down Expand Up @@ -101,10 +103,12 @@ export function base64ToKey(base64: Base64): BitArray {
}
}

export function uint8ArrayToKey(array: Uint8Array): BitArray {
export function uint8ArrayToKey(array: Uint8Array): AesKey {
return base64ToKey(uint8ArrayToBase64(array))
}

export function keyToUint8Array(key: BitArray): Uint8Array {
return base64ToUint8Array(keyToBase64(key))
}

export const fixedIv: Uint8Array = hexToUint8Array("88888888888888888888888888888888")
16 changes: 12 additions & 4 deletions packages/tutanota-crypto/test/AesTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import o from "@tutao/otest"
import type { Hex } from "@tutao/tutanota-utils"
import { concat, hexToUint8Array, stringToUtf8Uint8Array, uint8ArrayToBase64, uint8ArrayToHex, utf8Uint8ArrayToString } from "@tutao/tutanota-utils"
import {
assertNotNull,
concat,
Hex,
hexToUint8Array,
stringToUtf8Uint8Array,
uint8ArrayToBase64,
uint8ArrayToHex,
utf8Uint8ArrayToString,
} from "@tutao/tutanota-utils"
import {
_aes128RandomKey,
Aes256Key,
Expand All @@ -20,6 +28,7 @@ import { CryptoError } from "../lib/misc/CryptoError.js"
import { random } from "../lib/random/Randomizer.js"
import { assertThrows, throwsErrorWithMessage } from "@tutao/tutanota-test-utils"
import sjcl from "../lib/internal/sjcl.js"
import { hmacSha256 } from "../lib/index.js"

o.spec("aes", function () {
o("encryption roundtrip 128 without mac", () => arrayRoundtrip(aesEncrypt, aesDecrypt, _aes128RandomKey(), false))
Expand Down Expand Up @@ -246,8 +255,7 @@ export function aes256EncryptLegacy(key: Aes256Key, bytes: Uint8Array, iv: Uint8
let data = concat(iv, bitArrayToUint8Array(encryptedBits))

if (useMac) {
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
let macBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(data)))
const macBytes = hmacSha256(assertNotNull(subKeys.mKey), data)
data = concat(new Uint8Array([MAC_ENABLED_PREFIX]), data, macBytes)
}

Expand Down
27 changes: 27 additions & 0 deletions packages/tutanota-crypto/test/HmacTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import o from "@tutao/otest"
import { aes256RandomKey, hmacSha256, verifyHmacSha256 } from "../lib/index.js"
import { assertThrows } from "@tutao/tutanota-test-utils"
import { CryptoError } from "../lib/misc/CryptoError.js"

o.spec("hmac", function () {
o("round trip", function () {
const key = aes256RandomKey()
const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6])
const tag = hmacSha256(key, data)
verifyHmacSha256(key, data, tag)
})
o("throws if data is not the same", async function () {
const key = aes256RandomKey()
const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6])
const badData = new Uint8Array([6, 5, 4, 3, 2, 1, 0])
const tag = hmacSha256(key, data)
await assertThrows(CryptoError, async () => verifyHmacSha256(key, badData, tag))
})
o("throws if key is not the same", async function () {
const key = aes256RandomKey()
const badKey = aes256RandomKey()
const data = new Uint8Array([0, 1, 2, 3, 4, 5, 6])
const tag = hmacSha256(key, data)
await assertThrows(CryptoError, async () => verifyHmacSha256(badKey, data, tag))
})
})
1 change: 1 addition & 0 deletions packages/tutanota-crypto/test/Suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "./Sha256Test.js"
import "./TotpVerifierTest.js"
import "./EccTest.js"
import "./KyberTest.js"
import "./HmacTest.js"
import { bootstrapTests } from "./bootstrap.js"

await bootstrapTests()
Expand Down
19 changes: 18 additions & 1 deletion packages/tutanota-utils/lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,29 @@ export type lazy<T> = () => T
export type lazyAsync<T> = () => Promise<T>
export type Thunk = () => unknown

/**
* Integer constraint from 0 to n (using tail-recursion elimination)
*/
type Enumerate<N extends number, Acc extends number[] = []> = Acc["length"] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc["length"]]>

/**
* A key version must be an integer between 0 and 100.
*
* The constraint to < 100 is arbitrary and must be changed when we rotate keys more often.
*/
export type KeyVersion = Enumerate<100>

export function isKeyVersion(version: number): version is KeyVersion {
// we do not check the upper boundary (100) because this is just a limitation of the type system not a real one
return Number.isInteger(version) && version >= 0
}

/**
* A group key and its version.
*/
export type Versioned<T> = {
object: T
version: number
version: KeyVersion
}

/**
Expand Down
15 changes: 14 additions & 1 deletion packages/tutanota-utils/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,21 @@ export {
memoizedWithHiddenArgument,
BoundedExecutor,
freshVersioned,
isKeyVersion,
} from "./Utils.js"
export type {
Callback,
DeferredObject,
lazy,
lazyAsync,
Thunk,
DeferredObjectWithHandler,
MaybeLazy,
TimeoutSetter,
ErrorInfo,
Versioned,
KeyVersion,
} from "./Utils.js"
export type { Callback, DeferredObject, lazy, lazyAsync, Thunk, DeferredObjectWithHandler, MaybeLazy, TimeoutSetter, ErrorInfo, Versioned } from "./Utils.js"

export {
callWebAssemblyFunctionWithArguments,
Expand Down
Loading
Loading