Skip to content

Commit

Permalink
Enforce brace and indent style
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed Jan 10, 2025
1 parent fc58f5b commit 6fb6792
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 210 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ module.exports = {
rules: {
'@stylistic/semi': ["error", "never"],
'@stylistic/quotes': ["error", "double"],
"@stylistic/brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"@stylistic/indent": ["error", 4],
'curly': ["error", "multi-line"],

'eqeqeq': "error",
'no-var': "error",
Expand Down
197 changes: 100 additions & 97 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,132 +9,135 @@ import { decryptSTREAM, encryptSTREAM } from "./stream.js"
export { Stanza }

export interface Identity {
unwrapFileKey(stanzas: Stanza[]): Uint8Array | null | Promise<Uint8Array | null>;
unwrapFileKey(stanzas: Stanza[]): Uint8Array | null | Promise<Uint8Array | null>;
}

export interface Recipient {
wrapFileKey(fileKey: Uint8Array): Stanza[] | Promise<Stanza[]>;
wrapFileKey(fileKey: Uint8Array): Stanza[] | Promise<Stanza[]>;
}

export { generateIdentity, identityToRecipient } from "./recipients.js"

export class Encrypter {
private passphrase: string | null = null
private scryptWorkFactor = 18
private recipients: Recipient[] = []

setPassphrase(s: string): void {
if (this.passphrase !== null)
throw new Error("can encrypt to at most one passphrase")
if (this.recipients.length !== 0)
throw new Error("can't encrypt to both recipients and passphrases")
this.passphrase = s
}

setScryptWorkFactor(logN: number): void {
this.scryptWorkFactor = logN
}

addRecipient(s: string | Recipient): void {
if (this.passphrase !== null)
throw new Error("can't encrypt to both recipients and passphrases")

if (typeof s === "string") {
this.recipients.push(new X25519Recipient(s))
} else {
this.recipients.push(s)
private passphrase: string | null = null
private scryptWorkFactor = 18
private recipients: Recipient[] = []

setPassphrase(s: string): void {
if (this.passphrase !== null) {
throw new Error("can encrypt to at most one passphrase")
}
if (this.recipients.length !== 0) {
throw new Error("can't encrypt to both recipients and passphrases")
}
this.passphrase = s
}
}

async encrypt(file: Uint8Array | string): Promise<Uint8Array> {
if (typeof file === "string") {
file = new TextEncoder().encode(file)
setScryptWorkFactor(logN: number): void {
this.scryptWorkFactor = logN
}

const fileKey = randomBytes(16)
const stanzas: Stanza[] = []
addRecipient(s: string | Recipient): void {
if (this.passphrase !== null) {
throw new Error("can't encrypt to both recipients and passphrases")
}

let recipients = this.recipients
if (this.passphrase !== null) {
recipients = [new ScryptRecipient(this.passphrase, this.scryptWorkFactor)]
if (typeof s === "string") {
this.recipients.push(new X25519Recipient(s))
} else {
this.recipients.push(s)
}
}
for (const recipient of recipients) {
stanzas.push(...await recipient.wrapFileKey(fileKey))
}

const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
const mac = hmac(sha256, hmacKey, encodeHeaderNoMAC(stanzas))
const header = encodeHeader(stanzas, mac)

const nonce = randomBytes(16)
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
const payload = encryptSTREAM(streamKey, file)

const out = new Uint8Array(header.length + nonce.length + payload.length)
out.set(header)
out.set(nonce, header.length)
out.set(payload, header.length + nonce.length)
return out
}
async encrypt(file: Uint8Array | string): Promise<Uint8Array> {
if (typeof file === "string") {
file = new TextEncoder().encode(file)
}

const fileKey = randomBytes(16)
const stanzas: Stanza[] = []

let recipients = this.recipients
if (this.passphrase !== null) {
recipients = [new ScryptRecipient(this.passphrase, this.scryptWorkFactor)]
}
for (const recipient of recipients) {
stanzas.push(...await recipient.wrapFileKey(fileKey))
}

const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
const mac = hmac(sha256, hmacKey, encodeHeaderNoMAC(stanzas))
const header = encodeHeader(stanzas, mac)

const nonce = randomBytes(16)
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
const payload = encryptSTREAM(streamKey, file)

const out = new Uint8Array(header.length + nonce.length + payload.length)
out.set(header)
out.set(nonce, header.length)
out.set(payload, header.length + nonce.length)
return out
}
}

export class Decrypter {
private identities: Identity[] = []

addPassphrase(s: string): void {
this.identities.push(new ScryptIdentity(s))
}
private identities: Identity[] = []

addIdentity(s: string | CryptoKey | Identity): void {
if (typeof s === "string" || isCryptoKey(s)) {
this.identities.push(new X25519Identity(s))
} else {
this.identities.push(s)
}
}

async decrypt(file: Uint8Array, outputFormat?: "uint8array"): Promise<Uint8Array>
async decrypt(file: Uint8Array, outputFormat: "text"): Promise<string>
async decrypt(file: Uint8Array, outputFormat?: "text" | "uint8array"): Promise<string | Uint8Array> {
const h = parseHeader(file)
const fileKey = await this.unwrapFileKey(h.stanzas)
if (fileKey === null) {
throw Error("no identity matched any of the file's recipients")
addPassphrase(s: string): void {
this.identities.push(new ScryptIdentity(s))
}

const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
const mac = hmac(sha256, hmacKey, h.headerNoMAC)
if (!compareBytes(h.MAC, mac)) {
throw Error("invalid header HMAC")
addIdentity(s: string | CryptoKey | Identity): void {
if (typeof s === "string" || isCryptoKey(s)) {
this.identities.push(new X25519Identity(s))
} else {
this.identities.push(s)
}
}

const nonce = h.rest.subarray(0, 16)
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
const payload = h.rest.subarray(16)

const out = decryptSTREAM(streamKey, payload)
if (outputFormat === "text") return new TextDecoder().decode(out)
return out
}
async decrypt(file: Uint8Array, outputFormat?: "uint8array"): Promise<Uint8Array>
async decrypt(file: Uint8Array, outputFormat: "text"): Promise<string>
async decrypt(file: Uint8Array, outputFormat?: "text" | "uint8array"): Promise<string | Uint8Array> {
const h = parseHeader(file)
const fileKey = await this.unwrapFileKey(h.stanzas)
if (fileKey === null) {
throw Error("no identity matched any of the file's recipients")
}

const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
const mac = hmac(sha256, hmacKey, h.headerNoMAC)
if (!compareBytes(h.MAC, mac)) {
throw Error("invalid header HMAC")
}

const nonce = h.rest.subarray(0, 16)
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
const payload = h.rest.subarray(16)

const out = decryptSTREAM(streamKey, payload)
if (outputFormat === "text") return new TextDecoder().decode(out)
return out
}

private async unwrapFileKey(stanzas: Stanza[]): Promise<Uint8Array | null> {
for (const identity of this.identities) {
const fileKey = await identity.unwrapFileKey(stanzas)
if (fileKey !== null) return fileKey
private async unwrapFileKey(stanzas: Stanza[]): Promise<Uint8Array | null> {
for (const identity of this.identities) {
const fileKey = await identity.unwrapFileKey(stanzas)
if (fileKey !== null) return fileKey
}
return null
}
return null
}
}

function compareBytes(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) { return false }
let acc = 0
for (let i = 0; i < a.length; i++) {
acc |= a[i] ^ b[i]
}
return acc === 0
if (a.length !== b.length) { return false }
let acc = 0
for (let i = 0; i < a.length; i++) {
acc |= a[i] ^ b[i]
}
return acc === 0
}

function isCryptoKey(key: unknown): key is CryptoKey {
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey
}
9 changes: 3 additions & 6 deletions lib/recipients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ export async function identityToRecipient(identity: string | CryptoKey): Promise
const res = bech32.decodeToBytes(identity)
if (!identity.startsWith("AGE-SECRET-KEY-1") ||
res.prefix.toUpperCase() !== "AGE-SECRET-KEY-" ||
res.bytes.length !== 32)
throw Error("invalid identity")
res.bytes.length !== 32) { throw Error("invalid identity") }
scalar = res.bytes
}

Expand All @@ -39,8 +38,7 @@ export class X25519Recipient implements Recipient {
const res = bech32.decodeToBytes(s)
if (!s.startsWith("age1") ||
res.prefix.toLowerCase() !== "age" ||
res.bytes.length !== 32)
throw Error("invalid recipient")
res.bytes.length !== 32) { throw Error("invalid recipient") }
this.recipient = res.bytes
}

Expand Down Expand Up @@ -71,8 +69,7 @@ export class X25519Identity implements Identity {
const res = bech32.decodeToBytes(s)
if (!s.startsWith("AGE-SECRET-KEY-1") ||
res.prefix.toUpperCase() !== "AGE-SECRET-KEY-" ||
res.bytes.length !== 32)
throw Error("invalid identity")
res.bytes.length !== 32) { throw Error("invalid identity") }
this.identity = res.bytes
this.recipient = x25519.scalarMultBase(res.bytes)
}
Expand Down
9 changes: 6 additions & 3 deletions lib/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ export function decryptSTREAM(key: Uint8Array, ciphertext: Uint8Array): Uint8Arr
streamNonce[11] = 1 // Last chunk flag.
const chunk = chacha20poly1305(key, streamNonce).decrypt(ciphertext)
plaintextSlice.set(chunk)
if (chunk.length === 0 && plaintext.length !== 0)
if (chunk.length === 0 && plaintext.length !== 0) {
throw Error("empty final chunk")
if (plaintextSlice.length !== chunk.length)
}
if (plaintextSlice.length !== chunk.length) {
throw Error("stream: internal error: didn't fill expected plaintext buffer")
}

return plaintext
}
Expand Down Expand Up @@ -65,8 +67,9 @@ export function encryptSTREAM(key: Uint8Array, plaintext: Uint8Array): Uint8Arra
streamNonce[11] = 1 // Last chunk flag.
const chunk = chacha20poly1305(key, streamNonce).encrypt(plaintext)
ciphertextSlice.set(chunk)
if (ciphertextSlice.length !== chunk.length)
if (ciphertextSlice.length !== chunk.length) {
throw Error("stream: internal error: didn't fill expected ciphertext buffer")
}

return ciphertext
}
Loading

0 comments on commit 6fb6792

Please sign in to comment.