Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"postinstall": "patch-package",
"prepare": "husky install",
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"test:update": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js -u",
"spellcheck": "cspell lint --no-must-find-files"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/encryption/src/encryption/AppendEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
IdentifierMap,
ECKeyIdentifier,
} from '@masknet/shared-base'
import type { EncryptIO, EncryptionResultE2E } from './Encryption'
import type { EncryptIO, EncryptionResultE2E } from './EncryptionTypes'

export interface AppendEncryptionOptions {
version: -39 | -38 | -37
Expand Down
1 change: 1 addition & 0 deletions packages/encryption/src/encryption/DecryptionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface DecryptSuccess {
type: DecryptProgressKind.Success
content: TypedMessage
}
// TODO: rename as DecryptErrorReasons
export enum ErrorReasons {
PayloadBroken = '[@masknet/encryption] Payload is broken.',
PayloadDecryptedButTypedMessageBroken = "[@masknet/encryption] Payload decrypted, but it's inner TypedMessage is broken.",
Expand Down
147 changes: 89 additions & 58 deletions packages/encryption/src/encryption/Encryption.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,96 @@
import type { TypedMessage } from '@masknet/typed-message'
import type {
PersonaIdentifier,
ProfileIdentifier,
AESCryptoKey,
PostIVIdentifier,
EC_Public_CryptoKey,
EC_Private_CryptoKey,
ECKeyIdentifier,
IdentifierMap,
} from '@masknet/shared-base'
import { encodeArrayBuffer, unreachable } from '@dimensiondev/kit'
import { AESCryptoKey, PostIVIdentifier, ProfileIdentifier } from '@masknet/shared-base'
import { isTypedMessageText, encodeTypedMessageV38Format, encodeTypedMessageToDocument } from '@masknet/typed-message'
import { None, Option, Some } from 'ts-results'
import {
AESAlgorithmEnum,
AsymmetryCryptoKey,
encodePayload,
PayloadWellFormed,
PublicKeyAlgorithmEnum,
} from '../payload'
import { encryptWithAES } from '../utils'

export interface EncryptOptions {
/** Payload version to use. */
version: -38 | -37
/** Current author who started the encryption. */
whoAmI: ProfileIdentifier | PersonaIdentifier
/** The message to be encrypted. */
message: TypedMessage
/** Encryption target. */
target: EncryptTargetPublic | EncryptTargetE2E
import { EncryptError, EncryptErrorReasons, EncryptIO, EncryptOptions, EncryptResult } from './EncryptionTypes'

export * from './EncryptionTypes'
export async function encrypt(options: EncryptOptions, io: EncryptIO): Promise<EncryptResult> {
if (options.target.type === 'public') return encryptionPublic(options, io)
if (options.version === -38) {
return v38EncryptionE2E(options, io)
} else if (options.version === -37) {
return v37EncryptionE2E(options, io)
}
unreachable(options.version)
}

async function encryptionPublic(options: EncryptOptions, io: EncryptIO): Promise<EncryptResult> {
let message: Uint8Array

if (options.version === -38) {
if (!isTypedMessageText(options.message)) {
throw new EncryptError(EncryptErrorReasons.ComplexTypedMessageNotSupportedInPayload38)
}
message = encodeTypedMessageV38Format(options.message)
} else {
message = encodeTypedMessageToDocument(options.message)
}

const iv = getIV(io)
const authorPublic = queryAuthorPublicKey(options.author, io)

const postKey = await aes256GCM(io)
const encrypted = (await encryptWithAES(AESAlgorithmEnum.A256GCM, postKey, iv, message)).unwrap()

const encryption: PayloadWellFormed.PublicEncryption = {
iv,
type: 'public',
AESKey: { algr: AESAlgorithmEnum.A256GCM, key: postKey },
}
const payload: PayloadWellFormed.Payload = {
version: options.version,
author: options.author.isUnknown ? None : Some(options.author),
authorPublicKey: await authorPublic,
encryption,
encrypted,
signature: None,
}
return {
postKey,
identifier: new PostIVIdentifier(options.author.network, encodeArrayBuffer(iv)),
output: (await encodePayload.NoSign(payload)).unwrap(),
author: options.author,
}
}
export interface EncryptTargetPublic {
type: 'public'
async function v38EncryptionE2E(options: EncryptOptions, io: EncryptIO): Promise<EncryptResult> {
throw new Error('Not implemented')
}
export interface EncryptTargetE2E {
type: 'E2E'
target: (ProfileIdentifier | PersonaIdentifier)[]
async function v37EncryptionE2E(options: EncryptOptions, io: EncryptIO): Promise<EncryptResult> {
throw new Error('Not implemented')
}
export interface EncryptIO {
queryLinkedPersona(profile: ProfileIdentifier): Promise<PersonaIdentifier | null>
queryPublicKey(persona: PersonaIdentifier): Promise<EC_Public_CryptoKey | null>
queryLocalKey(id: ProfileIdentifier | PersonaIdentifier): Promise<AESCryptoKey | null>
queryPrivateKey(persona: PersonaIdentifier): Promise<EC_Private_CryptoKey | null>
/**
* Fill the arr with random values.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomValues?(arr: Uint8Array): Uint8Array
/**
* Generate a new AES Key.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomAESKey?(): Promise<AESCryptoKey>
/**
* Generate a pair of new EC key used for ECDH.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomECKey?(algr: 'ed25519' | 'P-256' | 'K-256'): Promise<[EC_Public_CryptoKey, EC_Private_CryptoKey]>

async function queryAuthorPublicKey(of: ProfileIdentifier, io: EncryptIO): Promise<Option<AsymmetryCryptoKey>> {
try {
const key = await io.queryPublicKey(of)
if (!key) return None
const k: AsymmetryCryptoKey = {
algr: PublicKeyAlgorithmEnum.secp256k1,
key,
}
return Some(k)
} catch (error) {
console.warn('[@masknet/encryption] Failed when query author public key', error)
return None
}
}
export interface EncryptResult {
encryptedBy: AESCryptoKey
output: string | Uint8Array
identifier: PostIVIdentifier
author: [ProfileIdentifier, PersonaIdentifier]
/** Additional information that need to be send to the internet in order to allow recipients to decrypt */
e2e?: IdentifierMap<ECKeyIdentifier, EncryptionResultE2E>
function getIV(io: EncryptIO): Uint8Array {
if (io.getRandomValues) return io.getRandomValues(new Uint8Array(16))
return crypto.getRandomValues(new Uint8Array(16))
}
export interface EncryptionResultE2E {
encryptedPostKey: Uint8Array
iv: Uint8Array
/** This feature is supported since v37. */
ephemeralPublicKey?: EC_Public_CryptoKey
async function aes256GCM(io: EncryptIO): Promise<AESCryptoKey> {
if (io.getRandomAESKey) return io.getRandomAESKey()
return (await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
'encrypt',
'decrypt',
])) as AESCryptoKey
}
declare function encrypt(options: EncryptOptions, io: EncryptIO): Promise<EncryptResult>
73 changes: 73 additions & 0 deletions packages/encryption/src/encryption/EncryptionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type {
ProfileIdentifier,
PersonaIdentifier,
EC_Public_CryptoKey,
AESCryptoKey,
EC_Private_CryptoKey,
PostIVIdentifier,
IdentifierMap,
ECKeyIdentifier,
} from '@masknet/shared-base'
import type { SerializableTypedMessages } from '@masknet/typed-message'

export interface EncryptOptions {
/** Payload version to use. */
version: -38 | -37
/** Current author who started the encryption. */
author: ProfileIdentifier
/** The message to be encrypted. */
message: SerializableTypedMessages
/** Encryption target. */
target: EncryptTargetPublic | EncryptTargetE2E
}
export interface EncryptTargetPublic {
type: 'public'
}
export interface EncryptTargetE2E {
type: 'E2E'
target: (ProfileIdentifier | PersonaIdentifier)[]
}
export interface EncryptIO {
queryLinkedPersona(profile: ProfileIdentifier): Promise<PersonaIdentifier | null>
queryPublicKey(persona: ProfileIdentifier | PersonaIdentifier): Promise<EC_Public_CryptoKey | null>
queryLocalKey(id: ProfileIdentifier | PersonaIdentifier): Promise<AESCryptoKey | null>
queryPrivateKey(persona: PersonaIdentifier): Promise<EC_Private_CryptoKey | null>
/**
* Fill the arr with random values.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomValues?(arr: Uint8Array): Uint8Array
/**
* Generate a new AES Key.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomAESKey?(): Promise<AESCryptoKey>
/**
* Generate a pair of new EC key used for ECDH.
* This should be only provided in the test environment to create a deterministic result.
*/
getRandomECKey?(algr: 'ed25519' | 'P-256' | 'K-256'): Promise<[EC_Public_CryptoKey, EC_Private_CryptoKey]>
}
export interface EncryptResult {
postKey: AESCryptoKey
output: string | Uint8Array
identifier: PostIVIdentifier
author: ProfileIdentifier
/** Additional information that need to be send to the internet in order to allow recipients to decrypt */
e2e?: IdentifierMap<ECKeyIdentifier, EncryptionResultE2E>
}
export interface EncryptionResultE2E {
encryptedPostKey: Uint8Array
iv: Uint8Array
/** This feature is supported since v37. */
ephemeralPublicKey?: EC_Public_CryptoKey
}
export enum EncryptErrorReasons {
ComplexTypedMessageNotSupportedInPayload38 = '[@masknet/encryption] Complex TypedMessage is not supported in payload v38.',
}
export class EncryptError extends Error {
static Reasons = EncryptErrorReasons
constructor(public override message: EncryptErrorReasons, cause?: any) {
super(message, { cause })
}
}
1 change: 1 addition & 0 deletions packages/encryption/src/encryption/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Decryption'
export * from './Encryption'
17 changes: 17 additions & 0 deletions packages/encryption/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ export {
type PayloadParseResult,
type PayloadWellFormed,
} from './payload'

export {
encrypt,
EncryptError,
EncryptErrorReasons,
type EncryptOptions,
type EncryptIO,
type EncryptResult,
type EncryptTargetE2E,
type EncryptTargetPublic,
type EncryptionResultE2E,
} from './encryption'

export {
decrypt,
DecryptError,
DecryptProgressKind,
DecryptIntermediateProgressKind,
// TODO: rename to DecryptErrorReasons
ErrorReasons,
type DecryptOptions,
type DecryptIO,
Expand All @@ -26,12 +40,14 @@ export {
type DecryptReportedInfo,
type DecryptSuccess,
} from './encryption'

export {
socialNetworkEncoder,
socialNetworkDecoder,
TwitterDecoder,
__TwitterEncoder,
} from './social-network-encode-decode'

export {
type DecodeImageOptions,
type EncodeImageOptions,
Expand All @@ -41,5 +57,6 @@ export {
steganographyEncodeImage,
GrayscaleAlgorithm,
} from './image-steganography'

// TODO: remove them in the future
export { importAsymmetryKeyFromJsonWebKeyOrSPKI, importAESFromJWK } from './utils'
2 changes: 1 addition & 1 deletion packages/encryption/src/utils/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function encryptWithAES(kind: AESAlgorithmEnum, key: CryptoKey, iv: Uint8
[AESAlgorithmEnum.A256GCM]: { name: 'AES-GCM', iv } as AesGcmParams,
} as const
return Result.wrapAsync(() => {
return crypto.subtle.encrypt(param[kind], key, message) as Promise<Uint8Array>
return crypto.subtle.encrypt(param[kind], key, message).then((x) => new Uint8Array(x))
})
}
export function decryptWithAES(kind: AESAlgorithmEnum, key: CryptoKey, iv: Uint8Array, message: Uint8Array) {
Expand Down
Loading