You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/test/age_test.dart

284 lines
8.6 KiB

// age without files, so that it also runs compiled to JavaScript: files
// written here as age.Encrypt writes them, read back across the chunk
// boundaries of the STREAM, with the errors of Go for its end-of-file cases;
// the header parser on short inputs; the passphrase identity with small
// work factors; and the rules of agewrap. The texts are those that
// age_vectors_test.dart checks against Go.
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/age.dart';
import 'package:datekeys/src/agewrap.dart';
import 'package:datekeys/src/base64.dart';
import 'package:datekeys/src/chacha20poly1305.dart';
import 'package:datekeys/src/scrypt.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'age_support.dart';
Uint8List seeded(String label, [int n = 32]) {
final out = Uint8List(n);
for (var i = 0, c = 0; i < n; c++) {
final d = sha256([...label.codeUnits, c]);
for (var j = 0; j < 32 && i < n; j++) {
out[i++] = d[j];
}
}
return out;
}
String? failure(Uint8List file, List<AgeIdentity> ids) {
try {
ageDecrypt(file, ids);
return null;
} on AgeException catch (e) {
return '${e.phase.name}: ${e.message}';
} on DateKeysException catch (e) {
return 'identity: ${e.message}';
}
}
const stream =
'failed to decrypt and authenticate payload chunk, file may be '
'corrupted or tampered with';
void main() {
final id = X25519Identity(seeded('identity'));
final fileKey = seeded('file key', 16);
final nonce = seeded('nonce', 16);
Uint8List fileOf(Uint8List plaintext) => ageFile(
[x25519Stanza(fileKey, id.recipient, seeded('ephemeral'))],
fileKey,
nonce,
plaintext,
);
test('files across the chunk boundaries read back', () {
for (final n in [0, 1, 65535, 65536, 65537, 131072, 131073]) {
final p = pattern(n);
final file = fileOf(p);
expect(ageDecrypt(file, [id]), p, reason: '$n bytes');
final h = parseAgeHeader(file);
expect(
file.length,
h.length + 16 + n + 16 * (n == 0 ? 1 : (n + 65535) ~/ 65536),
);
}
});
test('the end-of-file cases of the STREAM', () {
final one = fileOf(pattern(65536));
final h = parseAgeHeader(one).length + 16;
expect(
failure(Uint8List.sublistView(one, 0, h), [id]),
'payload: unexpected EOF',
);
expect(
failure(
concatBytes([
one,
[0],
]),
[id],
),
'payload: trailing data after end of encrypted file',
);
expect(
failure(Uint8List.sublistView(one, 0, one.length - 1), [id]),
'payload: $stream',
);
final two = fileOf(pattern(65537));
expect(
failure(Uint8List.sublistView(two, 0, h + 65552), [id]),
'payload: unexpected EOF',
);
expect(
failure(Uint8List.sublistView(two, 0, h + 65552 + 16), [id]),
'payload: last chunk is empty, try age v1.0.0, and please consider reporting this',
);
expect(
failure(Uint8List.sublistView(two, 0, h - 1), [id]),
'header: failed to read nonce: unexpected EOF',
);
expect(
failure(Uint8List.sublistView(two, 0, h - 16), [id]),
'header: failed to read nonce: EOF',
);
final bad = Uint8List.fromList(two)..[h + 100] ^= 1;
expect(failure(bad, [id]), 'payload: $stream');
});
test('pieces of every size give the same plaintext', () {
final p = pattern(140000);
final file = fileOf(p);
for (final step in [1000, 65552, 65553, 70000]) {
final opened = ageOpen(file, [id]);
final out = BytesBuilder();
for (var i = opened.payloadOffset; i < file.length; i += step) {
final end = i + step < file.length ? i + step : file.length;
opened.payload.add(file, i, end).forEach(out.add);
}
out.add(opened.payload.close());
expect(out.takeBytes(), p);
}
});
test('a delivered chunk, then the trailing data on the next call', () {
final file = concatBytes([
fileOf(pattern(65536)),
[1, 2],
]);
final opened = ageOpen(file, [id]);
final chunks = opened.payload.add(file, opened.payloadOffset);
expect(chunks.single, pattern(65536));
expect(
opened.payload.close,
throwsA(
isA<AgeException>().having(
(e) => e.message,
'message',
'trailing data after end of encrypted file',
),
),
);
});
test('the header: identities, the MAC and the parser', () {
final file = fileOf(pattern(10));
final other = X25519Identity(seeded('other'));
expect(
failure(file, [other]),
'header: identity did not match any of the recipients: incorrect identity for recipient block',
);
expect(
failure(file, [other, other]),
'header: no identity matched any of the recipients',
);
expect(failure(file, []), 'header: no identities specified');
expect(ageDecrypt(file, [other, id]), pattern(10));
final mac = Uint8List.fromList(file)
..[parseAgeHeader(file).length - 5] ^= 1;
expect(
failure(mac, [id]),
anyOf(
'header: bad header MAC',
startsWith('header: failed to read header'),
),
);
expect(
failure(Uint8List(0), [id]),
'header: failed to read header: parsing age header: file is empty',
);
expect(
failure(Uint8List.fromList('age-encryption.org/v1\n---'.codeUnits), [id]),
'header: failed to read header: failed to read header: EOF',
);
});
test('the passphrase identity', () {
final key = seeded('scrypt file key', 16);
final salt = seeded('salt', 16);
AgeStanza stanza(String pass, String logN) {
final k = scrypt(
pass.codeUnits,
concatBytes(['age-encryption.org/v1/scrypt'.codeUnits, salt]),
1 << int.parse(logN),
8,
1,
32,
);
return AgeStanza('scrypt', [
goBase64Encode(salt, padded: false),
logN,
], chacha20Poly1305Seal(k, Uint8List(12), key));
}
final file = ageFile([stanza('pass', '2')], key, nonce, pattern(20));
expect(ageDecrypt(file, [ScryptIdentity('pass')]), pattern(20));
expect(
failure(file, [ScryptIdentity('wrong')]),
'header: identity did not match any of the recipients: incorrect identity for recipient block: incorrect passphrase',
);
final big = ageFile([stanza('pass', '3')], key, nonce, pattern(20));
expect(
failure(big, [ScryptIdentity('pass', maxWorkFactor: 2)]),
'header: scrypt work factor too large: 3',
);
expect(() => ScryptIdentity(''), throwsA(isA<AgeException>()));
expect(() => ScryptIdentity('p', maxWorkFactor: 31), throwsArgumentError);
expect(defaultMaxScryptWorkFactor, 16);
});
test('agewrap on files written here', () {
final payload = fileOf(pattern(3));
expect(ageDecrypt(payload, [PayloadIdentity(id.secretKey)]), pattern(3));
expect(
failure(payload, [PayloadIdentity(seeded('stranger'))]),
"identity: agewrap: PAYLOAD_AGE is not encrypted to this control's R_PAYLOAD: ERR_INTEGRITY",
);
expect(
failure(payload, [
AccessIdentity(accessSlots, [id]),
]),
'identity: agewrap: INNER_ACCESS_AGE has 1 stanzas, want exactly 16: ERR_POLICY_STRUCTURE_MISMATCH',
);
expect(
ageDecrypt(payload, [
AccessIdentity(0, [id]),
]),
pattern(3),
);
expect(ageStanzas(payload).single.type, stanzaX25519);
expect(
() => ageStanzas(Uint8List(0)),
throwsA(
isA<DateKeysException>().having(
(e) => e.code,
'code',
ErrorCode.integrity,
),
),
);
expect(
() => x25519IdentityFromRaw(Uint8List(31)),
throwsA(
isA<DateKeysException>().having(
(e) => e.message,
'message',
'agewrap: X25519 identity is 31 bytes, want 32: ERR_INTEGRITY',
),
),
);
});
test('an identity written in Bech32 and back', () {
final s = id.toString();
expect(s, startsWith('AGE-SECRET-KEY-1'));
expect(X25519Identity.parse(s).secretKey, id.secretKey);
expect(
() => X25519Identity.parse(s.toLowerCase()),
throwsA(
isA<AgeException>().having(
(e) => e.message,
'message',
'malformed secret key: unknown type "age-secret-key-"',
),
),
);
expect(id.recipientString, startsWith('age1'));
expect(
() => X25519Identity.parse(id.recipientString),
throwsA(
isA<AgeException>().having(
(e) => e.message,
'message',
'malformed secret key: unknown type "age"',
),
),
);
});
}

Powered by TurnKey Linux.